Skip to content

Commit 811a69f

Browse files
authored
feat(site): add ability to create tokens from account tokens page (#6608)
* add token actions * added basic token form * removed token switch * refined date field * limiting lifetime days to maxTokenLifetime * broke apart files * added loader and error * fixed form layout * added some unit tests * fixed be tests * no authorize check
1 parent af61847 commit 811a69f

27 files changed

+737
-131
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"thead",
137137
"tios",
138138
"tmpdir",
139+
"tokenconfig",
139140
"tparallel",
140141
"trialer",
141142
"trimprefix",

coderd/apidoc/docs.go

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apikey.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"database/sql"
77
"errors"
88
"fmt"
9+
"math"
910
"net"
1011
"net/http"
1112
"strconv"
@@ -339,6 +340,38 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
339340
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
340341
}
341342

343+
// @Summary Get token config
344+
// @ID get-token-config
345+
// @Security CoderSessionToken
346+
// @Produce json
347+
// @Tags General
348+
// @Param user path string true "User ID, name, or me"
349+
// @Success 200 {object} codersdk.TokenConfig
350+
// @Router /users/{user}/keys/tokens/tokenconfig [get]
351+
func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
352+
values, err := api.DeploymentValues.WithoutSecrets()
353+
if err != nil {
354+
httpapi.InternalServerError(rw, err)
355+
return
356+
}
357+
358+
var maxTokenLifetime time.Duration
359+
// if --max-token-lifetime is unset (default value is math.MaxInt64)
360+
// send back a falsy value
361+
if values.MaxTokenLifetime.Value() == time.Duration(math.MaxInt64) {
362+
maxTokenLifetime = 0
363+
} else {
364+
maxTokenLifetime = values.MaxTokenLifetime.Value()
365+
}
366+
367+
httpapi.Write(
368+
r.Context(), rw, http.StatusOK,
369+
codersdk.TokenConfig{
370+
MaxTokenLifetime: maxTokenLifetime,
371+
},
372+
)
373+
}
374+
342375
// Generates a new ID and secret for an API key.
343376
func GenerateAPIKeyIDSecret() (id string, secret string, err error) {
344377
// Length of an API Key ID.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ func New(options *Options) *API {
561561
r.Route("/tokens", func(r chi.Router) {
562562
r.Post("/", api.postToken)
563563
r.Get("/", api.tokens)
564+
r.Get("/tokenconfig", api.tokenConfig)
564565
r.Route("/{keyname}", func(r chi.Router) {
565566
r.Get("/", api.apiKeyByName)
566567
})

coderd/coderdtest/authorize.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
9999
AssertObject: rbac.ResourceAPIKey,
100100
AssertAction: rbac.ActionRead,
101101
},
102+
"GET:/api/v2/users/{user}/keys/tokens/tokenconfig": {NoAuthorize: true},
102103
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
103104
AssertAction: rbac.ActionRead,
104105
AssertObject: workspaceRBACObj,

codersdk/apikey.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ type APIKeyWithOwner struct {
9797
Username string `json:"username"`
9898
}
9999

100+
type TokenConfig struct {
101+
MaxTokenLifetime time.Duration `json:"max_token_lifetime"`
102+
}
103+
100104
// asRequestOption returns a function that can be used in (*Client).Request.
101105
// It modifies the request query parameters.
102106
func (f TokensFilter) asRequestOption() RequestOption {
@@ -161,3 +165,17 @@ func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) err
161165
}
162166
return nil
163167
}
168+
169+
// GetTokenConfig returns deployment options related to token management
170+
func (c *Client) GetTokenConfig(ctx context.Context, userID string) (TokenConfig, error) {
171+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/tokenconfig", userID), nil)
172+
if err != nil {
173+
return TokenConfig{}, err
174+
}
175+
defer res.Body.Close()
176+
if res.StatusCode > http.StatusOK {
177+
return TokenConfig{}, ReadBodyAsError(res)
178+
}
179+
tokenConfig := TokenConfig{}
180+
return tokenConfig, json.NewDecoder(res.Body).Decode(&tokenConfig)
181+
}

