Skip to content

Commit 04797ca

Browse files
authored
Add support for fine-grained access tokens (#110)
* ui for adding tokens * remove console.log
1 parent 1827853 commit 04797ca

File tree

6 files changed

+257
-106
lines changed

6 files changed

+257
-106
lines changed

src/api/users.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { APIToken } from '@/models/token'
2-
import { Request } from '../utils/api'
2+
import { ApiTokenScope, Request } from '../utils/api'
33
import { User } from '@/models/user'
44
import { NotificationTriggerOptions, NotificationType } from '@/models/notifications'
55

@@ -29,9 +29,10 @@ export const UpdateNotificationSettings = async (provider: NotificationType, tri
2929
triggers,
3030
})
3131

32-
export const CreateLongLiveToken = async (name: string) =>
32+
export const CreateLongLiveToken = async (name: string, scopes: ApiTokenScope[]) =>
3333
await Request<SingleTokenResponse>(`/users/tokens`, 'POST', {
3434
name,
35+
scopes,
3536
})
3637

3738
export const DeleteLongLiveToken = async (id: string) =>

src/utils/api.ts

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ type FailureResponse = {
88
error: string
99
}
1010

11+
export type ApiTokenScope = |
12+
'task:read' |
13+
'task:write' |
14+
'label:read' |
15+
'label:write' |
16+
'user:read' |
17+
'user:write' |
18+
'token:write';
19+
1120
let isRefreshingAccessToken = false
1221
const isTokenNearExpiration = () => {
1322
const now = new Date()

src/views/Modals/Inputs/TextModal.tsx

-98
This file was deleted.
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ApiTokenScope } from '@/utils/api'
2+
import { moveFocusToJoyInput } from '@/utils/joy'
3+
import { TokenScopes } from '@/views/Tokens/TokenScopes'
4+
import { Box, Button, FormControl, Input, Modal, ModalDialog, Typography } from '@mui/joy'
5+
import React, { ChangeEvent } from 'react'
6+
7+
interface TokenModalProps {
8+
title: string
9+
current?: string
10+
okText?: string
11+
cancelText?: string
12+
13+
onClose: (newName: string | null, scopes: ApiTokenScope[]) => void
14+
}
15+
16+
interface TokenModalState {
17+
isOpen: boolean
18+
name: string
19+
errorName: boolean
20+
scopes: ApiTokenScope[]
21+
}
22+
23+
export class TokenModal extends React.Component<TokenModalProps, TokenModalState> {
24+
private inputRef = React.createRef<HTMLDivElement>()
25+
26+
constructor(props: TokenModalProps) {
27+
super(props)
28+
this.state = {
29+
isOpen: false,
30+
name: props.current ?? '',
31+
errorName: false,
32+
scopes: [],
33+
}
34+
}
35+
36+
public open = async (): Promise<void> => {
37+
await this.setState({
38+
isOpen: true,
39+
})
40+
41+
moveFocusToJoyInput(this.inputRef)
42+
}
43+
44+
private onSave = () => {
45+
const { name, scopes, errorName } = this.state
46+
47+
if (errorName) {
48+
return
49+
}
50+
51+
this.setState({
52+
isOpen: false,
53+
name: '',
54+
scopes: [],
55+
})
56+
57+
this.props.onClose(name, scopes)
58+
}
59+
60+
private onCancel = () => {
61+
this.setState({
62+
isOpen: false,
63+
name: '',
64+
scopes: [],
65+
})
66+
67+
this.props.onClose(null, [])
68+
}
69+
70+
private onNameChange = (e: ChangeEvent<HTMLInputElement>) => {
71+
this.setState({
72+
name: e.target.value,
73+
errorName: e.target.value.length === 0,
74+
})
75+
}
76+
77+
private onScopesChange = (scopes: ApiTokenScope[]) => {
78+
this.setState({
79+
scopes,
80+
})
81+
}
82+
83+
render(): React.ReactNode {
84+
const { title, okText, cancelText } = this.props
85+
const { name, isOpen, errorName, scopes } = this.state
86+
87+
const validState = name.length > 0 && scopes.length > 0
88+
89+
return (
90+
<Modal
91+
open={isOpen}
92+
onClose={this.onCancel}
93+
>
94+
<ModalDialog>
95+
<FormControl error>
96+
<Typography>{title}</Typography>
97+
<Input
98+
placeholder='Name your token'
99+
value={name}
100+
onChange={this.onNameChange}
101+
error={errorName}
102+
ref={this.inputRef}
103+
sx={{ minWidth: 300 }}
104+
/>
105+
</FormControl>
106+
107+
<TokenScopes onChange={this.onScopesChange} />
108+
109+
<Box
110+
display={'flex'}
111+
justifyContent={'space-around'}
112+
mt={1}
113+
>
114+
<Button
115+
onClick={this.onSave}
116+
fullWidth
117+
disabled={!validState}
118+
sx={{ mr: 1 }}
119+
>
120+
{okText ? okText : 'Save'}
121+
</Button>
122+
<Button
123+
onClick={this.onCancel}
124+
variant='outlined'
125+
>
126+
{cancelText ? cancelText : 'Cancel'}
127+
</Button>
128+
</Box>
129+
</ModalDialog>
130+
</Modal>
131+
)
132+
}
133+
}

src/views/Settings/APITokenSettings.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {
1616
} from '@mui/joy'
1717
import moment from 'moment'
1818
import React from 'react'
19-
import { TextModal } from '../Modals/Inputs/TextModal'
19+
import { TokenModal } from '../Modals/Inputs/TokenModal'
2020
import { ConfirmationModal } from '../Modals/Inputs/ConfirmationModal'
21+
import { ApiTokenScope } from '@/utils/api'
2122

2223
type APITokenSettingsProps = object
2324

@@ -30,7 +31,7 @@ export class APITokenSettings extends React.Component<
3031
APITokenSettingsProps,
3132
APITokenSettingsState
3233
> {
33-
private modalRef = React.createRef<TextModal>()
34+
private modalRef = React.createRef<TokenModal>()
3435
private tokenToDelete: APIToken | null = null
3536
private confirmModalRef = React.createRef<ConfirmationModal>()
3637

@@ -52,12 +53,12 @@ export class APITokenSettings extends React.Component<
5253
this.loadTokens()
5354
}
5455

55-
private handleSaveToken = async (name: string | null) => {
56+
private handleSaveToken = async (name: string | null, scopes: ApiTokenScope[]) => {
5657
if (!name) {
5758
return
5859
}
5960

60-
const data = await CreateLongLiveToken(name)
61+
const data = await CreateLongLiveToken(name, scopes)
6162
const newTokens = [...this.state.tokens]
6263
newTokens.push(data.token)
6364

@@ -125,7 +126,7 @@ export class APITokenSettings extends React.Component<
125126
<Box>
126127
<Typography level='body-md'>{token.name}</Typography>
127128
<Typography level='body-xs'>
128-
{moment(token.createdAt).fromNow()}<br />({moment(token.createdAt).format('lll')})
129+
{moment(token.createdAt).fromNow()}<br />
129130
</Typography>
130131
</Box>
131132
<Box>
@@ -179,7 +180,7 @@ export class APITokenSettings extends React.Component<
179180
Generate New Token
180181
</Button>
181182

182-
<TextModal
183+
<TokenModal
183184
ref={this.modalRef}
184185
title='Give a name for your new token:'
185186
onClose={this.handleSaveToken}

0 commit comments

Comments
 (0)