diff --git a/src/actions/auth.ts b/src/actions/auth.ts index e1e5fddfe..8847c6b32 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -92,8 +92,8 @@ const createAppToken = () => const app = getState().auth.app; const params = { - client_id: app.client_id!, - client_secret: app.client_secret!, + client_id: app?.client_id, + client_secret: app?.client_secret, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', grant_type: 'client_credentials', scope: getScopes(getState()), @@ -109,8 +109,8 @@ const createUserToken = (username: string, password: string) => const app = getState().auth.app; const params = { - client_id: app.client_id!, - client_secret: app.client_secret!, + client_id: app?.client_id, + client_secret: app?.client_secret, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', grant_type: 'password', username: username, @@ -126,8 +126,8 @@ export const otpVerify = (code: string, mfa_token: string) => (dispatch: AppDispatch, getState: () => RootState) => { const app = getState().auth.app; return api(getState, 'app').post('/oauth/mfa/challenge', { - client_id: app.client_id, - client_secret: app.client_secret, + client_id: app?.client_id, + client_secret: app?.client_secret, mfa_token: mfa_token, code: code, challenge_type: 'totp', @@ -208,12 +208,12 @@ export const logOut = (refresh = true) => if (!account) return dispatch(noOp); const params = { - client_id: state.auth.app.client_id!, - client_secret: state.auth.app.client_secret!, - token: state.auth.users.get(account.url)!.access_token, + client_id: state.auth.app?.client_id, + client_secret: state.auth.app?.client_secret, + token: state.auth.users[account.url]?.access_token, }; - return dispatch(revokeOAuthToken(params)) + return dispatch(revokeOAuthToken(params as Record)) .finally(() => { // Clear all stored cache from React Query queryClient.invalidateQueries(); @@ -246,7 +246,7 @@ export const switchAccount = (accountId: string, background = false) => export const fetchOwnAccounts = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - return state.auth.users.forEach((user) => { + return Object.values(state.auth.users).forEach((user) => { const account = selectAccount(state, user.id); if (!account) { dispatch(verifyCredentials(user.access_token, user.url)) diff --git a/src/actions/me.test.ts b/src/actions/me.test.ts deleted file mode 100644 index fe6ddd13a..000000000 --- a/src/actions/me.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { __stub } from 'soapbox/api'; -import { buildAccount } from 'soapbox/jest/factory'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers'; -import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth'; - -import { fetchMe, patchMe } from './me'; - -vi.mock('../../storage/kv-store', () => ({ - __esModule: true, - default: { - getItemOrError: vi.fn().mockReturnValue(Promise.resolve({})), - }, -})); - -let store: ReturnType; - -describe('fetchMe()', () => { - describe('without a token', () => { - beforeEach(() => { - const state = rootState; - store = mockStore(state); - }); - - it('dispatches the correct actions', async() => { - const expectedActions = [{ type: 'ME_FETCH_SKIP' }]; - await store.dispatch(fetchMe()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with a token', () => { - const accountUrl = 'accountUrl'; - const token = '123'; - - beforeEach(() => { - const state = { - ...rootState, - auth: ReducerRecord({ - me: accountUrl, - users: ImmutableMap({ - [accountUrl]: AuthUserRecord({ - 'access_token': token, - }), - }), - }), - entities: { - 'ACCOUNTS': { - store: { - [accountUrl]: buildAccount({ url: accountUrl }), - }, - lists: {}, - }, - }, - }; - - store = mockStore(state); - }); - - describe('with a successful API response', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/verify_credentials').reply(200, {}); - }); - }); - - it('dispatches the correct actions', async() => { - const expectedActions = [ - { type: 'ME_FETCH_REQUEST' }, - { type: 'AUTH_ACCOUNT_REMEMBER_REQUEST', accountUrl }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'AUTH_ACCOUNT_REMEMBER_SUCCESS', - account: {}, - accountUrl, - }, - { type: 'VERIFY_CREDENTIALS_REQUEST', token: '123' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { type: 'VERIFY_CREDENTIALS_SUCCESS', token: '123', account: {} }, - ]; - await store.dispatch(fetchMe()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('patchMe()', () => { - beforeEach(() => { - const state = rootState; - store = mockStore(state); - }); - - describe('with a successful API response', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPatch('/api/v1/accounts/update_credentials').reply(200, {}); - }); - }); - - it('dispatches the correct actions', async() => { - const expectedActions = [ - { type: 'ME_PATCH_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'ME_PATCH_SUCCESS', - me: {}, - }, - ]; - await store.dispatch(patchMe({})); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); diff --git a/src/actions/me.ts b/src/actions/me.ts index 7be77c52f..2933da913 100644 --- a/src/actions/me.ts +++ b/src/actions/me.ts @@ -33,11 +33,11 @@ const getMeUrl = (state: RootState) => { } }; -const getMeToken = (state: RootState) => { +function getMeToken(state: RootState): string | undefined { // Fallback for upgrading IDs to URLs const accountUrl = getMeUrl(state) || state.auth.me; - return state.auth.users.get(accountUrl!)?.access_token; -}; + return state.auth.users[accountUrl!]?.access_token; +} const fetchMe = () => (dispatch: AppDispatch, getState: () => RootState) => { diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx index 67a838a28..f5ab9fcd3 100644 --- a/src/components/sidebar-menu.tsx +++ b/src/components/sidebar-menu.tsx @@ -15,8 +15,7 @@ import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbo import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications'; import { makeGetOtherAccounts } from 'soapbox/selectors'; -import type { List as ImmutableList } from 'immutable'; -import type { Account as AccountEntity } from 'soapbox/types/entities'; +import type { Account as AccountEntity } from 'soapbox/schemas/account'; const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, @@ -86,7 +85,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const features = useFeatures(); const me = useAppSelector((state) => state.me); const { account } = useAccount(me || undefined); - const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); + const otherAccounts = useAppSelector((state) => getOtherAccounts(state)); const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen); const settings = useAppSelector((state) => getSettings(state)); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); diff --git a/src/contexts/nostr-context.tsx b/src/contexts/nostr-context.tsx index 0e5b1a753..c9a70ed70 100644 --- a/src/contexts/nostr-context.tsx +++ b/src/contexts/nostr-context.tsx @@ -26,7 +26,7 @@ export const NostrProvider: React.FC = ({ children }) => { const [isRelayOpen, setIsRelayOpen] = useState(false); const url = instance.nostr?.relay; - const accountPubkey = useAppSelector(({ meta, auth }) => meta.pubkey ?? auth.users.get(auth.me!)?.id); + const accountPubkey = useAppSelector(({ meta, auth }) => meta.pubkey ?? auth.users[auth.me!]?.id); const signer = useMemo( () => accountPubkey ? NKeys.get(accountPubkey) ?? window.nostr : undefined, diff --git a/src/features/auth-token-list/index.tsx b/src/features/auth-token-list/index.tsx index 40ca864c5..caaf6a66f 100644 --- a/src/features/auth-token-list/index.tsx +++ b/src/features/auth-token-list/index.tsx @@ -73,9 +73,9 @@ const AuthTokenList: React.FC = () => { const dispatch = useAppDispatch(); const intl = useIntl(); const tokens = useAppSelector(state => state.security.get('tokens').reverse()); - const currentTokenId = useAppSelector(state => { - const currentToken = state.auth.tokens.valueSeq().find((token) => token.me === state.auth.me); + const currentTokenId = useAppSelector(state => { + const currentToken = Object.values(state.auth.tokens).find((token) => token.me === state.auth.me); return currentToken?.id; }); @@ -86,7 +86,7 @@ const AuthTokenList: React.FC = () => { const body = tokens ? (
{tokens.map((token) => ( - + ))}
) : ; diff --git a/src/features/ui/components/profile-dropdown.tsx b/src/features/ui/components/profile-dropdown.tsx index 4ef3e5983..80961b5ac 100644 --- a/src/features/ui/components/profile-dropdown.tsx +++ b/src/features/ui/components/profile-dropdown.tsx @@ -1,7 +1,7 @@ import { useFloating } from '@floating-ui/react'; import clsx from 'clsx'; import throttle from 'lodash/throttle'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; @@ -9,11 +9,11 @@ import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth'; import Account from 'soapbox/components/account'; import { MenuDivider } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; +import { makeGetOtherAccounts } from 'soapbox/selectors'; import ThemeToggle from './theme-toggle'; -import type { Account as AccountEntity } from 'soapbox/types/entities'; +import type { Account as AccountEntity } from 'soapbox/schemas'; const messages = defineMessages({ add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, @@ -34,8 +34,6 @@ type IMenuItem = { action?: (event: React.MouseEvent) => void; } -const getAccount = makeGetAccount(); - const ProfileDropdown: React.FC = ({ account, children }) => { const dispatch = useAppDispatch(); const features = useFeatures(); @@ -43,8 +41,9 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const [visible, setVisible] = useState(false); const { x, y, strategy, refs } = useFloating({ placement: 'bottom-end' }); - const authUsers = useAppSelector((state) => state.auth.users); - const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!)); + + const getOtherAccounts = useCallback(makeGetOtherAccounts(), []); + const otherAccounts = useAppSelector((state) => getOtherAccounts(state)); const handleLogOut = () => { dispatch(logOut()); @@ -71,7 +70,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { menu.push({ text: renderAccount(account), to: `/@${account.acct}` }); - otherAccounts.forEach((otherAccount: AccountEntity) => { + otherAccounts.forEach((otherAccount) => { if (otherAccount && otherAccount.id !== account.id) { menu.push({ text: renderAccount(otherAccount), @@ -98,13 +97,13 @@ const ProfileDropdown: React.FC = ({ account, children }) => { }); return menu; - }, [account, authUsers, features]); + }, [account, otherAccounts, features]); const toggleVisible = () => setVisible(!visible); useEffect(() => { fetchOwnAccountThrottled(); - }, [account, authUsers]); + }, [account, otherAccounts]); useClickOutside(refs, () => { setVisible(false); diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts index b8b7dbf28..5d0baaf1b 100644 --- a/src/hooks/useApi.ts +++ b/src/hooks/useApi.ts @@ -9,7 +9,7 @@ import { useOwnAccount } from './useOwnAccount'; export function useApi(): MastodonClient { const { account } = useOwnAccount(); const authUserUrl = useAppSelector((state) => state.auth.me); - const accessToken = useAppSelector((state) => account ? state.auth.users.get(account.url)?.access_token : undefined); + const accessToken = useAppSelector((state) => account ? state.auth.users[account.url]?.access_token : undefined); const baseUrl = new URL(BuildConfig.BACKEND_URL || account?.url || authUserUrl || location.origin).origin; return useMemo(() => { diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index acfdcefd6..000000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useMemo } from 'react'; - -import { SoapboxAuth, soapboxAuthSchema, AuthUser } from 'soapbox/schemas/soapbox/soapbox-auth'; -import { Token } from 'soapbox/schemas/token'; - -import { useAppSelector } from './useAppSelector'; - -export function useAuth() { - const raw = useAppSelector((state) => state.auth); - - const data = useMemo(() => { - try { - return soapboxAuthSchema.parse(raw.toJS()); - } catch { - return { tokens: {}, users: {} }; - } - }, [raw]); - - const users = useMemo(() => Object.values(data.users), []); - const tokens = useMemo(() => Object.values(data.tokens), []); - - const user = data.me ? data.users[data.me] : undefined; - - return { - users, - tokens, - accountId: user?.id, - accountUrl: user?.url, - accessToken: user?.access_token, - }; -} \ No newline at end of file diff --git a/src/reducers/auth.test.ts b/src/reducers/auth.test.ts deleted file mode 100644 index fbee6005f..000000000 --- a/src/reducers/auth.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { - AUTH_APP_CREATED, - AUTH_LOGGED_IN, - AUTH_LOGGED_OUT, - VERIFY_CREDENTIALS_SUCCESS, - VERIFY_CREDENTIALS_FAIL, - SWITCH_ACCOUNT, -} from 'soapbox/actions/auth'; -import { ME_FETCH_SKIP } from 'soapbox/actions/me'; -import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload'; -import { buildAccount } from 'soapbox/jest/factory'; - -import reducer, { AuthAppRecord, AuthTokenRecord, AuthUserRecord, ReducerRecord } from './auth'; - -describe('auth reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any).toJS()).toMatchObject({ - app: {}, - users: {}, - tokens: {}, - me: null, - }); - }); - - describe('AUTH_APP_CREATED', () => { - it('should copy in the app', () => { - const token = { token_type: 'Bearer', access_token: 'ABCDEFG' }; - const action = { type: AUTH_APP_CREATED, app: token }; - - const result = reducer(undefined, action); - const expected = AuthAppRecord(token); - - expect(result.app).toEqual(expected); - }); - }); - - describe('AUTH_LOGGED_IN', () => { - it('should import the token', () => { - const token = { token_type: 'Bearer', access_token: 'ABCDEFG' }; - const action = { type: AUTH_LOGGED_IN, token }; - - const result = reducer(undefined, action); - const expected = ImmutableMap({ 'ABCDEFG': AuthTokenRecord(token) }); - - expect(result.tokens).toEqual(expected); - }); - - it('should merge the token with existing state', () => { - const state = ReducerRecord({ - tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }), - }); - - const expected = ImmutableMap({ - 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }), - 'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }), - }); - - const action = { - type: AUTH_LOGGED_IN, - token: { token_type: 'Bearer', access_token: 'HIJKLMN' }, - }; - - const result = reducer(state, action); - expect(result.tokens).toEqual(expected); - }); - }); - - describe('AUTH_LOGGED_OUT', () => { - it('deletes the user', () => { - const action = { - type: AUTH_LOGGED_OUT, - account: buildAccount({ url: 'https://gleasonator.com/users/alex' }), - }; - - const state = ReducerRecord({ - users: ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }), - }); - - const expected = ImmutableMap({ - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }); - - const result = reducer(state, action); - expect(result.users).toEqual(expected); - }); - - it('sets `me` to the next available user', () => { - const state = ReducerRecord({ - me: 'https://gleasonator.com/users/alex', - users: ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }), - }); - - const action = { - type: AUTH_LOGGED_OUT, - account: buildAccount({ url: 'https://gleasonator.com/users/alex' }), - }; - - const result = reducer(state, action); - expect(result.me).toEqual('https://gleasonator.com/users/benis'); - }); - }); - - describe('VERIFY_CREDENTIALS_SUCCESS', () => { - it('should import the user', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const expected = ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - }); - - const result = reducer(undefined, action); - expect(result.users).toEqual(expected); - }); - - it('should set the account in the token', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const state = ReducerRecord({ - tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }), - }); - - const expected = { - 'ABCDEFG': { - token_type: 'Bearer', - access_token: 'ABCDEFG', - account: '1234', - me: 'https://gleasonator.com/users/alex', - }, - }; - - const result = reducer(state, action); - expect(result.tokens.toJS()).toMatchObject(expected); - }); - - it('sets `me` to the account if unset', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const result = reducer(undefined, action); - expect(result.me).toEqual('https://gleasonator.com/users/alex'); - }); - - it('leaves `me` alone if already set', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const state = ReducerRecord({ me: 'https://gleasonator.com/users/benis' }); - - const result = reducer(state, action); - expect(result.me).toEqual('https://gleasonator.com/users/benis'); - }); - - it('deletes mismatched users', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const state = ReducerRecord({ - users: ImmutableMap({ - 'https://gleasonator.com/users/mk': AuthUserRecord({ id: '4567', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/mk' }), - 'https://gleasonator.com/users/curtis': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/curtis' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }), - }); - - const expected = ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }); - - const result = reducer(state, action); - expect(result.users).toEqual(expected); - }); - - it('upgrades from an ID to a URL', () => { - const action = { - type: VERIFY_CREDENTIALS_SUCCESS, - token: 'ABCDEFG', - account: { id: '1234', url: 'https://gleasonator.com/users/alex' }, - }; - - const state = ReducerRecord({ - me: '1234', - users: ImmutableMap({ - '1234': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG' }), - '5432': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN' }), - }), - tokens: ImmutableMap({ - 'ABCDEFG': AuthTokenRecord({ access_token: 'ABCDEFG', account: '1234' }), - }), - }); - - const expected = { - me: 'https://gleasonator.com/users/alex', - users: { - 'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }, - '5432': { id: '5432', access_token: 'HIJKLMN' }, - }, - tokens: { - 'ABCDEFG': { access_token: 'ABCDEFG', account: '1234', me: 'https://gleasonator.com/users/alex' }, - }, - }; - - const result = reducer(state, action); - expect(result.toJS()).toMatchObject(expected); - }); - }); - - describe('VERIFY_CREDENTIALS_FAIL', () => { - it('should delete the failed token if it 403\'d', () => { - const state = ReducerRecord({ - tokens: ImmutableMap({ - 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }), - 'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }), - }), - }); - - const expected = ImmutableMap({ - 'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }), - }); - - const action = { - type: VERIFY_CREDENTIALS_FAIL, - token: 'ABCDEFG', - error: { response: { status: 403 } }, - }; - - const result = reducer(state, action); - expect(result.tokens).toEqual(expected); - }); - - it('should delete any users associated with the failed token', () => { - const state = ReducerRecord({ - users: ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }), - }); - - const expected = ImmutableMap({ - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }); - - const action = { - type: VERIFY_CREDENTIALS_FAIL, - token: 'ABCDEFG', - error: { response: { status: 403 } }, - }; - - const result = reducer(state, action); - expect(result.users).toEqual(expected); - }); - - it('should reassign `me` to the next in line', () => { - const state = ReducerRecord({ - me: 'https://gleasonator.com/users/alex', - users: ImmutableMap({ - 'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }), - 'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }), - }), - }); - - const action = { - type: VERIFY_CREDENTIALS_FAIL, - token: 'ABCDEFG', - error: { response: { status: 403 } }, - }; - - const result = reducer(state, action); - expect(result.me).toEqual('https://gleasonator.com/users/benis'); - }); - }); - - describe('SWITCH_ACCOUNT', () => { - it('sets the value of `me`', () => { - const action = { - type: SWITCH_ACCOUNT, - account: { url: 'https://gleasonator.com/users/benis' }, - }; - - const result = reducer(undefined, action); - expect(result.me).toEqual('https://gleasonator.com/users/benis'); - }); - }); - - describe('ME_FETCH_SKIP', () => { - it('sets `me` to null', () => { - const state = ReducerRecord({ me: 'https://gleasonator.com/users/alex' }); - const action = { type: ME_FETCH_SKIP }; - const result = reducer(state, action); - expect(result.me).toEqual(null); - }); - }); - - describe('MASTODON_PRELOAD_IMPORT', () => { - it('imports the user and token', async () => { - const data = await import('soapbox/__fixtures__/mastodon_initial_state.json'); - - const action = { - type: MASTODON_PRELOAD_IMPORT, - data, - }; - - const expected = { - me: 'https://mastodon.social/@benis911', - app: {}, - users: { - 'https://mastodon.social/@benis911': { - id: '106801667066418367', - access_token: 'Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q', - url: 'https://mastodon.social/@benis911', - }, - }, - tokens: { - 'Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q': { - access_token: 'Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q', - account: '106801667066418367', - me: 'https://mastodon.social/@benis911', - scope: 'read write follow push', - token_type: 'Bearer', - }, - }, - }; - - const result = reducer(undefined, action); - expect(result.toJS()).toMatchObject(expected); - }); - }); -}); diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 6bd738e0e..a5d3076cc 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -11,7 +11,6 @@ import { getSettings } from 'soapbox/actions/settings'; import { Entities } from 'soapbox/entity-store/entities'; import { type MRFSimple } from 'soapbox/schemas/pleroma'; import { getDomain } from 'soapbox/utils/accounts'; -import { validId } from 'soapbox/utils/auth'; import ConfigDB from 'soapbox/utils/config-db'; import { getFeatures } from 'soapbox/utils/features'; import { shouldFilter } from 'soapbox/utils/timelines'; @@ -261,34 +260,27 @@ export const makeGetReport = () => { ); }; -const getAuthUserIds = createSelector([ - (state: RootState) => state.auth.users, -], authUsers => { - return authUsers.reduce((ids: ImmutableOrderedSet, authUser) => { - try { - const id = authUser.id; - return validId(id) ? ids.add(id) : ids; - } catch { - return ids; - } - }, ImmutableOrderedSet()); -}); - -export const makeGetOtherAccounts = () => { +export function makeGetOtherAccounts() { return createSelector([ (state: RootState) => state.entities[Entities.ACCOUNTS]?.store as EntityStore, - getAuthUserIds, + (state: RootState) => state.auth.users, (state: RootState) => state.me, ], - (accounts, authUserIds, me) => { - return authUserIds - .reduce((list: ImmutableList, id: string) => { - if (id === me) return list; - const account = accounts[id]; - return account ? list.push(account) : list; - }, ImmutableList()); + (store, authUsers, me): AccountSchema[] => { + const accountIds = Object.values(authUsers).map((authUser) => authUser.id); + + return accountIds.reduce((accounts, id: string) => { + if (id === me) return accounts; + + const account = store[id]; + if (account) { + accounts.push(account); + } + + return accounts; + }, []); }); -}; +} const getSimplePolicy = createSelector([ (state: RootState) => state.admin.configs, diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 73757fb80..34799cae5 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -30,13 +30,13 @@ export const isLoggedIn = (getState: () => RootState) => { return validId(getState().me); }; -export const getAppToken = (state: RootState) => state.auth.app.access_token as string; +export const getAppToken = (state: RootState) => state.auth.app?.access_token; export const getUserToken = (state: RootState, accountId?: string | false | null) => { if (!accountId) return; const accountUrl = selectAccount(state, accountId)?.url; if (!accountUrl) return; - return state.auth.users.get(accountUrl)?.access_token; + return state.auth.users[accountUrl]?.access_token; }; export const getAccessToken = (state: RootState) => { @@ -48,7 +48,7 @@ export const getAuthUserId = (state: RootState) => { const me = state.auth.me; return ImmutableList([ - state.auth.users.get(me!)?.id, + state.auth.users[me!]?.id, me, ].filter(id => id)).find(validId); }; @@ -57,7 +57,7 @@ export const getAuthUserUrl = (state: RootState) => { const me = state.auth.me; return ImmutableList([ - state.auth.users.get(me!)?.url, + state.auth.users[me!]?.url, me, ].filter(url => url)).find(isURL); };