docs/api/general.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,40 @@ curl -X GET http://coder-server:8080/api/v2/updatecheck \
516516
| Status | Meaning | Description | Schema |
517517
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
518518
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UpdateCheckResponse](schemas.md#codersdkupdatecheckresponse) |
519+
520+
## Get token config
521+
522+
### Code samples
523+
524+
```shell
525+
# Example request using curl
526+
curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/tokenconfig \
527+
-H 'Accept: application/json' \
528+
-H 'Coder-Session-Token: API_KEY'
529+
```
530+
531+
`GET /users/{user}/keys/tokens/tokenconfig`
532+
533+
### Parameters
534+
535+
| Name | In | Type | Required | Description |
536+
| ------ | ---- | ------ | -------- | -------------------- |
537+
| `user` | path | string | true | User ID, name, or me |
538+
539+
### Example responses
540+
541+
> 200 Response
542+
543+
```json
544+
{
545+
"max_token_lifetime": 0
546+
}
547+
```
548+
549+
### Responses
550+
551+
| Status | Meaning | Description | Schema |
552+
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------ |
553+
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.TokenConfig](schemas.md#codersdktokenconfig) |
554+
555+
To perform this operation, you must be authenticated. [Learn more](authentication.md).

docs/api/schemas.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,6 +3840,20 @@ Parameter represents a set value for the scope.
38403840
| `type` | `number` |
38413841
| `type` | `bool` |
38423842

3843+
## codersdk.TokenConfig
3844+
3845+
```json
3846+
{
3847+
"max_token_lifetime": 0
3848+
}
3849+
```
3850+
3851+
### Properties
3852+
3853+
| Name | Type | Required | Restrictions | Description |
3854+
| -------------------- | ------- | -------- | ------------ | ----------- |
3855+
| `max_token_lifetime` | integer | false | | |
3856+
38433857
## codersdk.TraceConfig
38443858

38453859
```json

site/src/AppRouter.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ const WorkspaceSettingsPage = lazy(
129129
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
130130
)
131131

132+
const CreateTokenPage = lazy(
133+
() => import("./pages/CreateTokenPage/CreateTokenPage"),
134+
)
135+
132136
export const AppRouter: FC = () => {
133137
return (
134138
<Suspense fallback={<FullScreenLoader />}>
@@ -217,7 +221,10 @@ export const AppRouter: FC = () => {
217221
<Route path="account" element={<AccountPage />} />
218222
<Route path="security" element={<SecurityPage />} />
219223
<Route path="ssh-keys" element={<SSHKeysPage />} />
220-
<Route path="tokens" element={<TokensPage />} />
224+
<Route path="tokens">
225+
<Route index element={<TokensPage />} />
226+
<Route path="new" element={<CreateTokenPage />} />
227+
</Route>
221228
</Route>
222229

223230
<Route path="/@:username">

site/src/api/api.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,22 @@ export const getTokens = async (
153153
return response.data
154154
}
155155

156-
export const deleteAPIKey = async (keyId: string): Promise<void> => {
156+
export const deleteToken = async (keyId: string): Promise<void> => {
157157
await axios.delete("/api/v2/users/me/keys/" + keyId)
158158
}
159159

160+
export const createToken = async (
161+
params: TypesGen.CreateTokenRequest,
162+
): Promise<TypesGen.GenerateAPIKeyResponse> => {
163+
const response = await axios.post(`/api/v2/users/me/keys/tokens`, params)
164+
return response.data
165+
}
166+
167+
export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
168+
const response = await axios.get("/api/v2/users/me/keys/tokens/tokenconfig")
169+
return response.data
170+
}
171+
160172
export const getUsers = async (
161173
options: TypesGen.UsersRequest,
162174
): Promise<TypesGen.GetUsersResponse> => {

site/src/api/typesGenerated.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,12 @@ export interface TemplateVersionsByTemplateRequest extends Pagination {
847847
readonly template_id: string
848848
}
849849

850+
// From codersdk/apikey.go
851+
export interface TokenConfig {
852+
// This is likely an enum in an external package ("time.Duration")
853+
readonly max_token_lifetime: number
854+
}
855+
850856
// From codersdk/apikey.go
851857
export interface TokensFilter {
852858
readonly include_all: boolean

site/src/components/Form/Form.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const FormSection: FC<
6464
description: string | JSX.Element
6565
classes?: {
6666
root?: string
67+
sectionInfo?: string
6768
infoTitle?: string
6869
}
6970
}
@@ -73,7 +74,12 @@ export const FormSection: FC<
7374

7475
return (
7576
<div className={combineClasses([styles.formSection, classes.root])}>
76-
<div className={styles.formSectionInfo}>
77+
<div
78+
className={combineClasses([
79+
classes.sectionInfo,
80+
styles.formSectionInfo,
81+
])}
82+
>
7783
<h2
7884
className={combineClasses([
7985
styles.formSectionInfoTitle,

site/src/components/FullPageForm/FullPageHorizontalForm.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { makeStyles } from "@material-ui/core/styles"
1111
export interface FullPageHorizontalFormProps {
1212
title: string
1313
detail?: ReactNode
14-
onCancel: () => void
14+
onCancel?: () => void
1515
}
1616

1717
export const FullPageHorizontalForm: FC<
@@ -23,9 +23,11 @@ export const FullPageHorizontalForm: FC<
2323
<Margins size="medium">
2424
<PageHeader
2525
actions={
26-
<Button variant="outlined" size="small" onClick={onCancel}>
27-
Cancel
28-
</Button>
26+
onCancel && (
27+
<Button variant="outlined" size="small" onClick={onCancel}>
28+
Cancel
29+
</Button>
30+
)
2931
}
3032
>
3133
<PageHeaderTitle>{title}</PageHeaderTitle>

0 commit comments

Comments
 (0)