init commit,
This commit is contained in:
329
99_references/voyager-main/src/features/auth/authSlice.ts
Normal file
329
99_references/voyager-main/src/features/auth/authSlice.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { AppDispatch, RootState } from "../../store";
|
||||
import { getRemoteHandle, parseLemmyJWT } from "../../helpers/lemmy";
|
||||
import { resetPosts } from "../post/postSlice";
|
||||
import { getClient } from "../../services/lemmy";
|
||||
import { resetComments } from "../comment/commentSlice";
|
||||
import { resetUsers } from "../user/userSlice";
|
||||
import { resetInbox } from "../inbox/inboxSlice";
|
||||
import { differenceWith, uniqBy } from "lodash";
|
||||
import { resetCommunities } from "../community/communitySlice";
|
||||
import { ApplicationContext } from "capacitor-application-context";
|
||||
import { resetInstances } from "../instances/instancesSlice";
|
||||
import { resetResolve } from "../resolve/resolveSlice";
|
||||
import { resetMod } from "../moderation/modSlice";
|
||||
import { getInstanceFromHandle, instanceSelector } from "./authSelectors";
|
||||
import { receivedSite, resetSite } from "./siteSlice";
|
||||
import { Register } from "lemmy-js-client";
|
||||
import { setDefaultFeed } from "../settings/settingsSlice";
|
||||
import { getDefaultServer } from "../../services/app";
|
||||
|
||||
const MULTI_ACCOUNT_STORAGE_NAME = "credentials";
|
||||
|
||||
/**
|
||||
* DO NOT CHANGE this type. It is persisted.
|
||||
*/
|
||||
export type Credential = {
|
||||
jwt?: string;
|
||||
|
||||
/**
|
||||
* Can either be user handle or instance url.
|
||||
*
|
||||
* e.g. `aeharding@lemmy.world` or `lemmy.world`
|
||||
*/
|
||||
handle: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DO NOT CHANGE this type. It is persisted.
|
||||
*/
|
||||
type CredentialStoragePayload = {
|
||||
accounts: Credential[];
|
||||
|
||||
/**
|
||||
* Can either be user handle or instance url.
|
||||
*
|
||||
* e.g. `aeharding@lemmy.world` or `lemmy.world`
|
||||
*/
|
||||
activeHandle: string;
|
||||
};
|
||||
|
||||
interface AuthState {
|
||||
accountData: CredentialStoragePayload | undefined;
|
||||
connectedInstance: string;
|
||||
}
|
||||
|
||||
const initialState: (connectedInstance?: string) => AuthState = (
|
||||
connectedInstance = "",
|
||||
) => ({
|
||||
accountData: getCredentialsFromStorage(),
|
||||
connectedInstance,
|
||||
});
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
addAccount: (state, action: PayloadAction<Credential>) => {
|
||||
state.accountData ??= {
|
||||
accounts: [action.payload],
|
||||
activeHandle: action.payload.handle,
|
||||
};
|
||||
|
||||
let cleanedPreviousAccounts;
|
||||
|
||||
if (
|
||||
state.accountData.accounts.length === 1 &&
|
||||
!state.accountData.accounts[0]?.handle.includes("@")
|
||||
) {
|
||||
// If only one account, and it's a guest, just switch it out
|
||||
cleanedPreviousAccounts = [action.payload];
|
||||
} else {
|
||||
// Remove guest accounts for this instance when logging in
|
||||
cleanedPreviousAccounts = differenceWith(
|
||||
state.accountData.accounts,
|
||||
[getInstanceFromHandle(action.payload.handle)],
|
||||
(a, b) => a.handle === b,
|
||||
);
|
||||
}
|
||||
|
||||
const accounts = uniqBy(
|
||||
[action.payload, ...cleanedPreviousAccounts],
|
||||
(c) => c.handle,
|
||||
);
|
||||
|
||||
state.accountData = {
|
||||
accounts,
|
||||
activeHandle: action.payload.handle,
|
||||
};
|
||||
|
||||
updateCredentialsStorage(state.accountData);
|
||||
},
|
||||
removeAccount: (state, action: PayloadAction<string>) => {
|
||||
if (!state.accountData) return;
|
||||
|
||||
const accounts = differenceWith(
|
||||
state.accountData.accounts,
|
||||
[action.payload],
|
||||
(a, b) => a.handle === b,
|
||||
);
|
||||
|
||||
const nextAccount = accounts[0];
|
||||
|
||||
if (!nextAccount) {
|
||||
state.accountData = undefined;
|
||||
updateCredentialsStorage(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
state.accountData.accounts = accounts;
|
||||
if (state.accountData.activeHandle === action.payload) {
|
||||
state.accountData.activeHandle = nextAccount.handle;
|
||||
}
|
||||
|
||||
updateCredentialsStorage(state.accountData);
|
||||
},
|
||||
setPrimaryAccount: (state, action: PayloadAction<string>) => {
|
||||
if (!state.accountData) return;
|
||||
|
||||
state.accountData.activeHandle = action.payload;
|
||||
|
||||
updateCredentialsStorage(state.accountData);
|
||||
},
|
||||
setAccounts: (state, action: PayloadAction<Credential[]>) => {
|
||||
if (!state.accountData) return;
|
||||
|
||||
state.accountData.accounts = action.payload;
|
||||
|
||||
updateCredentialsStorage(state.accountData);
|
||||
},
|
||||
|
||||
reset: (state) => {
|
||||
return initialState(state.connectedInstance);
|
||||
},
|
||||
|
||||
updateConnectedInstance(state, action: PayloadAction<string>) {
|
||||
if (import.meta.env.VITE__TEST_MODE) {
|
||||
state.connectedInstance = getDefaultServer();
|
||||
return;
|
||||
}
|
||||
state.connectedInstance = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const {
|
||||
addAccount,
|
||||
removeAccount,
|
||||
setPrimaryAccount,
|
||||
setAccounts,
|
||||
reset,
|
||||
updateConnectedInstance,
|
||||
} = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
|
||||
export const login =
|
||||
(baseUrl: string, username: string, password: string, totp?: string) =>
|
||||
async (dispatch: AppDispatch) => {
|
||||
const client = getClient(baseUrl);
|
||||
|
||||
const res = await client.login({
|
||||
username_or_email: username,
|
||||
// lemmy-ui has maxlength of 60. If we don't clamp too users will complain password won't work
|
||||
password: password.slice(0, 60),
|
||||
totp_2fa_token: totp || undefined,
|
||||
});
|
||||
|
||||
if (!res.jwt) {
|
||||
// todo
|
||||
throw new Error("broke");
|
||||
}
|
||||
|
||||
await dispatch(addJwt(baseUrl, res.jwt));
|
||||
};
|
||||
|
||||
export const register =
|
||||
(baseUrl: string, register: Register) => async (dispatch: AppDispatch) => {
|
||||
const client = getClient(baseUrl);
|
||||
|
||||
const res = await client.register(register);
|
||||
|
||||
if (!res.jwt) {
|
||||
return res;
|
||||
}
|
||||
|
||||
await dispatch(addJwt(baseUrl, res.jwt));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const addGuestInstance =
|
||||
(url: string) => async (dispatch: AppDispatch) => {
|
||||
const client = getClient(url);
|
||||
|
||||
const site = await client.getSite();
|
||||
|
||||
dispatch(resetAccountSpecificStoreData());
|
||||
dispatch(receivedSite(site));
|
||||
dispatch(addAccount({ handle: url }));
|
||||
dispatch(updateConnectedInstance(url));
|
||||
};
|
||||
|
||||
const addJwt =
|
||||
(baseUrl: string, jwt: string) => async (dispatch: AppDispatch) => {
|
||||
const authenticatedClient = getClient(baseUrl, jwt);
|
||||
|
||||
const site = await authenticatedClient.getSite();
|
||||
const myUser = site.my_user?.local_user_view?.person;
|
||||
|
||||
if (!myUser) throw new Error("broke");
|
||||
|
||||
dispatch(resetAccountSpecificStoreData());
|
||||
dispatch(receivedSite(site));
|
||||
dispatch(addAccount({ jwt, handle: getRemoteHandle(myUser) }));
|
||||
dispatch(updateConnectedInstance(parseLemmyJWT(jwt).iss));
|
||||
};
|
||||
|
||||
const resetAccountSpecificStoreData = () => (dispatch: AppDispatch) => {
|
||||
dispatch(resetPosts());
|
||||
dispatch(resetComments());
|
||||
dispatch(resetUsers());
|
||||
dispatch(resetInbox());
|
||||
dispatch(resetCommunities());
|
||||
dispatch(resetResolve());
|
||||
dispatch(resetInstances());
|
||||
dispatch(resetMod());
|
||||
dispatch(resetSite());
|
||||
dispatch(setDefaultFeed(undefined));
|
||||
};
|
||||
|
||||
export const logoutEverything = () => (dispatch: AppDispatch) => {
|
||||
dispatch(reset());
|
||||
dispatch(resetAccountSpecificStoreData());
|
||||
};
|
||||
|
||||
export const changeAccount =
|
||||
(handle: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(resetAccountSpecificStoreData());
|
||||
dispatch(setPrimaryAccount(handle));
|
||||
|
||||
const instanceUrl = instanceSelector(getState());
|
||||
if (instanceUrl) dispatch(updateConnectedInstance(instanceUrl));
|
||||
};
|
||||
|
||||
export const logoutAccount =
|
||||
(handle: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const accountData = getState().auth.accountData;
|
||||
const currentAccount = accountData?.accounts?.find(
|
||||
({ handle: h }) => handle === h,
|
||||
);
|
||||
|
||||
// Going to need to change active accounts
|
||||
if (handle === accountData?.activeHandle) {
|
||||
dispatch(resetAccountSpecificStoreData());
|
||||
}
|
||||
|
||||
// revoke token
|
||||
if (currentAccount && currentAccount.jwt)
|
||||
getClient(
|
||||
parseLemmyJWT(currentAccount.jwt).iss,
|
||||
currentAccount.jwt,
|
||||
)?.logout();
|
||||
|
||||
dispatch(removeAccount(handle));
|
||||
|
||||
const instanceUrl = instanceSelector(getState());
|
||||
if (instanceUrl) dispatch(updateConnectedInstance(instanceUrl));
|
||||
};
|
||||
|
||||
function updateCredentialsStorage(
|
||||
accounts: CredentialStoragePayload | undefined,
|
||||
) {
|
||||
updateApplicationContextIfNeeded(accounts);
|
||||
|
||||
if (!accounts) {
|
||||
localStorage.removeItem(MULTI_ACCOUNT_STORAGE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(MULTI_ACCOUNT_STORAGE_NAME, JSON.stringify(accounts));
|
||||
}
|
||||
|
||||
function getCredentialsFromStorage(): CredentialStoragePayload | undefined {
|
||||
const serializedCredentials = localStorage.getItem(
|
||||
MULTI_ACCOUNT_STORAGE_NAME,
|
||||
);
|
||||
if (!serializedCredentials) return;
|
||||
return JSON.parse(serializedCredentials);
|
||||
}
|
||||
|
||||
// Run once on app load to sync state if needed
|
||||
updateApplicationContextIfNeeded(getCredentialsFromStorage());
|
||||
|
||||
/**
|
||||
* This syncs application state for the Apple Watch App
|
||||
*/
|
||||
function updateApplicationContextIfNeeded(
|
||||
accounts: CredentialStoragePayload | undefined,
|
||||
) {
|
||||
const DEFAULT_INSTANCE = "lemmy.world";
|
||||
|
||||
const connectedInstance = (() => {
|
||||
if (!accounts) return DEFAULT_INSTANCE;
|
||||
if (!accounts.activeHandle.includes("@")) return accounts.activeHandle;
|
||||
|
||||
return accounts.activeHandle.slice(
|
||||
accounts.activeHandle.lastIndexOf("@") + 1,
|
||||
);
|
||||
})();
|
||||
|
||||
ApplicationContext.updateApplicationContext({
|
||||
connectedInstance,
|
||||
authToken: accounts
|
||||
? (accounts.accounts.find((a) => a.handle === accounts.activeHandle)
|
||||
?.jwt ?? "")
|
||||
: "",
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user