@ -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,
|
||||
|
||||
@ -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) => (
|
||||
|
||||
24
packages/pl-fe/src/queries/security/oauth-tokens.ts
Normal file
24
packages/pl-fe/src/queries/security/oauth-tokens.ts
Normal 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 };
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user