@package-broker/ui 0.19.7 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/api-types.d.ts +9987 -0
- package/dist/api-types.d.ts.map +1 -0
- package/dist/api-types.js +6 -0
- package/dist/api-types.js.map +1 -0
- package/dist/api.d.ts +88 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +179 -0
- package/dist/api.js.map +1 -0
- package/dist/app.d.ts +76 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +239 -0
- package/dist/app.js.map +1 -0
- package/dist/assets/app.0348e5888f99.js +15 -0
- package/dist/assets/app.e8cea8b32d3a.css +2 -0
- package/dist/assets/index.html +13 -0
- package/dist/assets/manifest.json +26 -0
- package/dist/assets/setup.98343039898f.js +1 -0
- package/dist/assets/setup.html +14 -0
- package/dist/auth.d.ts +170 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +551 -0
- package/dist/auth.js.map +1 -0
- package/dist/components/ui/alert.d.ts +10 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +22 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/badge.d.ts +8 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +18 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/button.d.ts +13 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +27 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +10 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +21 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/confirm-action.d.ts +12 -0
- package/dist/components/ui/confirm-action.d.ts.map +1 -0
- package/dist/components/ui/confirm-action.js +19 -0
- package/dist/components/ui/confirm-action.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +180 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +8 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +71 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/id-text.d.ts +11 -0
- package/dist/components/ui/id-text.d.ts.map +1 -0
- package/dist/components/ui/id-text.js +23 -0
- package/dist/components/ui/id-text.js.map +1 -0
- package/dist/components/ui/input.d.ts +5 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +6 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +7 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/list-state.d.ts +7 -0
- package/dist/components/ui/list-state.d.ts.map +1 -0
- package/dist/components/ui/list-state.js +5 -0
- package/dist/components/ui/list-state.js.map +1 -0
- package/dist/components/ui/overlay-host.d.ts +6 -0
- package/dist/components/ui/overlay-host.d.ts.map +1 -0
- package/dist/components/ui/overlay-host.js +36 -0
- package/dist/components/ui/overlay-host.js.map +1 -0
- package/dist/components/ui/row-actions.d.ts +17 -0
- package/dist/components/ui/row-actions.d.ts.map +1 -0
- package/dist/components/ui/row-actions.js +21 -0
- package/dist/components/ui/row-actions.js.map +1 -0
- package/dist/components/ui/select.d.ts +3 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +6 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/status-badge.d.ts +28 -0
- package/dist/components/ui/status-badge.d.ts.map +1 -0
- package/dist/components/ui/status-badge.js +50 -0
- package/dist/components/ui/status-badge.js.map +1 -0
- package/dist/components/ui/table.d.ts +27 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +81 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +14 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +14 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/time.d.ts +5 -0
- package/dist/components/ui/time.d.ts.map +1 -0
- package/dist/components/ui/time.js +14 -0
- package/dist/components/ui/time.js.map +1 -0
- package/dist/components/ui/toolbar.d.ts +20 -0
- package/dist/components/ui/toolbar.d.ts.map +1 -0
- package/dist/components/ui/toolbar.js +42 -0
- package/dist/components/ui/toolbar.js.map +1 -0
- package/dist/components/vulnerability-status.d.ts +8 -0
- package/dist/components/vulnerability-status.d.ts.map +1 -0
- package/dist/components/vulnerability-status.js +49 -0
- package/dist/components/vulnerability-status.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/use-merged-refs.d.ts +3 -0
- package/dist/lib/use-merged-refs.d.ts.map +1 -0
- package/dist/lib/use-merged-refs.js +22 -0
- package/dist/lib/use-merged-refs.js.map +1 -0
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +11 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +9 -0
- package/dist/main.js.map +1 -0
- package/dist/manifest.d.ts +8 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +21 -0
- package/dist/manifest.js.map +1 -0
- package/dist/page-hash.d.ts +23 -0
- package/dist/page-hash.d.ts.map +1 -0
- package/dist/page-hash.js +45 -0
- package/dist/page-hash.js.map +1 -0
- package/dist/pages/access.d.ts +69 -0
- package/dist/pages/access.d.ts.map +1 -0
- package/dist/pages/access.js +391 -0
- package/dist/pages/access.js.map +1 -0
- package/dist/pages/audit.d.ts +63 -0
- package/dist/pages/audit.d.ts.map +1 -0
- package/dist/pages/audit.js +182 -0
- package/dist/pages/audit.js.map +1 -0
- package/dist/pages/import.d.ts +41 -0
- package/dist/pages/import.d.ts.map +1 -0
- package/dist/pages/import.js +274 -0
- package/dist/pages/import.js.map +1 -0
- package/dist/pages/management.d.ts +47 -0
- package/dist/pages/management.d.ts.map +1 -0
- package/dist/pages/management.js +141 -0
- package/dist/pages/management.js.map +1 -0
- package/dist/pages/packages.d.ts +74 -0
- package/dist/pages/packages.d.ts.map +1 -0
- package/dist/pages/packages.js +505 -0
- package/dist/pages/packages.js.map +1 -0
- package/dist/pages/projects.d.ts +58 -0
- package/dist/pages/projects.d.ts.map +1 -0
- package/dist/pages/projects.js +357 -0
- package/dist/pages/projects.js.map +1 -0
- package/dist/pages/sources/add-package-dialog.d.ts +10 -0
- package/dist/pages/sources/add-package-dialog.d.ts.map +1 -0
- package/dist/pages/sources/add-package-dialog.js +19 -0
- package/dist/pages/sources/add-package-dialog.js.map +1 -0
- package/dist/pages/sources/proxy-message.d.ts +2 -0
- package/dist/pages/sources/proxy-message.d.ts.map +1 -0
- package/dist/pages/sources/proxy-message.js +2 -0
- package/dist/pages/sources/proxy-message.js.map +1 -0
- package/dist/pages/sources/source-form.d.ts +29 -0
- package/dist/pages/sources/source-form.d.ts.map +1 -0
- package/dist/pages/sources/source-form.js +463 -0
- package/dist/pages/sources/source-form.js.map +1 -0
- package/dist/pages/sources/sync-poll.d.ts +20 -0
- package/dist/pages/sources/sync-poll.d.ts.map +1 -0
- package/dist/pages/sources/sync-poll.js +71 -0
- package/dist/pages/sources/sync-poll.js.map +1 -0
- package/dist/pages/sources.d.ts +325 -0
- package/dist/pages/sources.d.ts.map +1 -0
- package/dist/pages/sources.js +1132 -0
- package/dist/pages/sources.js.map +1 -0
- package/dist/pages/tokens/client-config.d.ts +5 -0
- package/dist/pages/tokens/client-config.d.ts.map +1 -0
- package/dist/pages/tokens/client-config.js +9 -0
- package/dist/pages/tokens/client-config.js.map +1 -0
- package/dist/pages/tokens.d.ts +127 -0
- package/dist/pages/tokens.d.ts.map +1 -0
- package/dist/pages/tokens.js +744 -0
- package/dist/pages/tokens.js.map +1 -0
- package/dist/pending.d.ts +33 -0
- package/dist/pending.d.ts.map +1 -0
- package/dist/pending.js +156 -0
- package/dist/pending.js.map +1 -0
- package/dist/qr.d.ts +14 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +322 -0
- package/dist/qr.js.map +1 -0
- package/dist/setup.d.ts +49 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +390 -0
- package/dist/setup.js.map +1 -0
- package/dist/test-fixtures/api-types.d.ts +228 -0
- package/dist/test-fixtures/api-types.d.ts.map +1 -0
- package/dist/test-fixtures/api-types.js +6 -0
- package/dist/test-fixtures/api-types.js.map +1 -0
- package/package.json +37 -40
- package/public/index.html +11 -0
- package/README.md +0 -11
- package/dist/assets/main-3NLwl1eV.js +0 -111
- package/dist/assets/main-CsTEYyDl.css +0 -1
- package/dist/config.js +0 -3
- package/dist/index.html +0 -22
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useServices } from '../auth.js';
|
|
4
|
+
import { loadPending, savePending } from '../pending.js';
|
|
5
|
+
import { Button } from '../components/ui/button.js';
|
|
6
|
+
import { Input } from '../components/ui/input.js';
|
|
7
|
+
import { Label } from '../components/ui/label.js';
|
|
8
|
+
import { Select } from '../components/ui/select.js';
|
|
9
|
+
import { Alert, AlertDescription } from '../components/ui/alert.js';
|
|
10
|
+
import { DataTable } from '../components/ui/table.js';
|
|
11
|
+
import { StatusBadge } from '../components/ui/status-badge.js';
|
|
12
|
+
import { IdText } from '../components/ui/id-text.js';
|
|
13
|
+
import { Time } from '../components/ui/time.js';
|
|
14
|
+
import { Badge } from '../components/ui/badge.js';
|
|
15
|
+
import { LoadedListFilters } from '../components/ui/toolbar.js';
|
|
16
|
+
import { ListState } from '../components/ui/list-state.js';
|
|
17
|
+
import { ConfirmAction } from '../components/ui/confirm-action.js';
|
|
18
|
+
import { tokenClientConfig } from './tokens/client-config.js';
|
|
19
|
+
export function lifetimeChoices(policy) {
|
|
20
|
+
return [...new Set([30, 90, 180, 365, policy.maxDays, policy.defaultDays])]
|
|
21
|
+
.filter((days) => days > 0 && days <= policy.maxDays)
|
|
22
|
+
.sort((a, b) => a - b);
|
|
23
|
+
}
|
|
24
|
+
export function tokensReducer(state, action) {
|
|
25
|
+
if (action.type === 'navigate')
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
secret: null,
|
|
29
|
+
secretProject: null,
|
|
30
|
+
issuedId: null,
|
|
31
|
+
oldPersonalId: null,
|
|
32
|
+
pending: null,
|
|
33
|
+
renewal: null,
|
|
34
|
+
busy: false,
|
|
35
|
+
notice: null,
|
|
36
|
+
};
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
function pendingStorage(storage) {
|
|
40
|
+
return {
|
|
41
|
+
getItem: (key) => storage.getItem(`tokens:${key}`),
|
|
42
|
+
setItem: (key, value) => storage.setItem(`tokens:${key}`, value),
|
|
43
|
+
removeItem: (key) => storage.removeItem(`tokens:${key}`),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function clearPendingTokenIssue(storage) {
|
|
47
|
+
loadPending(pendingStorage(storage), Date.now());
|
|
48
|
+
}
|
|
49
|
+
function record(value) {
|
|
50
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
function restoredIssue(value) {
|
|
53
|
+
if (!record(value) || !record(value.draft))
|
|
54
|
+
return null;
|
|
55
|
+
const d = value.draft;
|
|
56
|
+
if (!['personal', 'project', 'organization'].includes(String(value.target)) ||
|
|
57
|
+
!(value.project === null || typeof value.project === 'string') ||
|
|
58
|
+
typeof value.key !== 'string' ||
|
|
59
|
+
typeof d.name !== 'string' ||
|
|
60
|
+
!['read', 'write', 'full'].includes(String(d.scope)) ||
|
|
61
|
+
!Number.isInteger(d.lifetimeDays) ||
|
|
62
|
+
Number(d.lifetimeDays) < 1 ||
|
|
63
|
+
typeof d.servicePrincipalId !== 'string' ||
|
|
64
|
+
!['all', 'selected'].includes(String(d.projectMode)) ||
|
|
65
|
+
!Array.isArray(d.projectIds) ||
|
|
66
|
+
!d.projectIds.every((id) => typeof id === 'string') ||
|
|
67
|
+
(value.rotate !== undefined && typeof value.rotate !== 'string') ||
|
|
68
|
+
(value.replace !== undefined && typeof value.replace !== 'string') ||
|
|
69
|
+
(d.overlapDays !== undefined && ![0, 1, 7].includes(Number(d.overlapDays))))
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
target: value.target,
|
|
73
|
+
project: value.project,
|
|
74
|
+
key: value.key,
|
|
75
|
+
...(typeof value.rotate === 'string' ? { rotate: value.rotate } : {}),
|
|
76
|
+
...(typeof value.replace === 'string' ? { replace: value.replace } : {}),
|
|
77
|
+
draft: {
|
|
78
|
+
name: d.name,
|
|
79
|
+
scope: d.scope,
|
|
80
|
+
lifetimeDays: Number(d.lifetimeDays),
|
|
81
|
+
servicePrincipalId: d.servicePrincipalId,
|
|
82
|
+
projectMode: d.projectMode,
|
|
83
|
+
projectIds: [...d.projectIds],
|
|
84
|
+
...(d.overlapDays === undefined ? {} : { overlapDays: Number(d.overlapDays) }),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function publicToken(t) {
|
|
89
|
+
return {
|
|
90
|
+
id: t.id,
|
|
91
|
+
kind: t.kind,
|
|
92
|
+
name: t.name,
|
|
93
|
+
scope: t.scope,
|
|
94
|
+
readOnly: t.readOnly,
|
|
95
|
+
status: t.status,
|
|
96
|
+
projectMode: t.projectMode,
|
|
97
|
+
projectCount: t.projectCount,
|
|
98
|
+
projectId: t.projectId,
|
|
99
|
+
userId: t.userId,
|
|
100
|
+
createdByUserId: t.createdByUserId,
|
|
101
|
+
servicePrincipalId: t.servicePrincipalId,
|
|
102
|
+
createdAt: t.createdAt,
|
|
103
|
+
expiresAt: t.expiresAt,
|
|
104
|
+
lastUsedAt: t.lastUsedAt,
|
|
105
|
+
revokedAt: t.revokedAt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function createTokensController(services, deps) {
|
|
109
|
+
let state = {
|
|
110
|
+
target: 'personal',
|
|
111
|
+
project: null,
|
|
112
|
+
items: [],
|
|
113
|
+
next: null,
|
|
114
|
+
principals: [],
|
|
115
|
+
principalsNext: null,
|
|
116
|
+
principalsRestricted: false,
|
|
117
|
+
names: {},
|
|
118
|
+
projectSlugs: {},
|
|
119
|
+
lifetime: null,
|
|
120
|
+
userId: null,
|
|
121
|
+
secret: null,
|
|
122
|
+
secretProject: null,
|
|
123
|
+
issuedId: null,
|
|
124
|
+
oldPersonalId: null,
|
|
125
|
+
pending: null,
|
|
126
|
+
renewal: null,
|
|
127
|
+
busy: false,
|
|
128
|
+
loading: false,
|
|
129
|
+
listError: false,
|
|
130
|
+
notice: null,
|
|
131
|
+
};
|
|
132
|
+
let epoch = 0;
|
|
133
|
+
let listing = 0;
|
|
134
|
+
let navigation = 0;
|
|
135
|
+
const listeners = new Set();
|
|
136
|
+
const set = (next) => {
|
|
137
|
+
state = { ...state, ...next };
|
|
138
|
+
for (const listener of listeners)
|
|
139
|
+
listener();
|
|
140
|
+
};
|
|
141
|
+
const storage = pendingStorage(deps.storage);
|
|
142
|
+
function failure(result) {
|
|
143
|
+
if (result.code === 'reauth_required')
|
|
144
|
+
services.requireReauth(result.request, { credential: false, secretFields: [] });
|
|
145
|
+
else
|
|
146
|
+
set({
|
|
147
|
+
notice: result.status === 412 || result.status === 428
|
|
148
|
+
? 'The record changed. Reload before trying again.'
|
|
149
|
+
: 'Unable to update tokens. Try again.',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function send(action) {
|
|
153
|
+
if (state.busy)
|
|
154
|
+
return;
|
|
155
|
+
const version = ++epoch;
|
|
156
|
+
set({
|
|
157
|
+
busy: true,
|
|
158
|
+
secret: null,
|
|
159
|
+
secretProject: null,
|
|
160
|
+
issuedId: null,
|
|
161
|
+
oldPersonalId: null,
|
|
162
|
+
pending: null,
|
|
163
|
+
notice: null,
|
|
164
|
+
});
|
|
165
|
+
const { draft: d, target, project, key } = action;
|
|
166
|
+
const base = { name: d.name, scope: d.scope, lifetimeDays: d.lifetimeDays };
|
|
167
|
+
const service = { ...base, servicePrincipalId: d.servicePrincipalId };
|
|
168
|
+
const rotation = {
|
|
169
|
+
name: d.name,
|
|
170
|
+
lifetimeDays: d.lifetimeDays,
|
|
171
|
+
overlapDays: d.overlapDays ?? 0,
|
|
172
|
+
};
|
|
173
|
+
const result = action.rotate !== undefined
|
|
174
|
+
? target === 'project'
|
|
175
|
+
? await services.client.request('post', '/_pb/projects/{project}/tokens/{token}/rotate', {
|
|
176
|
+
params: { project: project, token: action.rotate },
|
|
177
|
+
body: rotation,
|
|
178
|
+
idempotencyKey: key,
|
|
179
|
+
})
|
|
180
|
+
: await services.client.request('post', '/_pb/tokens/{token}/rotate', {
|
|
181
|
+
params: { token: action.rotate },
|
|
182
|
+
body: rotation,
|
|
183
|
+
idempotencyKey: key,
|
|
184
|
+
})
|
|
185
|
+
: target === 'personal'
|
|
186
|
+
? await services.client.request('post', '/_pb/me/tokens', {
|
|
187
|
+
body: { ...base, projectMode: d.projectMode, projectIds: d.projectIds },
|
|
188
|
+
idempotencyKey: key,
|
|
189
|
+
})
|
|
190
|
+
: target === 'project'
|
|
191
|
+
? await services.client.request('post', '/_pb/projects/{project}/tokens', {
|
|
192
|
+
params: { project: project },
|
|
193
|
+
body: service,
|
|
194
|
+
idempotencyKey: key,
|
|
195
|
+
})
|
|
196
|
+
: await services.client.request('post', '/_pb/tokens', {
|
|
197
|
+
body: service,
|
|
198
|
+
idempotencyKey: key,
|
|
199
|
+
});
|
|
200
|
+
if (version !== epoch)
|
|
201
|
+
return;
|
|
202
|
+
set({ busy: false });
|
|
203
|
+
if (result.ok) {
|
|
204
|
+
set({
|
|
205
|
+
secret: result.data.token,
|
|
206
|
+
secretProject: target === 'project' ? project : null,
|
|
207
|
+
oldPersonalId: action.replace ?? null,
|
|
208
|
+
issuedId: result.data.id,
|
|
209
|
+
notice: 'Save this token now; it is shown once.',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (result.code === 'reauth_required') {
|
|
213
|
+
const saved = savePending(storage, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
path: '/_pb/me/tokens',
|
|
216
|
+
body: action,
|
|
217
|
+
idempotencyKey: key,
|
|
218
|
+
credential: false,
|
|
219
|
+
secretFields: [],
|
|
220
|
+
});
|
|
221
|
+
set({
|
|
222
|
+
secret: null,
|
|
223
|
+
notice: saved.ok
|
|
224
|
+
? 'Confirm your identity, then confirm token issue.'
|
|
225
|
+
: 'Unable to save the pending action.',
|
|
226
|
+
});
|
|
227
|
+
if (saved.ok)
|
|
228
|
+
deps.navigate(`#/reauth?return=${encodeURIComponent(`/tokens${project === null ? '' : `?project=${encodeURIComponent(project)}`}`)}`);
|
|
229
|
+
}
|
|
230
|
+
else if (result.code === 'already_issued')
|
|
231
|
+
set({ notice: 'already issued, create a new token' });
|
|
232
|
+
else
|
|
233
|
+
failure(result);
|
|
234
|
+
}
|
|
235
|
+
const controller = {
|
|
236
|
+
snapshot: () => state,
|
|
237
|
+
subscribe(listener) {
|
|
238
|
+
listeners.add(listener);
|
|
239
|
+
return () => {
|
|
240
|
+
listeners.delete(listener);
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
navigate() {
|
|
244
|
+
epoch++;
|
|
245
|
+
navigation++;
|
|
246
|
+
listing++;
|
|
247
|
+
set(tokensReducer(state, { type: 'navigate' }));
|
|
248
|
+
},
|
|
249
|
+
dispose() {
|
|
250
|
+
controller.navigate();
|
|
251
|
+
listeners.clear();
|
|
252
|
+
},
|
|
253
|
+
closeSecret() {
|
|
254
|
+
if (state.busy)
|
|
255
|
+
return;
|
|
256
|
+
epoch++;
|
|
257
|
+
set({ secret: null, issuedId: null, oldPersonalId: null, notice: null, renewal: null });
|
|
258
|
+
},
|
|
259
|
+
restore() {
|
|
260
|
+
const loaded = loadPending(storage, deps.now());
|
|
261
|
+
const action = loaded.ok ? restoredIssue(loaded.action?.body) : null;
|
|
262
|
+
if (action !== null)
|
|
263
|
+
set({ target: action.target, project: action.project, pending: action, secret: null });
|
|
264
|
+
},
|
|
265
|
+
async start(target, project) {
|
|
266
|
+
controller.restore();
|
|
267
|
+
const action = state.pending;
|
|
268
|
+
await controller.load(action?.target ?? target, action === null ? project : action.project);
|
|
269
|
+
},
|
|
270
|
+
async selectTarget(target, project) {
|
|
271
|
+
if (state.busy)
|
|
272
|
+
return;
|
|
273
|
+
epoch++;
|
|
274
|
+
set(tokensReducer(state, { type: 'navigate' }));
|
|
275
|
+
await controller.load(target, project);
|
|
276
|
+
},
|
|
277
|
+
decline() {
|
|
278
|
+
set({ pending: null });
|
|
279
|
+
},
|
|
280
|
+
async confirm() {
|
|
281
|
+
const action = state.pending;
|
|
282
|
+
if (action === null || state.busy || state.loading)
|
|
283
|
+
return;
|
|
284
|
+
if (state.lifetime === null) {
|
|
285
|
+
await controller.load(action.target, action.project);
|
|
286
|
+
if (state.pending !== action || state.lifetime === null || state.loading)
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await send(action);
|
|
290
|
+
},
|
|
291
|
+
async issue(draft) {
|
|
292
|
+
await send({
|
|
293
|
+
target: state.renewal?.kind === 'pat'
|
|
294
|
+
? 'personal'
|
|
295
|
+
: state.renewal?.projectId
|
|
296
|
+
? 'project'
|
|
297
|
+
: state.target,
|
|
298
|
+
project: state.renewal?.kind === 'svc' && state.renewal.projectId !== null
|
|
299
|
+
? (state.projectSlugs[state.renewal.projectId] ?? state.project)
|
|
300
|
+
: state.project,
|
|
301
|
+
draft,
|
|
302
|
+
key: deps.newKey(),
|
|
303
|
+
...(state.renewal?.kind === 'svc' ? { rotate: state.renewal.id } : {}),
|
|
304
|
+
...(state.renewal?.kind === 'pat' ? { replace: state.renewal.id } : {}),
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
renew(token) {
|
|
308
|
+
if (state.busy || (token.kind === 'pat' && token.userId !== state.userId))
|
|
309
|
+
return;
|
|
310
|
+
if (token.kind === 'svc' &&
|
|
311
|
+
token.projectId !== null &&
|
|
312
|
+
state.projectSlugs[token.projectId] === undefined &&
|
|
313
|
+
state.target !== 'project') {
|
|
314
|
+
set({ notice: 'Choose this token’s project scope before renewing.' });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
set({ renewal: token, secret: null, issuedId: null, oldPersonalId: null, notice: null });
|
|
318
|
+
},
|
|
319
|
+
registryOrigin: () => new URL(deps.origin).origin,
|
|
320
|
+
clientConfig() {
|
|
321
|
+
return state.secret === null || state.secretProject === null
|
|
322
|
+
? null
|
|
323
|
+
: tokenClientConfig(deps.origin, state.secretProject, state.secret);
|
|
324
|
+
},
|
|
325
|
+
async copy(kind) {
|
|
326
|
+
if (state.secret === null)
|
|
327
|
+
return;
|
|
328
|
+
const text = kind === undefined ? state.secret : controller.clientConfig()?.[kind];
|
|
329
|
+
if (text === undefined)
|
|
330
|
+
return;
|
|
331
|
+
const label = kind === undefined ? 'Token' : 'Configuration';
|
|
332
|
+
const version = epoch;
|
|
333
|
+
try {
|
|
334
|
+
await deps.clipboard(text);
|
|
335
|
+
if (version === epoch)
|
|
336
|
+
set({ ...(deps.clearAfterCopy ? { secret: null } : {}), notice: `${label} copied.` });
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
if (version === epoch)
|
|
340
|
+
set({
|
|
341
|
+
notice: kind === undefined ? 'Unable to copy token.' : 'Unable to copy configuration.',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
async load(target = state.target, project = state.project, after) {
|
|
346
|
+
const version = ++listing;
|
|
347
|
+
set({
|
|
348
|
+
target,
|
|
349
|
+
project,
|
|
350
|
+
loading: true,
|
|
351
|
+
listError: false,
|
|
352
|
+
notice: null,
|
|
353
|
+
items: [],
|
|
354
|
+
next: null,
|
|
355
|
+
lifetime: null,
|
|
356
|
+
});
|
|
357
|
+
const result = target === 'personal'
|
|
358
|
+
? await services.client.request('get', '/_pb/me/tokens', { query: { after } })
|
|
359
|
+
: target === 'project' && project !== null
|
|
360
|
+
? await services.client.request('get', '/_pb/projects/{project}/tokens', {
|
|
361
|
+
params: { project },
|
|
362
|
+
query: { after },
|
|
363
|
+
})
|
|
364
|
+
: await services.client.request('get', '/_pb/tokens', { query: { after } });
|
|
365
|
+
if (version !== listing)
|
|
366
|
+
return;
|
|
367
|
+
set(result.ok
|
|
368
|
+
? {
|
|
369
|
+
items: result.data.items.map(publicToken),
|
|
370
|
+
next: result.data.next,
|
|
371
|
+
lifetime: result.data.lifetime,
|
|
372
|
+
loading: false,
|
|
373
|
+
}
|
|
374
|
+
: { loading: false, listError: true, lifetime: null, notice: 'Unable to load tokens.' });
|
|
375
|
+
},
|
|
376
|
+
async loadPrincipals(after) {
|
|
377
|
+
const version = navigation;
|
|
378
|
+
const result = await services.client.request('get', '/_pb/service-principals', {
|
|
379
|
+
query: { after },
|
|
380
|
+
});
|
|
381
|
+
if (version !== navigation)
|
|
382
|
+
return;
|
|
383
|
+
if (result.ok)
|
|
384
|
+
set({
|
|
385
|
+
principals: result.data.items.map((p) => ({
|
|
386
|
+
id: p.id,
|
|
387
|
+
name: p.name,
|
|
388
|
+
projectId: p.projectId,
|
|
389
|
+
status: p.status,
|
|
390
|
+
revision: p.revision,
|
|
391
|
+
createdByUserId: p.createdByUserId,
|
|
392
|
+
permissions: p.permissions,
|
|
393
|
+
})),
|
|
394
|
+
principalsNext: result.data.next,
|
|
395
|
+
principalsRestricted: false,
|
|
396
|
+
});
|
|
397
|
+
else if (result.status === 403)
|
|
398
|
+
set({ principals: [], principalsNext: null, principalsRestricted: true });
|
|
399
|
+
},
|
|
400
|
+
async identity() {
|
|
401
|
+
const version = navigation;
|
|
402
|
+
const result = await services.client.request('get', '/_pb/session');
|
|
403
|
+
if (version !== navigation)
|
|
404
|
+
return;
|
|
405
|
+
if (result.ok)
|
|
406
|
+
set({ userId: result.data.user.id });
|
|
407
|
+
const projectSlugs = {};
|
|
408
|
+
let projectAfter;
|
|
409
|
+
do {
|
|
410
|
+
const projects = await services.client.request('get', '/_pb/projects', {
|
|
411
|
+
query: { after: projectAfter },
|
|
412
|
+
});
|
|
413
|
+
if (version !== navigation)
|
|
414
|
+
return;
|
|
415
|
+
if (!projects.ok)
|
|
416
|
+
break;
|
|
417
|
+
for (const project of projects.data.items)
|
|
418
|
+
projectSlugs[project.id] = project.slug;
|
|
419
|
+
projectAfter = projects.data.next ?? undefined;
|
|
420
|
+
} while (projectAfter !== undefined);
|
|
421
|
+
set({ projectSlugs });
|
|
422
|
+
const permissions = await services.client.request('get', '/_pb/me/permissions');
|
|
423
|
+
if (version !== navigation)
|
|
424
|
+
return;
|
|
425
|
+
if (!permissions.ok || !permissions.data.organization.includes('org:users:read')) {
|
|
426
|
+
set({ names: {} });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
let after;
|
|
430
|
+
const names = {};
|
|
431
|
+
do {
|
|
432
|
+
const users = await services.client.request('get', '/_pb/users', { query: { after } });
|
|
433
|
+
if (version !== navigation)
|
|
434
|
+
return;
|
|
435
|
+
if (!users.ok)
|
|
436
|
+
break;
|
|
437
|
+
for (const user of users.data.items)
|
|
438
|
+
if (user.displayName !== null)
|
|
439
|
+
names[user.id] = user.displayName;
|
|
440
|
+
after = users.data.next ?? undefined;
|
|
441
|
+
} while (after !== undefined);
|
|
442
|
+
if (version === navigation)
|
|
443
|
+
set({ names });
|
|
444
|
+
},
|
|
445
|
+
async revoke(id, own = false) {
|
|
446
|
+
if (state.busy)
|
|
447
|
+
return;
|
|
448
|
+
const version = epoch;
|
|
449
|
+
set({ busy: true, secret: null, issuedId: null });
|
|
450
|
+
const result = state.target === 'personal' || own
|
|
451
|
+
? await services.client.request('delete', '/_pb/me/tokens/{token}', {
|
|
452
|
+
params: { token: id },
|
|
453
|
+
})
|
|
454
|
+
: state.target === 'project'
|
|
455
|
+
? await services.client.request('delete', '/_pb/projects/{project}/tokens/{token}', {
|
|
456
|
+
params: { project: state.project, token: id },
|
|
457
|
+
})
|
|
458
|
+
: await services.client.request('delete', '/_pb/tokens/{token}', {
|
|
459
|
+
params: { token: id },
|
|
460
|
+
});
|
|
461
|
+
if (version !== epoch)
|
|
462
|
+
return;
|
|
463
|
+
set({ busy: false });
|
|
464
|
+
if (!result.ok)
|
|
465
|
+
failure(result);
|
|
466
|
+
else {
|
|
467
|
+
await controller.load();
|
|
468
|
+
set({ notice: 'Token revoked.', renewal: null });
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
async createPrincipal(name, project, permissions) {
|
|
472
|
+
if (state.busy)
|
|
473
|
+
return;
|
|
474
|
+
const version = epoch;
|
|
475
|
+
set({ busy: true, secret: null, issuedId: null });
|
|
476
|
+
const body = {
|
|
477
|
+
name,
|
|
478
|
+
permissions: permissions,
|
|
479
|
+
...(project === '' ? {} : { project }),
|
|
480
|
+
};
|
|
481
|
+
const result = await services.client.request('post', '/_pb/service-principals', {
|
|
482
|
+
body,
|
|
483
|
+
idempotencyKey: deps.newKey(),
|
|
484
|
+
});
|
|
485
|
+
if (version !== epoch)
|
|
486
|
+
return;
|
|
487
|
+
set({ busy: false });
|
|
488
|
+
if (!result.ok)
|
|
489
|
+
failure(result);
|
|
490
|
+
else {
|
|
491
|
+
await controller.loadPrincipals();
|
|
492
|
+
set({ notice: 'Service principal created.' });
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
async suspendPrincipal(principal) {
|
|
496
|
+
if (state.busy)
|
|
497
|
+
return;
|
|
498
|
+
const version = epoch;
|
|
499
|
+
set({ busy: true, secret: null, issuedId: null });
|
|
500
|
+
const project = principal.projectId === null ? undefined : state.projectSlugs[principal.projectId];
|
|
501
|
+
const result = project !== undefined
|
|
502
|
+
? await services.client.request('post', '/_pb/projects/{project}/service-principals/{principal}/suspend', { params: { project, principal: principal.id } })
|
|
503
|
+
: await services.client.request('post', '/_pb/service-principals/{principal}/suspend', {
|
|
504
|
+
params: { principal: principal.id },
|
|
505
|
+
});
|
|
506
|
+
if (version !== epoch)
|
|
507
|
+
return;
|
|
508
|
+
set({ busy: false });
|
|
509
|
+
if (!result.ok)
|
|
510
|
+
failure(result);
|
|
511
|
+
else {
|
|
512
|
+
await controller.loadPrincipals();
|
|
513
|
+
set({ notice: 'Service principal suspended; its tokens no longer work.' });
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
return controller;
|
|
518
|
+
}
|
|
519
|
+
export function filterTokens(items, filters) {
|
|
520
|
+
return items.filter((item) => item.name.toLowerCase().includes(filters.q.toLowerCase()) &&
|
|
521
|
+
(!filters.scope || item.scope === filters.scope) &&
|
|
522
|
+
(!filters.status || item.status === filters.status));
|
|
523
|
+
}
|
|
524
|
+
export function TokenList({ state: s, controller: c }) {
|
|
525
|
+
const [filters, setFilters] = useState({ q: '', scope: '', status: '' });
|
|
526
|
+
const filtered = !!(filters.q || filters.scope || filters.status);
|
|
527
|
+
const items = filterTokens(s.items, filters);
|
|
528
|
+
const scope = s.target === 'personal'
|
|
529
|
+
? 'My personal tokens'
|
|
530
|
+
: s.target === 'project'
|
|
531
|
+
? `Project: ${s.project}`
|
|
532
|
+
: 'Organization';
|
|
533
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoadedListFilters, { values: filters, fields: [
|
|
534
|
+
{ key: 'q', label: 'Name (this page)' },
|
|
535
|
+
{ key: 'scope', label: 'Scope', options: ['read', 'write', 'full'] },
|
|
536
|
+
{ key: 'status', label: 'Status', options: ['active', 'expired', 'revoked'] },
|
|
537
|
+
], onChange: setFilters }), _jsxs("div", { className: "my-3 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("p", { className: "text-sm text-neutral-600", children: [items.length, " tokens on this page \u00B7", ' ', items.filter((token) => token.status === 'active').length, " active"] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.loading || s.busy, onClick: () => void c.load(), children: "Reload tokens" })] }), s.loading ? (_jsx(ListState, { kind: "loading", title: "Loading tokens\u2026" })) : s.listError ? (_jsx(ListState, { kind: "error", title: "Unable to load tokens." })) : items.length === 0 ? (_jsx(ListState, { kind: filtered ? 'filtered-empty' : 'empty', title: filtered ? 'No tokens match these filters on this page.' : 'No tokens on this page.' })) : (_jsx(DataTable, { label: "Tokens", items: items, columns: [
|
|
538
|
+
{
|
|
539
|
+
key: 'name',
|
|
540
|
+
label: 'Token',
|
|
541
|
+
value: (token) => token.name,
|
|
542
|
+
render: (token) => (_jsxs("div", { className: "grid gap-1", children: [_jsx("strong", { children: token.name }), _jsx(IdText, { value: token.id, label: "token ID" })] })),
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
key: 'status',
|
|
546
|
+
label: 'Status',
|
|
547
|
+
value: (token) => token.status,
|
|
548
|
+
render: (token) => _jsx(StatusBadge, { status: token.status }),
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
key: 'scope',
|
|
552
|
+
label: 'Scope',
|
|
553
|
+
value: (token) => token.scope,
|
|
554
|
+
render: (token) => _jsx(Badge, { variant: "outline", children: token.scope }),
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
key: 'project',
|
|
558
|
+
label: 'Project',
|
|
559
|
+
value: (token) => token.projectId === null
|
|
560
|
+
? token.kind === 'pat'
|
|
561
|
+
? token.projectMode === 'all'
|
|
562
|
+
? 'All accessible projects'
|
|
563
|
+
: `${token.projectCount} selected projects`
|
|
564
|
+
: 'Organization'
|
|
565
|
+
: (s.projectSlugs[token.projectId] ?? token.projectId),
|
|
566
|
+
priority: 2,
|
|
567
|
+
render: (token) => token.projectId !== null && !s.projectSlugs[token.projectId] ? (_jsx(IdText, { value: token.projectId, label: "project ID" })) : token.projectId !== null ? (s.projectSlugs[token.projectId]) : token.kind === 'pat' ? (token.projectMode === 'all' ? ('All accessible projects') : (`${token.projectCount} selected projects`)) : ('Organization'),
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
key: 'lastUsedAt',
|
|
571
|
+
label: 'Last used',
|
|
572
|
+
value: (token) => token.lastUsedAt ?? '',
|
|
573
|
+
priority: 2,
|
|
574
|
+
render: (token) => _jsx(Time, { value: token.lastUsedAt, fallback: "Never" }),
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
key: 'expiresAt',
|
|
578
|
+
label: 'Expires',
|
|
579
|
+
value: (token) => token.expiresAt ?? '',
|
|
580
|
+
priority: 3,
|
|
581
|
+
render: (token) => _jsx(Time, { value: token.expiresAt, fallback: "None" }),
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
key: 'issuer',
|
|
585
|
+
label: 'Issued by',
|
|
586
|
+
value: (token) => token.kind === 'svc' && token.createdByUserId !== null
|
|
587
|
+
? (s.names[token.createdByUserId] ?? 'a member')
|
|
588
|
+
: 'a member',
|
|
589
|
+
priority: 3,
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
key: 'actions',
|
|
593
|
+
label: 'Actions',
|
|
594
|
+
value: () => '',
|
|
595
|
+
sortable: false,
|
|
596
|
+
render: (token) => (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: s.busy ||
|
|
597
|
+
token.status !== 'active' ||
|
|
598
|
+
(token.kind === 'pat' && token.userId !== s.userId), onClick: () => c.renew(token), children: "Renew" }), _jsx(ConfirmAction, { action: "Revoke", target: `${token.name} (${token.id})`, scope: scope, description: "Revoking this token stops clients using it from authenticating. This cannot be undone. Create a new token to restore client access.", disabled: s.busy || token.status === 'revoked', onConfirm: () => void c.revoke(token.id) }, `${token.id}:${token.status}`)] })),
|
|
599
|
+
},
|
|
600
|
+
] })), s.next === null ? null : (_jsx(Button, { type: "button", disabled: s.busy || s.loading, onClick: () => void c.load(s.target, s.project, s.next), children: "Next tokens" })), _jsx("p", { className: "mt-3 text-xs text-neutral-600", children: "Secrets are never listed. Only tokens in your accessible scope appear here." })] }));
|
|
601
|
+
}
|
|
602
|
+
export function TokensView({ state: s, controller: c }) {
|
|
603
|
+
const personal = s.renewal?.kind === 'pat' || s.target === 'personal';
|
|
604
|
+
const policy = s.lifetime?.[personal ? 'personal' : 'service'];
|
|
605
|
+
const renewal = s.renewal;
|
|
606
|
+
const clientConfig = c.clientConfig();
|
|
607
|
+
const projectId = renewal?.kind === 'svc'
|
|
608
|
+
? renewal.projectId
|
|
609
|
+
: s.target === 'project'
|
|
610
|
+
? Object.keys(s.projectSlugs).find((id) => s.projectSlugs[id] === s.project)
|
|
611
|
+
: null;
|
|
612
|
+
const principalChoices = s.principalsRestricted
|
|
613
|
+
? [
|
|
614
|
+
...new Map(s.items
|
|
615
|
+
.filter((t) => t.kind === 'svc' && t.status === 'active' && t.servicePrincipalId !== null)
|
|
616
|
+
.map((t) => [
|
|
617
|
+
t.servicePrincipalId,
|
|
618
|
+
{
|
|
619
|
+
id: t.servicePrincipalId,
|
|
620
|
+
name: t.servicePrincipalId,
|
|
621
|
+
projectId: t.projectId,
|
|
622
|
+
},
|
|
623
|
+
])).values(),
|
|
624
|
+
]
|
|
625
|
+
: s.principals.filter((p) => p.status === 'active');
|
|
626
|
+
function issue(event) {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
if (s.busy)
|
|
629
|
+
return;
|
|
630
|
+
const form = event.currentTarget;
|
|
631
|
+
const data = new FormData(form);
|
|
632
|
+
const scope = renewal?.kind === 'svc' ? renewal.scope : String(data.get('scope'));
|
|
633
|
+
void c.issue({
|
|
634
|
+
name: String(data.get('name')),
|
|
635
|
+
scope,
|
|
636
|
+
lifetimeDays: Number(data.get('lifetimeDays')),
|
|
637
|
+
servicePrincipalId: String(data.get('principal') ?? ''),
|
|
638
|
+
projectMode: data.get('projectMode') === 'selected' ? 'selected' : 'all',
|
|
639
|
+
projectIds: String(data.get('projectIds') ?? '')
|
|
640
|
+
.split(/\s+/)
|
|
641
|
+
.filter(Boolean),
|
|
642
|
+
overlapDays: Number(data.get('overlap') ?? 0),
|
|
643
|
+
});
|
|
644
|
+
form.reset();
|
|
645
|
+
}
|
|
646
|
+
return (_jsxs("section", { "aria-labelledby": "page-heading", className: "space-y-4", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Tokens" }), _jsx("p", { className: "text-neutral-600", children: "Credentials for Composer and npm clients. Management writes use your signed-in session. Secrets are shown once." }), _jsx(Label, { htmlFor: "token-list", children: "Token list" }), _jsxs(Select, { id: "token-list", value: s.target, disabled: s.busy, onChange: (e) => {
|
|
647
|
+
void c.selectTarget(e.target.value, s.project);
|
|
648
|
+
}, children: [_jsx("option", { value: "personal", children: "My personal tokens" }), s.project === null ? null : _jsx("option", { value: "project", children: "Project tokens" }), _jsx("option", { value: "organization", children: "Organization tokens" })] }), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-medium", children: "Token lifetimes and access" }), _jsx("p", { className: "mt-3", children: "Your personal tokens are revoked when you leave the organization or your account is suspended. Access stops at once; the tokens are marked revoked shortly after." }), _jsx("p", { children: "Service tokens keep working when the person who created them leaves. Project owners can see, renew and revoke every service token of their project." }), _jsxs("dl", { className: "detail-list mt-3", children: [_jsx("dt", { children: "Personal maximum lifetime" }), _jsx("dd", { children: "1 year (the operator can set less)" }), _jsx("dt", { children: "Service maximum lifetime" }), _jsx("dd", { children: "1 year for every scope (the operator can change it)" })] })] }), s.notice === null || s.listError ? null : (_jsx(Alert, { children: _jsx(AlertDescription, { children: s.notice }) })), s.pending === null ? null : (_jsxs("section", { "aria-label": "Confirm token issue", className: "space-y-2", children: [_jsx("h2", { children: "Confirm token issue" }), _jsx("p", { children: "Your identity was checked. Issue this token now?" }), _jsx(Button, { type: "button", disabled: s.busy || s.loading || s.lifetime === null, onClick: () => void c.confirm(), children: "Confirm token issue" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.decline(), children: "Cancel" })] })), s.secret === null ? null : (_jsxs("section", { "aria-label": "Token secret", className: "detail-group space-y-3 border-emerald-200", children: [_jsx("h2", { children: "Token secret \u2014 shown once" }), _jsx("p", { children: "Copy it now. This value will not be shown again. Closing this panel keeps the token active." }), _jsx("pre", { className: "rounded-md border bg-neutral-50 p-3 overflow-x-auto whitespace-pre-wrap break-all", children: _jsx("code", { children: s.secret }) }), _jsx(Button, { type: "button", onClick: () => void c.copy(), children: "Copy token" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.closeSecret(), children: "Close secret panel" }), clientConfig === null ? (_jsxs("p", { children: ["Choose a project this token can access to configure Composer or npm. Open that project\u2019s registry at ", c.registryOrigin(), "/<project> for Composer or", ' ', c.registryOrigin(), "/<project>/npm/ for npm."] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: "Composer" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.composer }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('composer'), children: "Copy Composer configuration" }), _jsx("h3", { children: "npm (.npmrc)" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.npm }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('npm'), children: "Copy npm configuration" })] })), s.oldPersonalId !== null ? (_jsx(ConfirmAction, { action: "Revoke old personal token", target: s.oldPersonalId, scope: "My personal tokens", description: "Clients using the old token will lose access immediately. This cannot be undone. Update clients to the new token before revoking.", disabled: s.busy, onConfirm: () => void c.revoke(s.oldPersonalId, true) })) : null] })), s.pending !== null || s.secret !== null ? null : (_jsxs("details", { open: renewal !== null, className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: renewal === null ? 'Issue token' : 'Renew token' }), _jsxs("form", { onSubmit: issue, className: "grid gap-2", children: [_jsx("h2", { children: renewal === null ? 'Issue token' : 'Renew token' }), _jsx(Label, { htmlFor: "token-name", children: "Name" }), _jsx(Input, { id: "token-name", name: "name", defaultValue: renewal?.name ?? '', required: true }), _jsx(Label, { htmlFor: "token-scope", children: "Scope" }), _jsxs(Select, { id: "token-scope", name: "scope", defaultValue: renewal?.scope ?? 'read', disabled: renewal?.kind === 'svc', children: [_jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "write", children: "Write" }), _jsx("option", { value: "full", children: "Full" })] }), _jsx(Label, { htmlFor: "token-lifetime", children: "Lifetime" }), _jsx(Select, { id: "token-lifetime", name: "lifetimeDays", defaultValue: policy?.defaultDays, required: true, children: policy === undefined
|
|
649
|
+
? null
|
|
650
|
+
: lifetimeChoices(policy).map((days) => (_jsxs("option", { value: days, children: [days, " days"] }, days))) }), personal ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-project-mode", children: "Projects" }), _jsxs(Select, { id: "token-project-mode", name: "projectMode", defaultValue: renewal?.projectMode ?? 'all', children: [_jsx("option", { value: "all", children: "All accessible projects" }), _jsx("option", { value: "selected", children: "Selected projects" })] }), _jsx(Label, { htmlFor: "token-project-ids", children: "Selected project IDs (space separated; re-enter when renewing)" }), _jsx(Input, { id: "token-project-ids", name: "projectIds", required: renewal?.projectMode === 'selected' })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-principal", children: "Service principal" }), _jsxs(Select, { id: "token-principal", name: "principal", defaultValue: renewal?.servicePrincipalId ?? '', disabled: renewal?.kind === 'svc', required: renewal?.kind !== 'svc', children: [_jsx("option", { value: "", disabled: true, children: "Select a principal" }), principalChoices
|
|
651
|
+
.filter((p) => p.projectId === projectId)
|
|
652
|
+
.map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), s.principalsRestricted ? (_jsx("p", { children: "Principal discovery is limited to principals referenced by the tokens you can see." })) : null] })), renewal?.kind !== 'svc' ? null : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-overlap", children: "Overlap" }), _jsxs(Select, { id: "token-overlap", name: "overlap", defaultValue: "0", children: [_jsx("option", { value: "0", children: "None" }), _jsx("option", { value: "1", children: "1 day" }), _jsx("option", { value: "7", children: "7 days" })] }), _jsx("p", { children: "The old token stops at the end of the overlap or when the new token expires, whichever is first." })] })), _jsx(Button, { type: "submit", disabled: s.busy || s.loading || policy === undefined, children: renewal === null ? 'Issue token' : 'Renew token' }), renewal === null ? null : (_jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => c.closeSecret(), children: "Cancel renewal" }))] }, `${s.target}:${renewal?.id ?? ''}:${policy?.defaultDays}`)] }, `${s.target}:${renewal?.id ?? ''}`)), _jsx(TokenList, { state: s, controller: c }, `${s.target}:${s.project}`), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: "Service principals" }), _jsx("h2", { children: "Service principals" }), _jsxs("form", { className: "grid gap-2", onSubmit: (event) => {
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
if (s.busy)
|
|
655
|
+
return;
|
|
656
|
+
const form = event.currentTarget;
|
|
657
|
+
const data = new FormData(form);
|
|
658
|
+
void c.createPrincipal(String(data.get('name')), String(data.get('project')), String(data.get('permissions')).split(/\s+/).filter(Boolean));
|
|
659
|
+
form.reset();
|
|
660
|
+
}, children: [_jsx(Label, { htmlFor: "principal-name", children: "Principal name" }), _jsx(Input, { id: "principal-name", name: "name", required: true }), _jsx(Label, { htmlFor: "principal-project", children: "Project ID (empty for organization)" }), _jsx(Input, { id: "principal-project", name: "project" }), _jsx(Label, { htmlFor: "principal-permissions", children: "Permissions (space separated)" }), _jsx(Input, { id: "principal-permissions", name: "permissions" }), _jsx(Button, { type: "submit", disabled: s.busy, children: "Create service principal" })] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => void c.loadPrincipals(), children: "Reload service principals" }), s.principalsRestricted ? (_jsx(ListState, { kind: "placeholder", title: "Service principal discovery is restricted." })) : s.principals.length === 0 ? (_jsx(ListState, { kind: "empty", title: "No service principals loaded." })) : (_jsx(DataTable, { label: "Service principals", items: s.principals, columns: [
|
|
661
|
+
{
|
|
662
|
+
key: 'name',
|
|
663
|
+
label: 'Name',
|
|
664
|
+
value: (principal) => principal.name,
|
|
665
|
+
render: (principal) => (_jsxs("div", { className: "grid gap-1", children: [_jsx("strong", { children: principal.name }), _jsx(IdText, { value: principal.id, label: "principal ID" })] })),
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
key: 'status',
|
|
669
|
+
label: 'Status',
|
|
670
|
+
value: (principal) => principal.status,
|
|
671
|
+
render: (principal) => _jsx(StatusBadge, { status: principal.status }),
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
key: 'actions',
|
|
675
|
+
label: 'Actions',
|
|
676
|
+
value: () => '',
|
|
677
|
+
sortable: false,
|
|
678
|
+
render: (principal) => (_jsx(ConfirmAction, { action: "Revoke principal access", target: `${principal.name} (${principal.id})`, scope: principal.projectId === null
|
|
679
|
+
? 'Organization'
|
|
680
|
+
: `Project: ${s.projectSlugs[principal.projectId] ?? principal.projectId}`, description: "Suspending this principal stops all of its tokens from working. Update clients before continuing.", disabled: s.busy || principal.status !== 'active', onConfirm: () => void c.suspendPrincipal(principal) }, `${principal.id}:${principal.status}`)),
|
|
681
|
+
},
|
|
682
|
+
] })), s.principalsNext === null ? null : (_jsx(Button, { type: "button", disabled: s.busy, onClick: () => void c.loadPrincipals(s.principalsNext), children: "Next service principals" }))] })] }));
|
|
683
|
+
}
|
|
684
|
+
export function bindTokensLifecycle(controller, events, onNavigate) {
|
|
685
|
+
const change = () => {
|
|
686
|
+
controller.navigate();
|
|
687
|
+
onNavigate();
|
|
688
|
+
};
|
|
689
|
+
const clear = () => controller.navigate();
|
|
690
|
+
events.addEventListener('hashchange', change);
|
|
691
|
+
events.addEventListener('pagehide', clear);
|
|
692
|
+
events.addEventListener('pageshow', clear);
|
|
693
|
+
return () => {
|
|
694
|
+
events.removeEventListener('hashchange', change);
|
|
695
|
+
events.removeEventListener('pagehide', clear);
|
|
696
|
+
events.removeEventListener('pageshow', clear);
|
|
697
|
+
controller.dispose();
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
export function bindTokensReplay(controller, services) {
|
|
701
|
+
return services.subscribeReplay?.((result) => {
|
|
702
|
+
if (!result.ok)
|
|
703
|
+
return;
|
|
704
|
+
if (/^\/_pb\/(?:me\/)?tokens\/[^/]+$/.test(result.path) ||
|
|
705
|
+
/^\/_pb\/projects\/[^/]+\/tokens\/[^/]+$/.test(result.path) ||
|
|
706
|
+
/^\/_pb\/service-principals(?:\/[^/]+\/suspend)?$/.test(result.path) ||
|
|
707
|
+
/^\/_pb\/projects\/[^/]+\/service-principals\/[^/]+\/suspend$/.test(result.path)) {
|
|
708
|
+
void controller.load();
|
|
709
|
+
void controller.loadPrincipals();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
export function TokensPage() {
|
|
714
|
+
const services = useServices();
|
|
715
|
+
const controller = useMemo(() => createTokensController(services, {
|
|
716
|
+
storage: {
|
|
717
|
+
getItem: (key) => window.sessionStorage.getItem(key),
|
|
718
|
+
setItem: (key, value) => window.sessionStorage.setItem(key, value),
|
|
719
|
+
removeItem: (key) => window.sessionStorage.removeItem(key),
|
|
720
|
+
},
|
|
721
|
+
origin: window.location.origin,
|
|
722
|
+
clipboard: (text) => navigator.clipboard.writeText(text),
|
|
723
|
+
navigate: (hash) => {
|
|
724
|
+
window.location.hash = hash;
|
|
725
|
+
},
|
|
726
|
+
newKey: () => crypto.randomUUID(),
|
|
727
|
+
now: Date.now,
|
|
728
|
+
}), [services]);
|
|
729
|
+
const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot);
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
function change() {
|
|
732
|
+
const project = new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('project');
|
|
733
|
+
if (window.location.hash.startsWith('#/tokens'))
|
|
734
|
+
void controller.start(project === null ? 'personal' : 'project', project);
|
|
735
|
+
}
|
|
736
|
+
change();
|
|
737
|
+
void controller.identity();
|
|
738
|
+
void controller.loadPrincipals();
|
|
739
|
+
return bindTokensLifecycle(controller, window, change);
|
|
740
|
+
}, [controller]);
|
|
741
|
+
useEffect(() => bindTokensReplay(controller, services), [controller, services]);
|
|
742
|
+
return _jsx(TokensView, { state: state, controller: controller });
|
|
743
|
+
}
|
|
744
|
+
//# sourceMappingURL=tokens.js.map
|