@frigear-nu/oauth-server 0.0.9 → 0.1.0
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/dist/module.json +1 -1
- package/dist/module.mjs +6 -5
- package/dist/runtime/app/components/OAuthApplicationSlideover.d.vue.ts +18 -0
- package/dist/runtime/app/components/OAuthApplicationSlideover.vue +160 -0
- package/dist/runtime/app/components/OAuthApplicationSlideover.vue.d.ts +18 -0
- package/dist/runtime/app/components/OAuthCredentialsModal.d.vue.ts +9 -0
- package/dist/runtime/app/components/OAuthCredentialsModal.vue +53 -0
- package/dist/runtime/app/components/OAuthCredentialsModal.vue.d.ts +9 -0
- package/dist/runtime/app/composables/index.d.ts +2 -1
- package/dist/runtime/app/composables/index.js +2 -1
- package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +20 -0
- package/dist/runtime/app/composables/useOAuthApplicationActions.js +123 -0
- package/dist/runtime/app/composables/useOAuthApplications.d.ts +15 -4
- package/dist/runtime/app/composables/useOAuthApplications.js +15 -6
- package/dist/runtime/app/pages/admin.vue +57 -333
- package/dist/runtime/app/pages/applications.vue +25 -37
- package/dist/runtime/app/pages/authorize.vue +59 -70
- package/dist/runtime/i18n/lang/da.json +88 -0
- package/dist/runtime/i18n/lang/en.json +88 -0
- package/dist/runtime/menus.js +3 -16
- package/dist/runtime/server/db/schema.postgresql.d.ts +642 -0
- package/dist/runtime/server/db/schema.postgresql.js +60 -0
- package/package.json +16 -16
|
@@ -1,184 +1,68 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthAdminPage" });
|
|
3
3
|
const t = useModuleT("oauthServer");
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const emptyForm = () => ({
|
|
7
|
-
name: "",
|
|
8
|
-
websiteUrl: "",
|
|
9
|
-
loginUrl: "",
|
|
10
|
-
previewUrlPattern: "",
|
|
11
|
-
description: "",
|
|
12
|
-
icon: "i-lucide-globe",
|
|
13
|
-
priority: 0,
|
|
14
|
-
allowedRoles: [],
|
|
15
|
-
tags: []
|
|
16
|
-
});
|
|
17
|
-
const formOpen = ref(false);
|
|
18
|
-
const editingId = ref(null);
|
|
19
|
-
const form = ref(emptyForm());
|
|
20
|
-
const busy = ref(false);
|
|
21
|
-
const failure = ref(null);
|
|
22
|
-
const secret = ref(null);
|
|
4
|
+
const { applications, status, error } = useOAuthApplications();
|
|
5
|
+
const actions = useOAuthApplicationActions();
|
|
23
6
|
const forbidden = computed(() => error.value?.statusCode === 401 || error.value?.statusCode === 403);
|
|
24
|
-
|
|
25
|
-
editingId.value = null;
|
|
26
|
-
form.value = emptyForm();
|
|
27
|
-
failure.value = null;
|
|
28
|
-
formOpen.value = true;
|
|
29
|
-
}
|
|
30
|
-
function openEdit(application) {
|
|
31
|
-
editingId.value = application.id;
|
|
32
|
-
form.value = {
|
|
33
|
-
name: application.name,
|
|
34
|
-
websiteUrl: application.websiteUrl,
|
|
35
|
-
loginUrl: application.loginUrl ?? "",
|
|
36
|
-
previewUrlPattern: application.previewUrlPattern ?? "",
|
|
37
|
-
description: application.description ?? "",
|
|
38
|
-
icon: application.icon ?? "",
|
|
39
|
-
priority: application.priority,
|
|
40
|
-
allowedRoles: [...application.allowedRoles],
|
|
41
|
-
tags: [...application.tags]
|
|
42
|
-
};
|
|
43
|
-
failure.value = null;
|
|
44
|
-
formOpen.value = true;
|
|
45
|
-
}
|
|
46
|
-
const message = (err) => err.statusMessage || err.data?.message || "Something went wrong.";
|
|
47
|
-
async function run(task) {
|
|
48
|
-
busy.value = true;
|
|
49
|
-
failure.value = null;
|
|
50
|
-
try {
|
|
51
|
-
await task();
|
|
52
|
-
} catch (err) {
|
|
53
|
-
failure.value = message(err);
|
|
54
|
-
} finally {
|
|
55
|
-
busy.value = false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function submit() {
|
|
59
|
-
const value = form.value;
|
|
60
|
-
const input = {
|
|
61
|
-
name: value.name,
|
|
62
|
-
websiteUrl: value.websiteUrl,
|
|
63
|
-
loginUrl: value.loginUrl || null,
|
|
64
|
-
previewUrlPattern: value.previewUrlPattern || null,
|
|
65
|
-
description: value.description || null,
|
|
66
|
-
icon: value.icon || null,
|
|
67
|
-
priority: Number(value.priority) || 0,
|
|
68
|
-
allowedRoles: value.allowedRoles,
|
|
69
|
-
tags: value.tags
|
|
70
|
-
};
|
|
71
|
-
return run(async () => {
|
|
72
|
-
if (editingId.value) {
|
|
73
|
-
await update(editingId.value, input);
|
|
74
|
-
} else {
|
|
75
|
-
const created = await create(input);
|
|
76
|
-
secret.value = { name: created.name, clientId: created.clientId, clientSecret: created.clientSecret };
|
|
77
|
-
}
|
|
78
|
-
formOpen.value = false;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
const rotate = (application) => run(async () => {
|
|
82
|
-
if (!await confirmAction({
|
|
83
|
-
title: t("admin.rotateTitle", "Replace the client secret of {name}?", { name: application.name }),
|
|
84
|
-
description: t("admin.rotateDescription", "The old secret stops working and users are signed out."),
|
|
85
|
-
confirmText: t("admin.newSecret", "New secret"),
|
|
86
|
-
destructive: true
|
|
87
|
-
})) return;
|
|
88
|
-
const clientSecret = await rotateSecret(application.id);
|
|
89
|
-
secret.value = { name: application.name, clientId: application.id, clientSecret };
|
|
90
|
-
});
|
|
91
|
-
const toggle = (application) => run(async () => {
|
|
92
|
-
await update(application.id, { isActive: !application.isActive });
|
|
93
|
-
});
|
|
94
|
-
const signOutEveryone = (application) => run(async () => {
|
|
95
|
-
if (await confirmAction({
|
|
96
|
-
title: t("admin.signOutTitle", "Sign everyone out of {name}?", { name: application.name }),
|
|
97
|
-
confirmText: t("admin.signOutEveryone", "Sign everyone out"),
|
|
98
|
-
destructive: true
|
|
99
|
-
})) await revokeGrants(application.id);
|
|
100
|
-
});
|
|
101
|
-
const destroy = (application) => run(async () => {
|
|
102
|
-
if (await confirmAction({
|
|
103
|
-
title: t("admin.deleteTitle", "Delete {name}?", { name: application.name }),
|
|
104
|
-
description: t("admin.deleteDescription", "This cannot be undone."),
|
|
105
|
-
confirmText: t("admin.delete", "Delete"),
|
|
106
|
-
destructive: true
|
|
107
|
-
})) await remove(application.id);
|
|
108
|
-
});
|
|
109
|
-
const columns = [
|
|
7
|
+
const columns = computed(() => [
|
|
110
8
|
{ accessorKey: "name", header: t("admin.application", "Application") },
|
|
111
9
|
{ id: "access", header: t("admin.access", "Access") },
|
|
112
|
-
{ id: "grants", header: t("admin.signedIn", "Signed in") },
|
|
113
|
-
{ id: "actions", header: "" }
|
|
114
|
-
];
|
|
115
|
-
const rowActions = (application) => [[
|
|
116
|
-
{ label: t("admin.edit", "Edit"), icon: "i-lucide-pencil", onSelect: () => openEdit(application) },
|
|
117
|
-
{ label: application.isActive ? t("admin.disable", "Disable") : t("admin.enable", "Enable"), icon: application.isActive ? "i-lucide-pause" : "i-lucide-play", onSelect: () => toggle(application) },
|
|
118
|
-
{ label: t("admin.newSecret", "New secret"), icon: "i-lucide-key-round", onSelect: () => rotate(application) },
|
|
119
|
-
{ label: t("admin.signOutEveryone", "Sign everyone out"), icon: "i-lucide-log-out", onSelect: () => signOutEveryone(application) }
|
|
120
|
-
], [
|
|
121
|
-
{ label: t("admin.delete", "Delete"), icon: "i-lucide-trash", color: "error", onSelect: () => destroy(application) }
|
|
122
|
-
]];
|
|
123
|
-
const copy = (value) => navigator.clipboard?.writeText(value);
|
|
10
|
+
{ id: "grants", header: t("admin.signedIn", "Signed in"), meta: numericColumn },
|
|
11
|
+
{ id: "actions", header: "", meta: { class: { th: "w-0", td: "w-0" } } }
|
|
12
|
+
]);
|
|
124
13
|
</script>
|
|
125
14
|
|
|
126
15
|
<template>
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
16
|
+
<div class="flex flex-col gap-4">
|
|
17
|
+
<PageHeading
|
|
18
|
+
:title="t('admin.title', 'OAuth applications')"
|
|
19
|
+
:description="t('admin.description', 'The applications users can sign in to with their account.')"
|
|
20
|
+
>
|
|
21
|
+
<template
|
|
22
|
+
v-if="!forbidden"
|
|
23
|
+
#actions
|
|
24
|
+
>
|
|
130
25
|
<UButton
|
|
131
|
-
v-if="!forbidden"
|
|
132
26
|
icon="i-lucide-plus"
|
|
133
|
-
:label="t('admin.new', '
|
|
134
|
-
@click="
|
|
27
|
+
:label="t('admin.new', 'Add application')"
|
|
28
|
+
@click="actions.create()"
|
|
135
29
|
/>
|
|
136
30
|
</template>
|
|
137
|
-
</
|
|
31
|
+
</PageHeading>
|
|
138
32
|
|
|
139
|
-
<
|
|
33
|
+
<UEmpty
|
|
140
34
|
v-if="forbidden"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
:
|
|
35
|
+
icon="i-lucide-lock"
|
|
36
|
+
:title="t('admin.forbiddenTitle', 'You are not allowed to manage OAuth applications')"
|
|
37
|
+
:description="t('admin.forbiddenDescription', 'Ask an administrator for access.')"
|
|
144
38
|
/>
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/>
|
|
152
|
-
|
|
153
|
-
<AdminTable
|
|
39
|
+
<TableCard
|
|
40
|
+
v-else
|
|
41
|
+
:loading="status === 'pending' && !applications.length"
|
|
42
|
+
:refreshing="status === 'pending' && !!applications.length"
|
|
43
|
+
>
|
|
44
|
+
<UTable
|
|
154
45
|
:data="applications"
|
|
155
46
|
:columns="columns"
|
|
156
|
-
:
|
|
47
|
+
:ui="tableUi()"
|
|
48
|
+
@select="(_event, row2) => actions.edit(row2.original)"
|
|
157
49
|
>
|
|
158
50
|
<template #name-cell="{ row }">
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<p class="truncate text-sm text-muted">
|
|
175
|
-
{{ row.original.websiteUrl }}
|
|
176
|
-
</p>
|
|
177
|
-
<p class="truncate text-xs text-muted">
|
|
178
|
-
{{ t("admin.callback", "Callback") }}: {{ row.original.callbackUrl }}
|
|
179
|
-
</p>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
51
|
+
<TableIdentity
|
|
52
|
+
:title="row.original.name"
|
|
53
|
+
:description="row.original.websiteUrl"
|
|
54
|
+
:avatar="{ icon: row.original.icon || 'i-lucide-globe' }"
|
|
55
|
+
>
|
|
56
|
+
<template #badges>
|
|
57
|
+
<UBadge
|
|
58
|
+
v-if="!row.original.isActive"
|
|
59
|
+
color="neutral"
|
|
60
|
+
variant="subtle"
|
|
61
|
+
size="sm"
|
|
62
|
+
:label="t('admin.disabled', 'Disabled')"
|
|
63
|
+
/>
|
|
64
|
+
</template>
|
|
65
|
+
</TableIdentity>
|
|
182
66
|
</template>
|
|
183
67
|
<template #access-cell="{ row }">
|
|
184
68
|
<div class="flex flex-wrap gap-1">
|
|
@@ -200,181 +84,21 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
200
84
|
{{ row.original.activeGrants ?? 0 }}
|
|
201
85
|
</template>
|
|
202
86
|
<template #actions-cell="{ row }">
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
color="neutral"
|
|
208
|
-
variant="ghost"
|
|
209
|
-
size="sm"
|
|
210
|
-
:disabled="busy"
|
|
211
|
-
:aria-label="t('admin.actions', 'Actions')"
|
|
212
|
-
/>
|
|
213
|
-
</UDropdownMenu>
|
|
214
|
-
</div>
|
|
87
|
+
<RowMenu
|
|
88
|
+
:items="actions.menu(row.original)"
|
|
89
|
+
:subject="row.original.name"
|
|
90
|
+
/>
|
|
215
91
|
</template>
|
|
216
92
|
<template #empty>
|
|
217
|
-
<
|
|
93
|
+
<UEmpty
|
|
94
|
+
variant="naked"
|
|
218
95
|
icon="i-lucide-app-window"
|
|
219
|
-
:title="t('admin.
|
|
96
|
+
:title="t('admin.emptyTitle', 'No applications yet')"
|
|
97
|
+
:description="t('admin.emptyDescription', 'Add an application to let users sign in to it with their account.')"
|
|
98
|
+
:actions="[{ label: t('admin.new', 'Add application'), icon: 'i-lucide-plus', onClick: () => actions.create() }]"
|
|
220
99
|
/>
|
|
221
100
|
</template>
|
|
222
|
-
</
|
|
223
|
-
</
|
|
224
|
-
|
|
225
|
-
<UModal
|
|
226
|
-
v-model:open="formOpen"
|
|
227
|
-
:title="editingId ? 'Edit application' : 'New application'"
|
|
228
|
-
>
|
|
229
|
-
<template #body>
|
|
230
|
-
<form
|
|
231
|
-
id="oauth-application-form"
|
|
232
|
-
class="grid gap-4"
|
|
233
|
-
@submit.prevent="submit"
|
|
234
|
-
>
|
|
235
|
-
<UFormField
|
|
236
|
-
label="Name"
|
|
237
|
-
required
|
|
238
|
-
>
|
|
239
|
-
<UInput
|
|
240
|
-
v-model="form.name"
|
|
241
|
-
class="w-full"
|
|
242
|
-
required
|
|
243
|
-
/>
|
|
244
|
-
</UFormField>
|
|
245
|
-
<UFormField
|
|
246
|
-
label="Website URL"
|
|
247
|
-
description="The origin of the application, without a path. The callback is the website URL plus the configured callback path."
|
|
248
|
-
required
|
|
249
|
-
>
|
|
250
|
-
<UInput
|
|
251
|
-
v-model="form.websiteUrl"
|
|
252
|
-
class="w-full"
|
|
253
|
-
placeholder="https://app.example.com"
|
|
254
|
-
required
|
|
255
|
-
/>
|
|
256
|
-
</UFormField>
|
|
257
|
-
<UFormField
|
|
258
|
-
label="Login URL"
|
|
259
|
-
description="Where the Open button goes. Defaults to the website URL."
|
|
260
|
-
>
|
|
261
|
-
<UInput
|
|
262
|
-
v-model="form.loginUrl"
|
|
263
|
-
class="w-full"
|
|
264
|
-
/>
|
|
265
|
-
</UFormField>
|
|
266
|
-
<UFormField
|
|
267
|
-
label="Preview URL pattern"
|
|
268
|
-
description="Optional, e.g. https://*.vercel.app, for preview deployments."
|
|
269
|
-
>
|
|
270
|
-
<UInput
|
|
271
|
-
v-model="form.previewUrlPattern"
|
|
272
|
-
class="w-full"
|
|
273
|
-
/>
|
|
274
|
-
</UFormField>
|
|
275
|
-
<UFormField
|
|
276
|
-
label="Allowed roles"
|
|
277
|
-
description="Leave empty to allow every signed-in user."
|
|
278
|
-
>
|
|
279
|
-
<UInputTags
|
|
280
|
-
v-model="form.allowedRoles"
|
|
281
|
-
class="w-full"
|
|
282
|
-
/>
|
|
283
|
-
</UFormField>
|
|
284
|
-
<UFormField label="Tags">
|
|
285
|
-
<UInputTags
|
|
286
|
-
v-model="form.tags"
|
|
287
|
-
class="w-full"
|
|
288
|
-
/>
|
|
289
|
-
</UFormField>
|
|
290
|
-
<UFormField label="Description">
|
|
291
|
-
<UTextarea
|
|
292
|
-
v-model="form.description"
|
|
293
|
-
class="w-full"
|
|
294
|
-
/>
|
|
295
|
-
</UFormField>
|
|
296
|
-
<div class="grid grid-cols-2 gap-4">
|
|
297
|
-
<UFormField label="Icon">
|
|
298
|
-
<UInput
|
|
299
|
-
v-model="form.icon"
|
|
300
|
-
class="w-full"
|
|
301
|
-
/>
|
|
302
|
-
</UFormField>
|
|
303
|
-
<UFormField label="Priority">
|
|
304
|
-
<UInput
|
|
305
|
-
v-model.number="form.priority"
|
|
306
|
-
type="number"
|
|
307
|
-
class="w-full"
|
|
308
|
-
/>
|
|
309
|
-
</UFormField>
|
|
310
|
-
</div>
|
|
311
|
-
<UAlert
|
|
312
|
-
v-if="failure"
|
|
313
|
-
color="error"
|
|
314
|
-
variant="subtle"
|
|
315
|
-
:title="failure"
|
|
316
|
-
/>
|
|
317
|
-
</form>
|
|
318
|
-
</template>
|
|
319
|
-
<template #footer>
|
|
320
|
-
<div class="flex w-full justify-end gap-2">
|
|
321
|
-
<UButton
|
|
322
|
-
color="neutral"
|
|
323
|
-
variant="outline"
|
|
324
|
-
@click="formOpen = false"
|
|
325
|
-
>
|
|
326
|
-
Cancel
|
|
327
|
-
</UButton>
|
|
328
|
-
<UButton
|
|
329
|
-
type="submit"
|
|
330
|
-
form="oauth-application-form"
|
|
331
|
-
:loading="busy"
|
|
332
|
-
>
|
|
333
|
-
{{ editingId ? "Save" : "Create" }}
|
|
334
|
-
</UButton>
|
|
335
|
-
</div>
|
|
336
|
-
</template>
|
|
337
|
-
</UModal>
|
|
338
|
-
|
|
339
|
-
<UModal
|
|
340
|
-
:open="!!secret"
|
|
341
|
-
:title="secret ? `Credentials for ${secret.name}` : ''"
|
|
342
|
-
description="Copy the client secret now. It is only shown once."
|
|
343
|
-
@update:open="secret = null"
|
|
344
|
-
>
|
|
345
|
-
<template #body>
|
|
346
|
-
<div
|
|
347
|
-
v-if="secret"
|
|
348
|
-
class="grid gap-3 text-sm"
|
|
349
|
-
>
|
|
350
|
-
<div>
|
|
351
|
-
<div class="text-muted">
|
|
352
|
-
Client ID
|
|
353
|
-
</div>
|
|
354
|
-
<code class="break-all">{{ secret.clientId }}</code>
|
|
355
|
-
<UButton
|
|
356
|
-
size="xs"
|
|
357
|
-
variant="link"
|
|
358
|
-
@click="copy(secret.clientId)"
|
|
359
|
-
>
|
|
360
|
-
Copy
|
|
361
|
-
</UButton>
|
|
362
|
-
</div>
|
|
363
|
-
<div>
|
|
364
|
-
<div class="text-muted">
|
|
365
|
-
Client secret
|
|
366
|
-
</div>
|
|
367
|
-
<code class="break-all">{{ secret.clientSecret }}</code>
|
|
368
|
-
<UButton
|
|
369
|
-
size="xs"
|
|
370
|
-
variant="link"
|
|
371
|
-
@click="copy(secret.clientSecret)"
|
|
372
|
-
>
|
|
373
|
-
Copy
|
|
374
|
-
</UButton>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
</template>
|
|
378
|
-
</UModal>
|
|
379
|
-
</AdminPage>
|
|
101
|
+
</UTable>
|
|
102
|
+
</TableCard>
|
|
103
|
+
</div>
|
|
380
104
|
</template>
|
|
@@ -1,50 +1,38 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthApplicationsPage" });
|
|
3
|
+
const t = useModuleT("oauthServer");
|
|
3
4
|
const { applications, status } = useMyApplications();
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
7
|
<template>
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
Applications
|
|
10
|
-
</h1>
|
|
11
|
-
<p
|
|
8
|
+
<div class="flex flex-col gap-4">
|
|
9
|
+
<UEmpty
|
|
12
10
|
v-if="status !== 'pending' && !applications.length"
|
|
13
|
-
|
|
11
|
+
icon="i-lucide-layout-grid"
|
|
12
|
+
:title="t('applications.emptyTitle', 'No applications yet')"
|
|
13
|
+
:description="t('applications.emptyDescription', 'There are no applications you can sign in to yet.')"
|
|
14
|
+
/>
|
|
15
|
+
<div
|
|
16
|
+
v-else
|
|
17
|
+
class="grid gap-4 md:grid-cols-2"
|
|
14
18
|
>
|
|
15
|
-
|
|
16
|
-
</p>
|
|
17
|
-
<div class="grid gap-4 md:grid-cols-2">
|
|
18
|
-
<UCard
|
|
19
|
+
<UPageCard
|
|
19
20
|
v-for="application in applications"
|
|
20
21
|
:key="application.id"
|
|
22
|
+
variant="outline"
|
|
23
|
+
:title="application.name"
|
|
24
|
+
:description="application.description ?? application.websiteUrl"
|
|
25
|
+
:icon="application.icon || 'i-lucide-globe'"
|
|
21
26
|
>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<p
|
|
32
|
-
v-if="application.description"
|
|
33
|
-
class="text-sm text-muted"
|
|
34
|
-
>
|
|
35
|
-
{{ application.description }}
|
|
36
|
-
</p>
|
|
37
|
-
</div>
|
|
38
|
-
<UButton
|
|
39
|
-
:to="application.loginUrl"
|
|
40
|
-
external
|
|
41
|
-
trailing-icon="i-lucide-arrow-up-right"
|
|
42
|
-
size="sm"
|
|
43
|
-
>
|
|
44
|
-
Open
|
|
45
|
-
</UButton>
|
|
46
|
-
</div>
|
|
47
|
-
</UCard>
|
|
27
|
+
<UButton
|
|
28
|
+
:to="application.loginUrl"
|
|
29
|
+
external
|
|
30
|
+
trailing-icon="i-lucide-arrow-up-right"
|
|
31
|
+
size="sm"
|
|
32
|
+
:label="t('applications.open', 'Open')"
|
|
33
|
+
:aria-label="t('applications.openApplication', 'Open {name}', { name: application.name })"
|
|
34
|
+
/>
|
|
35
|
+
</UPageCard>
|
|
48
36
|
</div>
|
|
49
|
-
</
|
|
37
|
+
</div>
|
|
50
38
|
</template>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthAuthorizePage" });
|
|
3
|
+
const t = useModuleT("oauthServer");
|
|
3
4
|
const route = useRoute();
|
|
4
5
|
const signInPath = useAuthPages().login;
|
|
6
|
+
const showError = useErrorToast();
|
|
5
7
|
const { data: consent, error } = await useFetch("/api/oauth-server/consent", {
|
|
6
8
|
query: route.query,
|
|
7
9
|
key: `oauth-server:consent:${route.fullPath}`
|
|
@@ -9,16 +11,17 @@ const { data: consent, error } = await useFetch("/api/oauth-server/consent", {
|
|
|
9
11
|
if (error.value?.statusCode === 401) {
|
|
10
12
|
await navigateTo(`${signInPath}?redirect=${encodeURIComponent(route.fullPath)}`);
|
|
11
13
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const scopeLabel = (scope) => {
|
|
15
|
+
const known = {
|
|
16
|
+
openid: t("authorize.scope.openid", "Verify your identity"),
|
|
17
|
+
profile: t("authorize.scope.profile", "Your name and profile picture"),
|
|
18
|
+
email: t("authorize.scope.email", "Your email address")
|
|
19
|
+
};
|
|
20
|
+
return known[scope] ?? scope;
|
|
16
21
|
};
|
|
17
22
|
const pending = ref(null);
|
|
18
|
-
const failure = ref(null);
|
|
19
23
|
async function decide(approved) {
|
|
20
24
|
pending.value = approved ? "approve" : "deny";
|
|
21
|
-
failure.value = null;
|
|
22
25
|
try {
|
|
23
26
|
const { redirectUrl } = await $fetch("/oauth/authorize", {
|
|
24
27
|
method: "POST",
|
|
@@ -26,83 +29,69 @@ async function decide(approved) {
|
|
|
26
29
|
});
|
|
27
30
|
await navigateTo(redirectUrl, { external: true });
|
|
28
31
|
} catch (err) {
|
|
29
|
-
|
|
32
|
+
showError(err, t("authorize.failed", "Could not complete the sign-in. Please try again."));
|
|
30
33
|
pending.value = null;
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
</script>
|
|
34
37
|
|
|
35
38
|
<template>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<UIcon
|
|
44
|
-
:name="consent.application.icon || 'i-lucide-globe'"
|
|
45
|
-
class="size-8"
|
|
46
|
-
/>
|
|
47
|
-
<div>
|
|
48
|
-
<h1 class="text-lg font-semibold">
|
|
49
|
-
{{ consent.application.name }}
|
|
50
|
-
</h1>
|
|
51
|
-
<p class="text-sm text-muted">
|
|
52
|
-
{{ consent.application.websiteUrl }}
|
|
53
|
-
</p>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</template>
|
|
39
|
+
<AuthCard
|
|
40
|
+
v-if="consent"
|
|
41
|
+
:title="t('authorize.title', 'Sign in to {name}', { name: consent.application.name })"
|
|
42
|
+
>
|
|
43
|
+
<template #description>
|
|
44
|
+
{{ t("authorize.signedInAs", "Signed in as {name} ({email}).", { name: consent.user.name, email: consent.user.email }) }}
|
|
45
|
+
</template>
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
<div class="flex items-center gap-3">
|
|
48
|
+
<UIcon
|
|
49
|
+
:name="consent.application.icon || 'i-lucide-globe'"
|
|
50
|
+
class="size-8 shrink-0"
|
|
51
|
+
/>
|
|
52
|
+
<div class="min-w-0">
|
|
53
|
+
<p class="text-highlighted font-medium">
|
|
54
|
+
{{ consent.application.name }}
|
|
55
|
+
</p>
|
|
56
|
+
<p class="truncate text-sm text-muted">
|
|
57
|
+
{{ consent.application.websiteUrl }}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="flex flex-col gap-2 text-sm">
|
|
63
|
+
<p>{{ t("authorize.wants", "{name} wants to:", { name: consent.application.name }) }}</p>
|
|
64
|
+
<ul class="list-inside list-disc space-y-1">
|
|
63
65
|
<li
|
|
64
66
|
v-for="scope in consent.scope"
|
|
65
67
|
:key="scope"
|
|
66
68
|
>
|
|
67
|
-
{{
|
|
69
|
+
{{ scopeLabel(scope) }}
|
|
68
70
|
</li>
|
|
69
71
|
</ul>
|
|
70
|
-
|
|
71
|
-
v-if="failure"
|
|
72
|
-
color="error"
|
|
73
|
-
variant="subtle"
|
|
74
|
-
:title="failure"
|
|
75
|
-
/>
|
|
72
|
+
</div>
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</UButton>
|
|
95
|
-
</div>
|
|
96
|
-
</template>
|
|
97
|
-
</UCard>
|
|
74
|
+
<div class="flex justify-end gap-2">
|
|
75
|
+
<UButton
|
|
76
|
+
color="neutral"
|
|
77
|
+
variant="outline"
|
|
78
|
+
:label="t('authorize.deny', 'Deny')"
|
|
79
|
+
:loading="pending === 'deny'"
|
|
80
|
+
:disabled="!!pending"
|
|
81
|
+
@click="decide(false)"
|
|
82
|
+
/>
|
|
83
|
+
<UButton
|
|
84
|
+
:label="t('authorize.allow', 'Allow')"
|
|
85
|
+
:loading="pending === 'approve'"
|
|
86
|
+
:disabled="!!pending"
|
|
87
|
+
@click="decide(true)"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</AuthCard>
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
title="This sign-in request cannot be completed"
|
|
105
|
-
:description="error?.statusMessage || 'The request is invalid.'"
|
|
106
|
-
/>
|
|
107
|
-
</UContainer>
|
|
92
|
+
<AuthCard
|
|
93
|
+
v-else
|
|
94
|
+
:title="t('authorize.invalidTitle', 'This sign-in request cannot be completed')"
|
|
95
|
+
:description="error?.statusMessage || t('authorize.invalidDescription', 'The request is invalid.')"
|
|
96
|
+
/>
|
|
108
97
|
</template>
|