Skip to content

Commit 156f38a

Browse files
committed
Implement log in sessions page with ability to view and revoke active 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
1 parent b511489 commit 156f38a

File tree

3 files changed

+130
-52
lines changed

3 files changed

+130
-52
lines changed

src/common/api/authService.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ interface AuthParams {
5454
token: string;
5555
id: string;
5656
};
57+
getTokens: string;
58+
revokeToken: string;
5759
}
5860

5961
interface AuthResults {
@@ -123,11 +125,35 @@ interface AuthResults {
123125
linked: { provusername: string; id: string; user: string }[]; // if already linked
124126
};
125127
postLinkPick: void;
128+
getTokens: {
129+
current: AuthResults['getTokens']['tokens'][number];
130+
dev: boolean;
131+
revokeurl: string;
132+
service: boolean;
133+
createurl: string;
134+
tokens: {
135+
type: string;
136+
id: string;
137+
expires: number;
138+
created: number;
139+
user: string;
140+
custom: unknown;
141+
os: string;
142+
osver: string;
143+
agent: string;
144+
agentver: string;
145+
device: string;
146+
ip: string;
147+
}[];
148+
user: string;
149+
revokeallurl: string;
150+
};
151+
revokeToken: boolean;
126152
}
127153

128154
// Auth does not use JSONRpc, so we use queryFn to make custom queries
129155
export const authApi = baseApi
130-
.enhanceEndpoints({ addTagTypes: ['AccountMe'] })
156+
.enhanceEndpoints({ addTagTypes: ['AccountMe', 'AccountTokens'] })
131157
.injectEndpoints({
132158
endpoints: (builder) => ({
133159
authFromToken: builder.query<TokenResponse, string>({
@@ -198,12 +224,31 @@ export const authApi = baseApi
198224
url: `/api/V2/users/search/${search}`,
199225
}),
200226
}),
201-
revokeToken: builder.mutation<boolean, string>({
227+
getTokens: builder.query<
228+
AuthResults['getTokens'],
229+
AuthParams['getTokens']
230+
>({
231+
query: (token) =>
232+
authService({
233+
headers: {
234+
accept: 'application/json',
235+
Authorization: token,
236+
},
237+
url: encode`/tokens/`,
238+
method: 'GET',
239+
}),
240+
providesTags: ['AccountTokens'],
241+
}),
242+
revokeToken: builder.mutation<
243+
AuthResults['revokeToken'],
244+
AuthParams['revokeToken']
245+
>({
202246
query: (tokenId) =>
203247
authService({
204248
url: encode`/tokens/revoke/${tokenId}`,
205249
method: 'DELETE',
206250
}),
251+
invalidatesTags: ['AccountTokens'],
207252
}),
208253
getLoginChoice: builder.query<
209254
AuthResults['getLoginChoice'],
@@ -312,6 +357,7 @@ export const {
312357
setMe,
313358
getUsers,
314359
searchUsers,
360+
getTokens,
315361
revokeToken,
316362
getLoginChoice,
317363
postLoginPick,
Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
1+
import { faCheck, faInfoCircle, faX } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import {
44
Button,
@@ -12,28 +12,20 @@ import {
1212
Typography,
1313
} from '@mui/material';
1414
import { FC } from 'react';
15-
16-
/**
17-
* Dummy data for the log in sessions table.
18-
* Can be deleted once table is linked to backend.
19-
*/
20-
const sampleSessions = [
21-
{
22-
created: 'Jul 9, 2024 at 9:05am ',
23-
expires: '10d 18h 42m ',
24-
browser: 'Chrome 125.0.0.0 ',
25-
operatingSystem: 'Mac OS X 10.15.7 ',
26-
ipAddress: '192.184.174.53',
27-
},
28-
];
15+
import { getTokens, revokeToken } from '../../common/api/authService';
16+
import { Loader } from '../../common/components';
17+
import { useAppSelector } from '../../common/hooks';
18+
import { useLogout } from '../login/LogIn';
2919

3020
/**
3121
* Content for the Log In Sessions tab in the Account page
3222
*/
3323
export const LogInSessions: FC = () => {
34-
const currentSessions = sampleSessions;
35-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36-
const otherSessions: any[] = [];
24+
const token = useAppSelector(({ auth }) => auth.token ?? '');
25+
const tokenSessions = getTokens.useQuery(token, { skip: !token });
26+
27+
const currentToken = tokenSessions.data?.current;
28+
const otherTokens = tokenSessions.data?.tokens;
3729

3830
return (
3931
<Stack
@@ -43,7 +35,7 @@ export const LogInSessions: FC = () => {
4335
aria-labelledby="sessions-tab"
4436
>
4537
<Stack direction="row" justifyContent="space-between">
46-
<Typography variant="h2">My Current Log In Sessions</Typography>
38+
<Typography variant="h2">Current Log In Session</Typography>
4739
<Tooltip
4840
title={
4941
<Stack spacing={1}>
@@ -74,24 +66,28 @@ export const LogInSessions: FC = () => {
7466
</TableRow>
7567
</TableHead>
7668
<TableBody>
77-
{currentSessions.map((session, i) => (
78-
<TableRow key={`${session}-${i}`}>
79-
<TableCell>{session.created}</TableCell>
80-
<TableCell>{session.expires}</TableCell>
81-
<TableCell>{session.browser}</TableCell>
82-
<TableCell>{session.operatingSystem}</TableCell>
83-
<TableCell>{session.ipAddress}</TableCell>
84-
<TableCell>
85-
<Button variant="contained" color="error">
86-
Log out
87-
</Button>
88-
</TableCell>
89-
</TableRow>
90-
))}
69+
<TableRow>
70+
<TableCell>
71+
{new Date(currentToken?.created ?? 0).toLocaleString()}
72+
</TableCell>
73+
<TableCell>
74+
{new Date(currentToken?.expires ?? 0).toLocaleString()}
75+
</TableCell>
76+
<TableCell>
77+
{currentToken?.agent} {currentToken?.agentver}
78+
</TableCell>
79+
<TableCell>
80+
{currentToken?.os} {currentToken?.osver}
81+
</TableCell>
82+
<TableCell>{currentToken?.ip}</TableCell>
83+
<TableCell>
84+
<LogOutButton tokenId={currentToken?.id} />
85+
</TableCell>
86+
</TableRow>
9187
</TableBody>
9288
</Table>
9389
<Typography variant="h2">Other Log In Sessions</Typography>
94-
{otherSessions && otherSessions.length > 0 && (
90+
{otherTokens && otherTokens.length > 0 && (
9591
<Table>
9692
<TableHead>
9793
<TableRow>
@@ -104,26 +100,62 @@ export const LogInSessions: FC = () => {
104100
</TableRow>
105101
</TableHead>
106102
<TableBody>
107-
{otherSessions.map((session, i) => (
108-
<TableRow key={`${session}-${i}`}>
109-
<TableCell>{session.created}</TableCell>
110-
<TableCell>{session.expires}</TableCell>
111-
<TableCell>{session.browser}</TableCell>
112-
<TableCell>{session.operatingSystem}</TableCell>
113-
<TableCell>{session.ipAddress}</TableCell>
103+
{otherTokens.map((otherToken, i) => (
104+
<TableRow key={`${otherToken.id}-${i}`}>
105+
<TableCell>
106+
{new Date(otherToken.created ?? 0).toLocaleString()}
107+
</TableCell>
108+
<TableCell>
109+
{new Date(otherToken.expires ?? 0).toLocaleString()}
110+
</TableCell>
111+
<TableCell>
112+
{otherToken.agent} {otherToken.agentver}
113+
</TableCell>
114+
<TableCell>
115+
{otherToken.os} {otherToken.osver}
116+
</TableCell>
117+
<TableCell>{otherToken.ip}</TableCell>
114118
<TableCell>
115-
<Button variant="contained" color="error">
116-
Log out
117-
</Button>
119+
<LogOutButton tokenId={otherToken.id} />
118120
</TableCell>
119121
</TableRow>
120122
))}
121123
</TableBody>
122124
</Table>
123125
)}
124-
{(!otherSessions || otherSessions.length === 0) && (
126+
{(!otherTokens || otherTokens.length === 0) && (
125127
<i>No additional active log in sessions.</i>
126128
)}
127129
</Stack>
128130
);
129131
};
132+
133+
const LogOutButton = ({ tokenId }: { tokenId?: string }) => {
134+
const logout = useLogout();
135+
const currentTokenId = useAppSelector(({ auth }) => auth.tokenInfo?.id);
136+
const [tirggerRevoke, revoke] = revokeToken.useMutation();
137+
return (
138+
<Button
139+
variant="contained"
140+
color="error"
141+
onClick={() => {
142+
if (currentTokenId === tokenId) {
143+
logout();
144+
} else if (tokenId) {
145+
tirggerRevoke(tokenId);
146+
}
147+
}}
148+
endIcon={
149+
revoke.isLoading ? (
150+
<Loader loading={true} type="spinner" />
151+
) : revoke.isSuccess ? (
152+
<FontAwesomeIcon icon={faCheck} />
153+
) : revoke.isError ? (
154+
<FontAwesomeIcon icon={faX} />
155+
) : undefined
156+
}
157+
>
158+
Log out
159+
</Button>
160+
);
161+
};

src/features/login/LoggedOut.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useCheckLoggedIn } from './LogIn';
1010
import orcidLogo from '../../common/assets/orcid.png';
1111
import globusLogo from '../../common/assets/globus.png';
1212
import googleLogo from '../../common/assets/google.webp';
13-
import classes from './LogIn.module.scss';
13+
import providerClasses from '../auth/providers.module.scss';
1414

1515
export const LoggedOut = () => {
1616
useCheckLoggedIn(undefined);
@@ -34,7 +34,7 @@ export const LoggedOut = () => {
3434
browser, you should sign out of any provider accounts you have
3535
used to access KBase.
3636
</Typography>
37-
<Box className={classes['separator']} />
37+
<Box className={providerClasses['separator']} />
3838
<Stack spacing={1}>
3939
<Button
4040
role="link"
@@ -46,7 +46,7 @@ export const LoggedOut = () => {
4646
<img
4747
src={orcidLogo}
4848
alt="ORCID logo"
49-
className={classes['sso-logo']}
49+
className={providerClasses['sso-logo']}
5050
/>
5151
}
5252
>
@@ -62,7 +62,7 @@ export const LoggedOut = () => {
6262
<img
6363
src={googleLogo}
6464
alt="Google logo"
65-
className={classes['sso-logo']}
65+
className={providerClasses['sso-logo']}
6666
/>
6767
}
6868
>
@@ -78,7 +78,7 @@ export const LoggedOut = () => {
7878
<img
7979
src={globusLogo}
8080
alt="Globus logo"
81-
className={classes['sso-logo']}
81+
className={providerClasses['sso-logo']}
8282
/>
8383
}
8484
>

0 commit comments

Comments
 (0)