Skip to content

Commit

Permalink
Implement log in sessions page with ability to view and revoke active…
Browse files Browse the repository at this point in the history
… tokens

• Add getTokens endpoint to auth service
• Show current and other active sessions
• Add token revocation functionality
• Display session details in table format
• Move SSO provider styles to shared module
  • Loading branch information
dauglyon committed Dec 12, 2024
1 parent b511489 commit 156f38a
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 52 deletions.
50 changes: 48 additions & 2 deletions src/common/api/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ interface AuthParams {
token: string;
id: string;
};
getTokens: string;
revokeToken: string;
}

interface AuthResults {
Expand Down Expand Up @@ -123,11 +125,35 @@ interface AuthResults {
linked: { provusername: string; id: string; user: string }[]; // if already linked
};
postLinkPick: void;
getTokens: {
current: AuthResults['getTokens']['tokens'][number];
dev: boolean;
revokeurl: string;
service: boolean;
createurl: string;
tokens: {
type: string;
id: string;
expires: number;
created: number;
user: string;
custom: unknown;
os: string;
osver: string;
agent: string;
agentver: string;
device: string;
ip: string;
}[];
user: string;
revokeallurl: string;
};
revokeToken: boolean;
}

// Auth does not use JSONRpc, so we use queryFn to make custom queries
export const authApi = baseApi
.enhanceEndpoints({ addTagTypes: ['AccountMe'] })
.enhanceEndpoints({ addTagTypes: ['AccountMe', 'AccountTokens'] })
.injectEndpoints({
endpoints: (builder) => ({
authFromToken: builder.query<TokenResponse, string>({
Expand Down Expand Up @@ -198,12 +224,31 @@ export const authApi = baseApi
url: `/api/V2/users/search/${search}`,
}),
}),
revokeToken: builder.mutation<boolean, string>({
getTokens: builder.query<
AuthResults['getTokens'],
AuthParams['getTokens']
>({
query: (token) =>
authService({
headers: {
accept: 'application/json',
Authorization: token,
},
url: encode`/tokens/`,
method: 'GET',
}),
providesTags: ['AccountTokens'],
}),
revokeToken: builder.mutation<
AuthResults['revokeToken'],
AuthParams['revokeToken']
>({
query: (tokenId) =>
authService({
url: encode`/tokens/revoke/${tokenId}`,
method: 'DELETE',
}),
invalidatesTags: ['AccountTokens'],
}),
getLoginChoice: builder.query<
AuthResults['getLoginChoice'],
Expand Down Expand Up @@ -312,6 +357,7 @@ export const {
setMe,
getUsers,
searchUsers,
getTokens,
revokeToken,
getLoginChoice,
postLoginPick,
Expand Down
122 changes: 77 additions & 45 deletions src/features/account/LogInSessions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { faCheck, faInfoCircle, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
Button,
Expand All @@ -12,28 +12,20 @@ import {
Typography,
} from '@mui/material';
import { FC } from 'react';

/**
* Dummy data for the log in sessions table.
* Can be deleted once table is linked to backend.
*/
const sampleSessions = [
{
created: 'Jul 9, 2024 at 9:05am ',
expires: '10d 18h 42m ',
browser: 'Chrome 125.0.0.0 ',
operatingSystem: 'Mac OS X 10.15.7 ',
ipAddress: '192.184.174.53',
},
];
import { getTokens, revokeToken } from '../../common/api/authService';
import { Loader } from '../../common/components';
import { useAppSelector } from '../../common/hooks';
import { useLogout } from '../login/LogIn';

/**
* Content for the Log In Sessions tab in the Account page
*/
export const LogInSessions: FC = () => {
const currentSessions = sampleSessions;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const otherSessions: any[] = [];
const token = useAppSelector(({ auth }) => auth.token ?? '');
const tokenSessions = getTokens.useQuery(token, { skip: !token });

const currentToken = tokenSessions.data?.current;
const otherTokens = tokenSessions.data?.tokens;

return (
<Stack
Expand All @@ -43,7 +35,7 @@ export const LogInSessions: FC = () => {
aria-labelledby="sessions-tab"
>
<Stack direction="row" justifyContent="space-between">
<Typography variant="h2">My Current Log In Sessions</Typography>
<Typography variant="h2">Current Log In Session</Typography>
<Tooltip
title={
<Stack spacing={1}>
Expand Down Expand Up @@ -74,24 +66,28 @@ export const LogInSessions: FC = () => {
</TableRow>
</TableHead>
<TableBody>
{currentSessions.map((session, i) => (
<TableRow key={`${session}-${i}`}>
<TableCell>{session.created}</TableCell>
<TableCell>{session.expires}</TableCell>
<TableCell>{session.browser}</TableCell>
<TableCell>{session.operatingSystem}</TableCell>
<TableCell>{session.ipAddress}</TableCell>
<TableCell>
<Button variant="contained" color="error">
Log out
</Button>
</TableCell>
</TableRow>
))}
<TableRow>
<TableCell>
{new Date(currentToken?.created ?? 0).toLocaleString()}
</TableCell>
<TableCell>
{new Date(currentToken?.expires ?? 0).toLocaleString()}
</TableCell>
<TableCell>
{currentToken?.agent} {currentToken?.agentver}
</TableCell>
<TableCell>
{currentToken?.os} {currentToken?.osver}
</TableCell>
<TableCell>{currentToken?.ip}</TableCell>
<TableCell>
<LogOutButton tokenId={currentToken?.id} />
</TableCell>
</TableRow>
</TableBody>
</Table>
<Typography variant="h2">Other Log In Sessions</Typography>
{otherSessions && otherSessions.length > 0 && (
{otherTokens && otherTokens.length > 0 && (
<Table>
<TableHead>
<TableRow>
Expand All @@ -104,26 +100,62 @@ export const LogInSessions: FC = () => {
</TableRow>
</TableHead>
<TableBody>
{otherSessions.map((session, i) => (
<TableRow key={`${session}-${i}`}>
<TableCell>{session.created}</TableCell>
<TableCell>{session.expires}</TableCell>
<TableCell>{session.browser}</TableCell>
<TableCell>{session.operatingSystem}</TableCell>
<TableCell>{session.ipAddress}</TableCell>
{otherTokens.map((otherToken, i) => (
<TableRow key={`${otherToken.id}-${i}`}>
<TableCell>
{new Date(otherToken.created ?? 0).toLocaleString()}
</TableCell>
<TableCell>
{new Date(otherToken.expires ?? 0).toLocaleString()}
</TableCell>
<TableCell>
{otherToken.agent} {otherToken.agentver}
</TableCell>
<TableCell>
{otherToken.os} {otherToken.osver}
</TableCell>
<TableCell>{otherToken.ip}</TableCell>
<TableCell>
<Button variant="contained" color="error">
Log out
</Button>
<LogOutButton tokenId={otherToken.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{(!otherSessions || otherSessions.length === 0) && (
{(!otherTokens || otherTokens.length === 0) && (
<i>No additional active log in sessions.</i>
)}
</Stack>
);
};

const LogOutButton = ({ tokenId }: { tokenId?: string }) => {
const logout = useLogout();
const currentTokenId = useAppSelector(({ auth }) => auth.tokenInfo?.id);
const [tirggerRevoke, revoke] = revokeToken.useMutation();
return (
<Button
variant="contained"
color="error"
onClick={() => {
if (currentTokenId === tokenId) {
logout();
} else if (tokenId) {
tirggerRevoke(tokenId);
}
}}
endIcon={
revoke.isLoading ? (
<Loader loading={true} type="spinner" />
) : revoke.isSuccess ? (
<FontAwesomeIcon icon={faCheck} />
) : revoke.isError ? (
<FontAwesomeIcon icon={faX} />
) : undefined
}
>
Log out
</Button>
);
};
10 changes: 5 additions & 5 deletions src/features/login/LoggedOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useCheckLoggedIn } from './LogIn';
import orcidLogo from '../../common/assets/orcid.png';
import globusLogo from '../../common/assets/globus.png';
import googleLogo from '../../common/assets/google.webp';
import classes from './LogIn.module.scss';
import providerClasses from '../auth/providers.module.scss';

export const LoggedOut = () => {
useCheckLoggedIn(undefined);
Expand All @@ -34,7 +34,7 @@ export const LoggedOut = () => {
browser, you should sign out of any provider accounts you have
used to access KBase.
</Typography>
<Box className={classes['separator']} />
<Box className={providerClasses['separator']} />
<Stack spacing={1}>
<Button
role="link"
Expand All @@ -46,7 +46,7 @@ export const LoggedOut = () => {
<img
src={orcidLogo}
alt="ORCID logo"
className={classes['sso-logo']}
className={providerClasses['sso-logo']}
/>
}
>
Expand All @@ -62,7 +62,7 @@ export const LoggedOut = () => {
<img
src={googleLogo}
alt="Google logo"
className={classes['sso-logo']}
className={providerClasses['sso-logo']}
/>
}
>
Expand All @@ -78,7 +78,7 @@ export const LoggedOut = () => {
<img
src={globusLogo}
alt="Globus logo"
className={classes['sso-logo']}
className={providerClasses['sso-logo']}
/>
}
>
Expand Down

0 comments on commit 156f38a

Please sign in to comment.