@frigear-nu/oauth-server 0.0.0-stage → 0.0.3
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/README.md +49 -2
- package/dist/module.d.mts +206 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +107 -0
- package/dist/runtime/app/composables/index.d.ts +1 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useOAuthApplications.d.ts +19 -0
- package/dist/runtime/app/composables/useOAuthApplications.js +41 -0
- package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin.vue +384 -0
- package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
- package/dist/runtime/app/pages/applications.d.vue.ts +3 -0
- package/dist/runtime/app/pages/applications.vue +50 -0
- package/dist/runtime/app/pages/applications.vue.d.ts +3 -0
- package/dist/runtime/app/pages/authorize.d.vue.ts +3 -0
- package/dist/runtime/app/pages/authorize.vue +108 -0
- package/dist/runtime/app/pages/authorize.vue.d.ts +3 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +27 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +682 -0
- package/dist/runtime/server/db/schema.sqlite.js +60 -0
- package/dist/runtime/server/routes/api/admin/application-revoke.post.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/application-revoke.post.js +7 -0
- package/dist/runtime/server/routes/api/admin/application-secret.post.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/application-secret.post.js +5 -0
- package/dist/runtime/server/routes/api/admin/application.delete.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/application.delete.js +6 -0
- package/dist/runtime/server/routes/api/admin/application.get.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/application.get.js +6 -0
- package/dist/runtime/server/routes/api/admin/application.patch.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/application.patch.js +9 -0
- package/dist/runtime/server/routes/api/admin/applications.get.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/applications.get.js +11 -0
- package/dist/runtime/server/routes/api/admin/applications.post.d.ts +2 -0
- package/dist/runtime/server/routes/api/admin/applications.post.js +8 -0
- package/dist/runtime/server/routes/api/consent.get.d.ts +2 -0
- package/dist/runtime/server/routes/api/consent.get.js +19 -0
- package/dist/runtime/server/routes/api/me-applications.get.d.ts +2 -0
- package/dist/runtime/server/routes/api/me-applications.get.js +4 -0
- package/dist/runtime/server/routes/oidc/authorize.get.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/authorize.get.js +15 -0
- package/dist/runtime/server/routes/oidc/authorize.post.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/authorize.post.js +27 -0
- package/dist/runtime/server/routes/oidc/jwks.get.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/jwks.get.js +4 -0
- package/dist/runtime/server/routes/oidc/openid-configuration.get.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/openid-configuration.get.js +21 -0
- package/dist/runtime/server/routes/oidc/revoke.post.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/revoke.post.js +13 -0
- package/dist/runtime/server/routes/oidc/token.post.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/token.post.js +56 -0
- package/dist/runtime/server/routes/oidc/userinfo.get.d.ts +2 -0
- package/dist/runtime/server/routes/oidc/userinfo.get.js +18 -0
- package/dist/runtime/server/utils/oauth-access.d.ts +34 -0
- package/dist/runtime/server/utils/oauth-access.js +49 -0
- package/dist/runtime/server/utils/oauth-applications.d.ts +65 -0
- package/dist/runtime/server/utils/oauth-applications.js +126 -0
- package/dist/runtime/server/utils/oauth-authorize.d.ts +29 -0
- package/dist/runtime/server/utils/oauth-authorize.js +33 -0
- package/dist/runtime/server/utils/oauth-config.d.ts +16 -0
- package/dist/runtime/server/utils/oauth-config.js +20 -0
- package/dist/runtime/server/utils/oauth-crypto.d.ts +18 -0
- package/dist/runtime/server/utils/oauth-crypto.js +48 -0
- package/dist/runtime/server/utils/oauth-grants.d.ts +60 -0
- package/dist/runtime/server/utils/oauth-grants.js +70 -0
- package/dist/runtime/server/utils/oauth-keys.d.ts +41 -0
- package/dist/runtime/server/utils/oauth-keys.js +55 -0
- package/dist/runtime/server/utils/oauth-tokens.d.ts +25 -0
- package/dist/runtime/server/utils/oauth-tokens.js +34 -0
- package/dist/runtime/utils/index.d.ts +24 -0
- package/dist/runtime/utils/index.js +51 -0
- package/dist/types.d.mts +3 -0
- package/package.json +92 -3
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineOptions({ name: "OAuthAdminPage" });
|
|
3
|
+
const { applications, status, error, create, update, remove, rotateSecret, revokeGrants } = useOAuthApplications();
|
|
4
|
+
const emptyForm = () => ({
|
|
5
|
+
name: "",
|
|
6
|
+
websiteUrl: "",
|
|
7
|
+
loginUrl: "",
|
|
8
|
+
previewUrlPattern: "",
|
|
9
|
+
description: "",
|
|
10
|
+
icon: "i-lucide-globe",
|
|
11
|
+
priority: 0,
|
|
12
|
+
allowedRoles: [],
|
|
13
|
+
tags: []
|
|
14
|
+
});
|
|
15
|
+
const formOpen = ref(false);
|
|
16
|
+
const editingId = ref(null);
|
|
17
|
+
const form = ref(emptyForm());
|
|
18
|
+
const busy = ref(false);
|
|
19
|
+
const failure = ref(null);
|
|
20
|
+
const secret = ref(null);
|
|
21
|
+
const forbidden = computed(() => error.value?.statusCode === 401 || error.value?.statusCode === 403);
|
|
22
|
+
function openCreate() {
|
|
23
|
+
editingId.value = null;
|
|
24
|
+
form.value = emptyForm();
|
|
25
|
+
failure.value = null;
|
|
26
|
+
formOpen.value = true;
|
|
27
|
+
}
|
|
28
|
+
function openEdit(application) {
|
|
29
|
+
editingId.value = application.id;
|
|
30
|
+
form.value = {
|
|
31
|
+
name: application.name,
|
|
32
|
+
websiteUrl: application.websiteUrl,
|
|
33
|
+
loginUrl: application.loginUrl ?? "",
|
|
34
|
+
previewUrlPattern: application.previewUrlPattern ?? "",
|
|
35
|
+
description: application.description ?? "",
|
|
36
|
+
icon: application.icon ?? "",
|
|
37
|
+
priority: application.priority,
|
|
38
|
+
allowedRoles: [...application.allowedRoles],
|
|
39
|
+
tags: [...application.tags]
|
|
40
|
+
};
|
|
41
|
+
failure.value = null;
|
|
42
|
+
formOpen.value = true;
|
|
43
|
+
}
|
|
44
|
+
const message = (err) => err.statusMessage || err.data?.message || "Something went wrong.";
|
|
45
|
+
async function run(task) {
|
|
46
|
+
busy.value = true;
|
|
47
|
+
failure.value = null;
|
|
48
|
+
try {
|
|
49
|
+
await task();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
failure.value = message(err);
|
|
52
|
+
} finally {
|
|
53
|
+
busy.value = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function submit() {
|
|
57
|
+
const value = form.value;
|
|
58
|
+
const input = {
|
|
59
|
+
name: value.name,
|
|
60
|
+
websiteUrl: value.websiteUrl,
|
|
61
|
+
loginUrl: value.loginUrl || null,
|
|
62
|
+
previewUrlPattern: value.previewUrlPattern || null,
|
|
63
|
+
description: value.description || null,
|
|
64
|
+
icon: value.icon || null,
|
|
65
|
+
priority: Number(value.priority) || 0,
|
|
66
|
+
allowedRoles: value.allowedRoles,
|
|
67
|
+
tags: value.tags
|
|
68
|
+
};
|
|
69
|
+
return run(async () => {
|
|
70
|
+
if (editingId.value) {
|
|
71
|
+
await update(editingId.value, input);
|
|
72
|
+
} else {
|
|
73
|
+
const created = await create(input);
|
|
74
|
+
secret.value = { name: created.name, clientId: created.clientId, clientSecret: created.clientSecret };
|
|
75
|
+
}
|
|
76
|
+
formOpen.value = false;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const rotate = (application) => run(async () => {
|
|
80
|
+
if (!confirm(`Replace the client secret of ${application.name}? The old secret stops working and users are signed out.`)) return;
|
|
81
|
+
const clientSecret = await rotateSecret(application.id);
|
|
82
|
+
secret.value = { name: application.name, clientId: application.id, clientSecret };
|
|
83
|
+
});
|
|
84
|
+
const toggle = (application) => run(async () => {
|
|
85
|
+
await update(application.id, { isActive: !application.isActive });
|
|
86
|
+
});
|
|
87
|
+
const signOutEveryone = (application) => run(async () => {
|
|
88
|
+
if (confirm(`Sign everyone out of ${application.name}?`)) await revokeGrants(application.id);
|
|
89
|
+
});
|
|
90
|
+
const destroy = (application) => run(async () => {
|
|
91
|
+
if (confirm(`Delete ${application.name}? This cannot be undone.`)) await remove(application.id);
|
|
92
|
+
});
|
|
93
|
+
const copy = (value) => navigator.clipboard?.writeText(value);
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<UContainer class="py-10">
|
|
98
|
+
<div class="mb-6 flex items-center justify-between gap-4">
|
|
99
|
+
<h1 class="text-2xl font-semibold">
|
|
100
|
+
OAuth applications
|
|
101
|
+
</h1>
|
|
102
|
+
<UButton
|
|
103
|
+
v-if="!forbidden"
|
|
104
|
+
icon="i-lucide-plus"
|
|
105
|
+
@click="openCreate"
|
|
106
|
+
>
|
|
107
|
+
New application
|
|
108
|
+
</UButton>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<UAlert
|
|
112
|
+
v-if="forbidden"
|
|
113
|
+
color="error"
|
|
114
|
+
variant="subtle"
|
|
115
|
+
title="You are not allowed to manage OAuth applications."
|
|
116
|
+
/>
|
|
117
|
+
<UAlert
|
|
118
|
+
v-else-if="failure && !formOpen"
|
|
119
|
+
class="mb-4"
|
|
120
|
+
color="error"
|
|
121
|
+
variant="subtle"
|
|
122
|
+
:title="failure"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<p
|
|
126
|
+
v-if="!forbidden && status !== 'pending' && !applications.length"
|
|
127
|
+
class="text-muted"
|
|
128
|
+
>
|
|
129
|
+
No applications yet.
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
<div class="grid gap-4">
|
|
133
|
+
<UCard
|
|
134
|
+
v-for="application in applications"
|
|
135
|
+
:key="application.id"
|
|
136
|
+
>
|
|
137
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
138
|
+
<div class="flex min-w-0 items-start gap-3">
|
|
139
|
+
<UIcon
|
|
140
|
+
:name="application.icon || 'i-lucide-globe'"
|
|
141
|
+
class="mt-1 size-6 shrink-0"
|
|
142
|
+
/>
|
|
143
|
+
<div class="min-w-0">
|
|
144
|
+
<h2 class="flex items-center gap-2 font-medium">
|
|
145
|
+
{{ application.name }}
|
|
146
|
+
<UBadge
|
|
147
|
+
v-if="!application.isActive"
|
|
148
|
+
color="neutral"
|
|
149
|
+
variant="subtle"
|
|
150
|
+
>
|
|
151
|
+
Disabled
|
|
152
|
+
</UBadge>
|
|
153
|
+
</h2>
|
|
154
|
+
<p class="truncate text-sm text-muted">
|
|
155
|
+
{{ application.websiteUrl }} · {{ application.activeGrants ?? 0 }} signed in
|
|
156
|
+
</p>
|
|
157
|
+
<p class="truncate text-xs text-muted">
|
|
158
|
+
Callback: {{ application.callbackUrl }}
|
|
159
|
+
</p>
|
|
160
|
+
<div class="mt-2 flex flex-wrap gap-1">
|
|
161
|
+
<UBadge
|
|
162
|
+
v-for="role in application.allowedRoles"
|
|
163
|
+
:key="role"
|
|
164
|
+
variant="subtle"
|
|
165
|
+
>
|
|
166
|
+
{{ role }}
|
|
167
|
+
</UBadge>
|
|
168
|
+
<UBadge
|
|
169
|
+
v-if="!application.allowedRoles.length"
|
|
170
|
+
color="neutral"
|
|
171
|
+
variant="subtle"
|
|
172
|
+
>
|
|
173
|
+
Every signed-in user
|
|
174
|
+
</UBadge>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="flex flex-wrap gap-2">
|
|
179
|
+
<UButton
|
|
180
|
+
size="sm"
|
|
181
|
+
color="neutral"
|
|
182
|
+
variant="outline"
|
|
183
|
+
:disabled="busy"
|
|
184
|
+
@click="openEdit(application)"
|
|
185
|
+
>
|
|
186
|
+
Edit
|
|
187
|
+
</UButton>
|
|
188
|
+
<UButton
|
|
189
|
+
size="sm"
|
|
190
|
+
color="neutral"
|
|
191
|
+
variant="outline"
|
|
192
|
+
:disabled="busy"
|
|
193
|
+
@click="toggle(application)"
|
|
194
|
+
>
|
|
195
|
+
{{ application.isActive ? "Disable" : "Enable" }}
|
|
196
|
+
</UButton>
|
|
197
|
+
<UButton
|
|
198
|
+
size="sm"
|
|
199
|
+
color="neutral"
|
|
200
|
+
variant="outline"
|
|
201
|
+
:disabled="busy"
|
|
202
|
+
@click="rotate(application)"
|
|
203
|
+
>
|
|
204
|
+
New secret
|
|
205
|
+
</UButton>
|
|
206
|
+
<UButton
|
|
207
|
+
size="sm"
|
|
208
|
+
color="neutral"
|
|
209
|
+
variant="outline"
|
|
210
|
+
:disabled="busy"
|
|
211
|
+
@click="signOutEveryone(application)"
|
|
212
|
+
>
|
|
213
|
+
Sign everyone out
|
|
214
|
+
</UButton>
|
|
215
|
+
<UButton
|
|
216
|
+
size="sm"
|
|
217
|
+
color="error"
|
|
218
|
+
variant="outline"
|
|
219
|
+
:disabled="busy"
|
|
220
|
+
@click="destroy(application)"
|
|
221
|
+
>
|
|
222
|
+
Delete
|
|
223
|
+
</UButton>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</UCard>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<UModal
|
|
230
|
+
v-model:open="formOpen"
|
|
231
|
+
:title="editingId ? 'Edit application' : 'New application'"
|
|
232
|
+
>
|
|
233
|
+
<template #body>
|
|
234
|
+
<form
|
|
235
|
+
id="oauth-application-form"
|
|
236
|
+
class="grid gap-4"
|
|
237
|
+
@submit.prevent="submit"
|
|
238
|
+
>
|
|
239
|
+
<UFormField
|
|
240
|
+
label="Name"
|
|
241
|
+
required
|
|
242
|
+
>
|
|
243
|
+
<UInput
|
|
244
|
+
v-model="form.name"
|
|
245
|
+
class="w-full"
|
|
246
|
+
required
|
|
247
|
+
/>
|
|
248
|
+
</UFormField>
|
|
249
|
+
<UFormField
|
|
250
|
+
label="Website URL"
|
|
251
|
+
help="The origin of the application, without a path. The callback is the website URL plus the configured callback path."
|
|
252
|
+
required
|
|
253
|
+
>
|
|
254
|
+
<UInput
|
|
255
|
+
v-model="form.websiteUrl"
|
|
256
|
+
class="w-full"
|
|
257
|
+
placeholder="https://app.example.com"
|
|
258
|
+
required
|
|
259
|
+
/>
|
|
260
|
+
</UFormField>
|
|
261
|
+
<UFormField
|
|
262
|
+
label="Login URL"
|
|
263
|
+
help="Where the Open button goes. Defaults to the website URL."
|
|
264
|
+
>
|
|
265
|
+
<UInput
|
|
266
|
+
v-model="form.loginUrl"
|
|
267
|
+
class="w-full"
|
|
268
|
+
/>
|
|
269
|
+
</UFormField>
|
|
270
|
+
<UFormField
|
|
271
|
+
label="Preview URL pattern"
|
|
272
|
+
help="Optional, e.g. https://*.vercel.app, for preview deployments."
|
|
273
|
+
>
|
|
274
|
+
<UInput
|
|
275
|
+
v-model="form.previewUrlPattern"
|
|
276
|
+
class="w-full"
|
|
277
|
+
/>
|
|
278
|
+
</UFormField>
|
|
279
|
+
<UFormField
|
|
280
|
+
label="Allowed roles"
|
|
281
|
+
help="Leave empty to allow every signed-in user."
|
|
282
|
+
>
|
|
283
|
+
<UInputTags
|
|
284
|
+
v-model="form.allowedRoles"
|
|
285
|
+
class="w-full"
|
|
286
|
+
/>
|
|
287
|
+
</UFormField>
|
|
288
|
+
<UFormField label="Tags">
|
|
289
|
+
<UInputTags
|
|
290
|
+
v-model="form.tags"
|
|
291
|
+
class="w-full"
|
|
292
|
+
/>
|
|
293
|
+
</UFormField>
|
|
294
|
+
<UFormField label="Description">
|
|
295
|
+
<UTextarea
|
|
296
|
+
v-model="form.description"
|
|
297
|
+
class="w-full"
|
|
298
|
+
/>
|
|
299
|
+
</UFormField>
|
|
300
|
+
<div class="grid grid-cols-2 gap-4">
|
|
301
|
+
<UFormField label="Icon">
|
|
302
|
+
<UInput
|
|
303
|
+
v-model="form.icon"
|
|
304
|
+
class="w-full"
|
|
305
|
+
/>
|
|
306
|
+
</UFormField>
|
|
307
|
+
<UFormField label="Priority">
|
|
308
|
+
<UInput
|
|
309
|
+
v-model.number="form.priority"
|
|
310
|
+
type="number"
|
|
311
|
+
class="w-full"
|
|
312
|
+
/>
|
|
313
|
+
</UFormField>
|
|
314
|
+
</div>
|
|
315
|
+
<UAlert
|
|
316
|
+
v-if="failure"
|
|
317
|
+
color="error"
|
|
318
|
+
variant="subtle"
|
|
319
|
+
:title="failure"
|
|
320
|
+
/>
|
|
321
|
+
</form>
|
|
322
|
+
</template>
|
|
323
|
+
<template #footer>
|
|
324
|
+
<div class="flex w-full justify-end gap-2">
|
|
325
|
+
<UButton
|
|
326
|
+
color="neutral"
|
|
327
|
+
variant="outline"
|
|
328
|
+
@click="formOpen = false"
|
|
329
|
+
>
|
|
330
|
+
Cancel
|
|
331
|
+
</UButton>
|
|
332
|
+
<UButton
|
|
333
|
+
type="submit"
|
|
334
|
+
form="oauth-application-form"
|
|
335
|
+
:loading="busy"
|
|
336
|
+
>
|
|
337
|
+
{{ editingId ? "Save" : "Create" }}
|
|
338
|
+
</UButton>
|
|
339
|
+
</div>
|
|
340
|
+
</template>
|
|
341
|
+
</UModal>
|
|
342
|
+
|
|
343
|
+
<UModal
|
|
344
|
+
:open="!!secret"
|
|
345
|
+
:title="secret ? `Credentials for ${secret.name}` : ''"
|
|
346
|
+
description="Copy the client secret now. It is only shown once."
|
|
347
|
+
@update:open="secret = null"
|
|
348
|
+
>
|
|
349
|
+
<template #body>
|
|
350
|
+
<div
|
|
351
|
+
v-if="secret"
|
|
352
|
+
class="grid gap-3 text-sm"
|
|
353
|
+
>
|
|
354
|
+
<div>
|
|
355
|
+
<div class="text-muted">
|
|
356
|
+
Client ID
|
|
357
|
+
</div>
|
|
358
|
+
<code class="break-all">{{ secret.clientId }}</code>
|
|
359
|
+
<UButton
|
|
360
|
+
size="xs"
|
|
361
|
+
variant="link"
|
|
362
|
+
@click="copy(secret.clientId)"
|
|
363
|
+
>
|
|
364
|
+
Copy
|
|
365
|
+
</UButton>
|
|
366
|
+
</div>
|
|
367
|
+
<div>
|
|
368
|
+
<div class="text-muted">
|
|
369
|
+
Client secret
|
|
370
|
+
</div>
|
|
371
|
+
<code class="break-all">{{ secret.clientSecret }}</code>
|
|
372
|
+
<UButton
|
|
373
|
+
size="xs"
|
|
374
|
+
variant="link"
|
|
375
|
+
@click="copy(secret.clientSecret)"
|
|
376
|
+
>
|
|
377
|
+
Copy
|
|
378
|
+
</UButton>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</template>
|
|
382
|
+
</UModal>
|
|
383
|
+
</UContainer>
|
|
384
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineOptions({ name: "OAuthApplicationsPage" });
|
|
3
|
+
const { applications, status } = useMyApplications();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<UContainer class="py-10">
|
|
8
|
+
<h1 class="mb-6 text-2xl font-semibold">
|
|
9
|
+
Applications
|
|
10
|
+
</h1>
|
|
11
|
+
<p
|
|
12
|
+
v-if="status !== 'pending' && !applications.length"
|
|
13
|
+
class="text-muted"
|
|
14
|
+
>
|
|
15
|
+
There are no applications you can sign in to yet.
|
|
16
|
+
</p>
|
|
17
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
18
|
+
<UCard
|
|
19
|
+
v-for="application in applications"
|
|
20
|
+
:key="application.id"
|
|
21
|
+
>
|
|
22
|
+
<div class="flex items-start gap-3">
|
|
23
|
+
<UIcon
|
|
24
|
+
:name="application.icon || 'i-lucide-globe'"
|
|
25
|
+
class="mt-1 size-6 shrink-0"
|
|
26
|
+
/>
|
|
27
|
+
<div class="min-w-0 flex-1">
|
|
28
|
+
<h2 class="font-medium">
|
|
29
|
+
{{ application.name }}
|
|
30
|
+
</h2>
|
|
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>
|
|
48
|
+
</div>
|
|
49
|
+
</UContainer>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineOptions({ name: "OAuthAuthorizePage" });
|
|
3
|
+
const route = useRoute();
|
|
4
|
+
const signInPath = useRuntimeConfig().public.users?.pages?.login || "/login";
|
|
5
|
+
const { data: consent, error } = await useFetch("/api/oauth-server/consent", {
|
|
6
|
+
query: route.query,
|
|
7
|
+
key: `oauth-server:consent:${route.fullPath}`
|
|
8
|
+
});
|
|
9
|
+
if (error.value?.statusCode === 401) {
|
|
10
|
+
await navigateTo(`${signInPath}?redirect=${encodeURIComponent(route.fullPath)}`);
|
|
11
|
+
}
|
|
12
|
+
const scopeLabels = {
|
|
13
|
+
openid: "Verify your identity",
|
|
14
|
+
profile: "Your name and profile picture",
|
|
15
|
+
email: "Your email address"
|
|
16
|
+
};
|
|
17
|
+
const pending = ref(null);
|
|
18
|
+
const failure = ref(null);
|
|
19
|
+
async function decide(approved) {
|
|
20
|
+
pending.value = approved ? "approve" : "deny";
|
|
21
|
+
failure.value = null;
|
|
22
|
+
try {
|
|
23
|
+
const { redirectUrl } = await $fetch("/oauth/authorize", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: { ...route.query, approved }
|
|
26
|
+
});
|
|
27
|
+
await navigateTo(redirectUrl, { external: true });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
failure.value = err.statusMessage || "Something went wrong. Please try again.";
|
|
30
|
+
pending.value = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<UContainer class="py-10">
|
|
37
|
+
<UCard
|
|
38
|
+
v-if="consent"
|
|
39
|
+
class="mx-auto max-w-md"
|
|
40
|
+
>
|
|
41
|
+
<template #header>
|
|
42
|
+
<div class="flex items-center gap-3">
|
|
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>
|
|
57
|
+
|
|
58
|
+
<p class="mb-3 text-sm">
|
|
59
|
+
Signed in as <strong>{{ consent.user.name }}</strong> ({{ consent.user.email }}).
|
|
60
|
+
<strong>{{ consent.application.name }}</strong> wants to:
|
|
61
|
+
</p>
|
|
62
|
+
<ul class="mb-4 list-inside list-disc space-y-1 text-sm">
|
|
63
|
+
<li
|
|
64
|
+
v-for="scope in consent.scope"
|
|
65
|
+
:key="scope"
|
|
66
|
+
>
|
|
67
|
+
{{ scopeLabels[scope] ?? scope }}
|
|
68
|
+
</li>
|
|
69
|
+
</ul>
|
|
70
|
+
<UAlert
|
|
71
|
+
v-if="failure"
|
|
72
|
+
color="error"
|
|
73
|
+
variant="subtle"
|
|
74
|
+
:title="failure"
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<template #footer>
|
|
78
|
+
<div class="flex justify-end gap-2">
|
|
79
|
+
<UButton
|
|
80
|
+
color="neutral"
|
|
81
|
+
variant="outline"
|
|
82
|
+
:loading="pending === 'deny'"
|
|
83
|
+
:disabled="!!pending"
|
|
84
|
+
@click="decide(false)"
|
|
85
|
+
>
|
|
86
|
+
Deny
|
|
87
|
+
</UButton>
|
|
88
|
+
<UButton
|
|
89
|
+
:loading="pending === 'approve'"
|
|
90
|
+
:disabled="!!pending"
|
|
91
|
+
@click="decide(true)"
|
|
92
|
+
>
|
|
93
|
+
Allow
|
|
94
|
+
</UButton>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
</UCard>
|
|
98
|
+
|
|
99
|
+
<UAlert
|
|
100
|
+
v-else
|
|
101
|
+
class="mx-auto max-w-md"
|
|
102
|
+
color="error"
|
|
103
|
+
variant="subtle"
|
|
104
|
+
title="This sign-in request cannot be completed"
|
|
105
|
+
:description="error?.statusMessage || 'The request is invalid.'"
|
|
106
|
+
/>
|
|
107
|
+
</UContainer>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineMenuItem, defineMenuSource, extendMenuItem } from "@frigear-nu/menu/utils";
|
|
2
|
+
export default defineMenuSource(() => {
|
|
3
|
+
const config = useRuntimeConfig().public.oauthServer;
|
|
4
|
+
if (config?.pages?.applications) {
|
|
5
|
+
const item = defineMenuItem({
|
|
6
|
+
id: "oauth-applications",
|
|
7
|
+
label: "Applications",
|
|
8
|
+
description: "Applications you can sign in to.",
|
|
9
|
+
icon: "i-lucide-layout-grid",
|
|
10
|
+
to: config.pages.applications,
|
|
11
|
+
order: 50
|
|
12
|
+
});
|
|
13
|
+
extendMenuItem("account", item);
|
|
14
|
+
extendMenuItem("dashboard.account", item);
|
|
15
|
+
}
|
|
16
|
+
if (config?.pages?.admin) {
|
|
17
|
+
extendMenuItem("dashboard.account", defineMenuItem({
|
|
18
|
+
id: "oauth-admin",
|
|
19
|
+
label: "OAuth applications",
|
|
20
|
+
description: "Manage the applications users can sign in to.",
|
|
21
|
+
icon: "i-lucide-key-round",
|
|
22
|
+
to: config.pages.admin,
|
|
23
|
+
order: 90,
|
|
24
|
+
tags: ["admin"]
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
});
|