Skip to content

Commit 102b6ec

Browse files
committed
add UI and basic UI tests
1 parent 521c2fe commit 102b6ec

File tree

8 files changed

+768
-7
lines changed

8 files changed

+768
-7
lines changed

src/api/routes/apiKey.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { z } from "zod";
2525

2626
const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
2727
await fastify.register(rateLimiter, {
28-
limit: 5,
28+
limit: 15,
2929
duration: 30,
3030
rateLimitIdentifier: "apiKey",
3131
});
@@ -178,10 +178,12 @@ const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
178178
const unmarshalled = result.Items.map((x) =>
179179
unmarshall(x),
180180
) as ApiKeyDynamoEntry[];
181-
const filtered = unmarshalled.map((x) => ({
182-
...x,
183-
keyHash: undefined,
184-
}));
181+
const filtered = unmarshalled
182+
.map((x) => ({
183+
...x,
184+
keyHash: undefined,
185+
}))
186+
.filter((x) => !x.expiresAt || x.expiresAt < Date.now());
185187
return reply.status(200).send(filtered);
186188
} catch (e) {
187189
if (e instanceof BaseError) {

src/common/types/apiKey.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { AppRoles } from "../roles.js";
22
import { z } from "zod"
33

4-
export type ApiKeyDynamoEntry = {
4+
export type ApiKeyMaskedEntry = {
55
keyId: string;
6-
keyHash: string;
76
roles: AppRoles[];
87
owner: string;
98
description: string;
109
createdAt: number;
1110
expiresAt?: number;
11+
}
12+
13+
export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & {
14+
keyHash: string;
1215
};
1316

1417
export type DecomposedApiKey = {
@@ -31,3 +34,5 @@ export const apiKeyPostBody = z.object({
3134
message: "expiresAt must be a future epoch time.",
3235
}).openapi({ description: "Epoch timestamp of when the key expires.", example: 1745362658 })
3336
})
37+
38+
export type ApiKeyPostBody = z.infer<typeof apiKeyPostBody>;

src/ui/Router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ManageRoomRequestsPage } from './pages/roomRequest/RoomRequestLanding.p
2424
import { ViewRoomRequest } from './pages/roomRequest/ViewRoomRequest.page';
2525
import { ViewLogsPage } from './pages/logs/ViewLogs.page';
2626
import { TermsOfService } from './pages/tos/TermsOfService.page';
27+
import { ManageApiKeysPage } from './pages/apiKeys/ManageKeys.page';
2728

2829
const ProfileRediect: React.FC = () => {
2930
const location = useLocation();
@@ -195,6 +196,10 @@ const authenticatedRouter = createBrowserRouter([
195196
path: '/logs',
196197
element: <ViewLogsPage />,
197198
},
199+
{
200+
path: '/apiKeys',
201+
element: <ManageApiKeysPage />,
202+
},
198203
// Catch-all route for authenticated users shows 404 page
199204
{
200205
path: '*',

src/ui/components/AppShell/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
IconLock,
2121
IconDoor,
2222
IconHistory,
23+
IconKey,
2324
} from '@tabler/icons-react';
2425
import { ReactNode } from 'react';
2526
import { useNavigate } from 'react-router-dom';
@@ -89,6 +90,13 @@ export const navItems = [
8990
description: null,
9091
validRoles: [AppRoles.AUDIT_LOG_VIEWER],
9192
},
93+
{
94+
link: '/apiKeys',
95+
name: 'API Keys',
96+
icon: IconKey,
97+
description: null,
98+
validRoles: [AppRoles.MANAGE_ORG_API_KEYS],
99+
},
92100
];
93101

94102
export const extLinks = [
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useState } from 'react';
2+
import { Text, Overlay, Box, ActionIcon } from '@mantine/core';
3+
import { IconEye, IconEyeOff } from '@tabler/icons-react';
4+
5+
export const BlurredTextDisplay: React.FC<{ text: string; initialState?: boolean }> = ({
6+
text,
7+
initialState = false,
8+
}) => {
9+
const [visible, setVisible] = useState(initialState);
10+
11+
return (
12+
<Box pos="relative" maw={400} mx="auto">
13+
<Text
14+
ta="center"
15+
fw={600}
16+
fz="sm"
17+
p="md"
18+
bg="var(--mantine-color-gray-light)"
19+
style={{
20+
wordBreak: 'break-all',
21+
borderRadius: 4,
22+
}}
23+
>
24+
{text}
25+
</Text>
26+
27+
{!visible && (
28+
<Overlay
29+
blur={7}
30+
radius={3}
31+
opacity={1}
32+
color="var(--mantine-color-gray-light)"
33+
zIndex={5}
34+
center
35+
style={{
36+
position: 'absolute', // Made position explicit
37+
top: 0,
38+
left: 0,
39+
right: 0,
40+
bottom: 0,
41+
}}
42+
/>
43+
)}
44+
45+
<ActionIcon
46+
variant="light"
47+
size="sm"
48+
onClick={() => setVisible((v) => !v)}
49+
pos="absolute"
50+
top={5}
51+
right={5}
52+
style={{ zIndex: 10 }}
53+
>
54+
{visible ? <IconEyeOff size={16} /> : <IconEye size={16} />}
55+
</ActionIcon>
56+
</Box>
57+
);
58+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useState } from 'react';
2+
import { Card, Container, Divider, Title, Text } from '@mantine/core';
3+
import { AuthGuard } from '@ui/components/AuthGuard';
4+
import { AppRoles } from '@common/roles';
5+
import { useApi } from '@ui/util/api';
6+
import { OrgApiKeyTable } from './ManageKeysTable';
7+
8+
export const ManageApiKeysPage: React.FC = () => {
9+
const api = useApi('core');
10+
11+
return (
12+
<AuthGuard
13+
resourceDef={{ service: 'core', validRoles: [AppRoles.MANAGE_ORG_API_KEYS] }}
14+
showSidebar={true}
15+
>
16+
<Container>
17+
<Title>API Keys</Title>
18+
<Text>Manage organization API keys.</Text>
19+
<Text size="xs" c="dimmed">
20+
These keys' permissions are not tied to any one user, and can be managed by organization
21+
admins.
22+
</Text>
23+
<Divider m="md" />
24+
<OrgApiKeyTable
25+
getApiKeys={() => api.get('/api/v1/apiKey/org').then((res) => res.data)}
26+
deleteApiKeys={(ids) =>
27+
Promise.all(ids.map((id) => api.delete(`/api/v1/apiKey/org/${id}`))).then(() => {})
28+
}
29+
createApiKey={(data) => api.post('/api/v1/apiKey/org', data).then((res) => res.data)}
30+
/>
31+
</Container>
32+
</AuthGuard>
33+
);
34+
};

0 commit comments

Comments
 (0)