pl-fe: migrate tokens list

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk
2025-03-15 12:30:15 +01:00
parent 43167628bd
commit 9188fcad95
4 changed files with 36 additions and 53 deletions

View File

@ -11,26 +11,9 @@ import { normalizeUsername } from 'pl-fe/utils/input';
import { AUTH_LOGGED_OUT, messages } from './auth';
import type { OauthToken } from 'pl-api';
import type { Account } from 'pl-fe/normalizers/account';
import type { AppDispatch, RootState } from 'pl-fe/store';
const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS' as const;
const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS' as const;
const fetchOAuthTokens = () =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.getOauthTokens().then(({ items: tokens }) => {
dispatch<SecurityAction>({ type: FETCH_TOKENS_SUCCESS, tokens });
});
const revokeOAuthTokenById = (tokenId: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.deleteOauthToken(tokenId).then(() => {
dispatch<SecurityAction>({ type: REVOKE_TOKEN_SUCCESS, tokenId });
});
const changePassword = (oldPassword: string, newPassword: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.changePassword(oldPassword, newPassword);
@ -63,16 +46,9 @@ const moveAccount = (targetAccount: string, password: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.moveAccount(targetAccount, password);
type SecurityAction =
| { type: typeof FETCH_TOKENS_SUCCESS; tokens: Array<OauthToken> }
| { type: typeof REVOKE_TOKEN_SUCCESS; tokenId: string }
| { type: typeof AUTH_LOGGED_OUT; account: Account }
type SecurityAction = { type: typeof AUTH_LOGGED_OUT; account: Account }
export {
FETCH_TOKENS_SUCCESS,
REVOKE_TOKEN_SUCCESS,
fetchOAuthTokens,
revokeOAuthTokenById,
changePassword,
resetPassword,
changeEmail,

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { useInfiniteQuery, useMutation } from '@tanstack/react-query';
import React from 'react';
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'pl-fe/actions/security';
import Badge from 'pl-fe/components/badge';
import Button from 'pl-fe/components/ui/button';
import Card, { CardBody, CardHeader, CardTitle } from 'pl-fe/components/ui/card';
@ -11,8 +11,8 @@ import Icon from 'pl-fe/components/ui/icon';
import Spinner from 'pl-fe/components/ui/spinner';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { oauthTokensQueryOptions, revokeOauthTokenMutationOptions } from 'pl-fe/queries/security/oauth-tokens';
import { useModalsStore } from 'pl-fe/stores/modals';
import type { OauthToken } from 'pl-api';
@ -31,9 +31,10 @@ interface IAuthToken {
}
const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const revokeMutation = useMutation(revokeOauthTokenMutationOptions(token.id));
const { openModal } = useModalsStore();
const handleRevoke = () => {
@ -43,11 +44,11 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
message: intl.formatMessage(messages.revokeSessionMessage),
confirm: intl.formatMessage(messages.revokeSessionConfirm),
onConfirm: () => {
dispatch(revokeOAuthTokenById(token.id));
revokeMutation.mutate();
},
});
else {
dispatch(revokeOAuthTokenById(token.id));
revokeMutation.mutate();
}
};
@ -144,19 +145,16 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
};
const AuthTokenList: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const tokens = useAppSelector(state => state.security.tokens.toReversed());
const { data: tokens } = useInfiniteQuery(oauthTokensQueryOptions);
const currentTokenId = useAppSelector(state => {
const currentToken = Object.values(state.auth.tokens).find((token) => token.me === state.auth.me);
return currentToken?.id;
});
useEffect(() => {
dispatch(fetchOAuthTokens());
}, []);
const body = tokens ? (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
{tokens.map((token) => (

View File

@ -0,0 +1,24 @@
import { create } from 'mutative';
import { getClient } from 'pl-fe/api';
import { queryClient } from '../client';
import { makePaginatedResponseQueryOptions } from '../utils/make-paginated-response-query-options';
import { mutationOptions } from '../utils/mutation-options';
const oauthTokensQueryOptions = makePaginatedResponseQueryOptions(
['security', 'oauthTokens'],
(client) => client.settings.getOauthTokens(),
)();
const revokeOauthTokenMutationOptions = (oauthTokenId: string) => mutationOptions({
mutationKey: ['security', 'oauthTokens', oauthTokenId],
mutationFn: () => getClient().settings.deleteOauthToken(oauthTokenId),
onSettled: () => {
queryClient.setQueryData(oauthTokensQueryOptions.queryKey, (data) => create(data, (draft) => {
draft?.pages.forEach(page => page.items = page.items.filter(({ id }) => id !== oauthTokenId));
}));
},
});
export { oauthTokensQueryOptions, revokeOauthTokenMutationOptions };

View File

@ -6,19 +6,14 @@ import {
MFA_DISABLE_SUCCESS,
type MfaAction,
} from '../actions/mfa';
import { FETCH_TOKENS_SUCCESS, REVOKE_TOKEN_SUCCESS, type SecurityAction } from '../actions/security';
import type { OauthToken } from 'pl-api';
interface State {
tokens: Array<OauthToken>;
mfa: {
settings: Record<string, boolean>;
};
}
const initialState: State = {
tokens: [],
mfa: {
settings: {
totp: false,
@ -26,24 +21,14 @@ const initialState: State = {
},
};
const deleteToken = (state: State, tokenId: string) => state.tokens = state.tokens.filter(token => token.id !== tokenId);
const importMfa = (state: State, data: any) => state.mfa = data;
const enableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: true };
const disableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: false };
const security = (state = initialState, action: MfaAction | SecurityAction) => {
const security = (state = initialState, action: MfaAction) => {
switch (action.type) {
case FETCH_TOKENS_SUCCESS:
return create(state, (draft) => {
draft.tokens = action.tokens;
});
case REVOKE_TOKEN_SUCCESS:
return create(state, (draft) => {
deleteToken(draft, action.tokenId);
});
case MFA_FETCH_SUCCESS:
return create(state, (draft) => {
importMfa(draft, action.data);