@frigear-nu/oauth-server 0.1.1 → 0.1.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/dist/module.d.mts +27 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +39 -20
- package/dist/runtime/app/components/OAuthApplicationSlideover.vue +41 -0
- package/dist/runtime/app/components/OAuthCredentialsModal.d.vue.ts +2 -1
- package/dist/runtime/app/components/OAuthCredentialsModal.vue +10 -3
- package/dist/runtime/app/components/OAuthCredentialsModal.vue.d.ts +2 -1
- package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +3 -0
- package/dist/runtime/app/composables/useOAuthApplicationActions.js +32 -15
- package/dist/runtime/app/composables/useOAuthApplications.d.ts +20 -20
- package/dist/runtime/app/composables/useOAuthApplications.js +35 -18
- package/dist/runtime/app/pages/admin.vue +116 -58
- package/dist/runtime/app/pages/applications.vue +59 -24
- package/dist/runtime/app/pages/authorize.vue +3 -2
- package/dist/runtime/i18n/lang/da.json +21 -4
- package/dist/runtime/i18n/lang/en.json +21 -4
- package/dist/runtime/server/bootstrap/my-applications.d.ts +7 -0
- package/dist/runtime/server/bootstrap/my-applications.js +4 -0
- package/dist/runtime/server/db/schema.postgresql.d.ts +17 -0
- package/dist/runtime/server/db/schema.postgresql.js +7 -1
- package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
- package/dist/runtime/server/db/schema.sqlite.js +7 -1
- package/dist/runtime/server/middleware/oauth-cors.d.ts +2 -0
- package/dist/runtime/server/middleware/oauth-cors.js +25 -0
- package/dist/runtime/server/routes/api/admin/application-revoke.post.js +5 -3
- package/dist/runtime/server/routes/api/admin/application-secret.post.js +3 -1
- package/dist/runtime/server/routes/api/admin/application.delete.js +4 -3
- package/dist/runtime/server/routes/api/admin/application.get.js +6 -3
- package/dist/runtime/server/routes/api/admin/application.patch.js +4 -3
- package/dist/runtime/server/routes/api/admin/applications.get.js +20 -8
- package/dist/runtime/server/routes/api/admin/applications.post.js +3 -3
- package/dist/runtime/server/routes/api/consent.get.js +4 -3
- package/dist/runtime/server/routes/api/me-applications.get.js +2 -4
- package/dist/runtime/server/routes/oidc/authorize.get.js +5 -6
- package/dist/runtime/server/routes/oidc/authorize.post.js +1 -2
- package/dist/runtime/server/routes/oidc/openid-configuration.get.js +8 -6
- package/dist/runtime/server/routes/oidc/revoke.post.js +19 -7
- package/dist/runtime/server/routes/oidc/token.post.js +24 -9
- package/dist/runtime/server/utils/oauth-access.d.ts +2 -0
- package/dist/runtime/server/utils/oauth-access.js +7 -1
- package/dist/runtime/server/utils/oauth-applications.d.ts +30 -4
- package/dist/runtime/server/utils/oauth-applications.js +77 -17
- package/dist/runtime/server/utils/oauth-authorize.d.ts +8 -4
- package/dist/runtime/server/utils/oauth-authorize.js +11 -9
- package/dist/runtime/server/utils/oauth-config.d.ts +7 -0
- package/dist/runtime/server/utils/oauth-config.js +10 -0
- package/dist/runtime/server/utils/oauth-crypto.d.ts +9 -2
- package/dist/runtime/server/utils/oauth-crypto.js +10 -4
- package/dist/runtime/server/utils/oauth-keys.js +3 -2
- package/dist/runtime/utils/api.d.ts +28 -0
- package/dist/runtime/utils/api.js +27 -0
- package/dist/runtime/utils/index.d.ts +15 -1
- package/dist/runtime/utils/index.js +33 -1
- package/dist/types.d.mts +1 -1
- package/package.json +16 -11
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { oauthApplicationsApi } from "../composables/useOAuthApplications";
|
|
2
3
|
defineOptions({ name: "OAuthAdminPage" });
|
|
3
4
|
const t = useModuleT("oauthServer");
|
|
4
|
-
const
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
const { page, pageSize, search, sort: sortState, query, filtered, reset } = useTableQuery({
|
|
7
|
+
sort: { id: "createdAt", desc: true }
|
|
8
|
+
});
|
|
9
|
+
const sort = useTableSort(null, sortState);
|
|
10
|
+
const { sorting } = sort;
|
|
11
|
+
const { applications, total, data, status, error, loading, refreshing, refresh } = await useOAuthApplications(query);
|
|
5
12
|
const actions = useOAuthApplicationActions();
|
|
6
13
|
const forbidden = computed(() => error.value?.statusCode === 401 || error.value?.statusCode === 403);
|
|
14
|
+
const failed = computed(() => !!error.value && !forbidden.value && !data.value);
|
|
7
15
|
const columns = computed(() => [
|
|
8
|
-
{ accessorKey: "name", header: t("admin.application", "Application") },
|
|
16
|
+
{ accessorKey: "name", header: sort.header("name", t("admin.application", "Application")) },
|
|
9
17
|
{ id: "access", header: t("admin.access", "Access") },
|
|
10
|
-
{ id: "grants", header: t("admin.signedIn", "Signed in"), meta: numericColumn },
|
|
18
|
+
{ id: "grants", accessorFn: (row) => row.activeGrants ?? 0, header: t("admin.signedIn", "Signed in"), meta: numericColumn },
|
|
11
19
|
{ id: "actions", header: "", meta: { class: { th: "w-0", td: "w-0" } } }
|
|
12
20
|
]);
|
|
21
|
+
async function openFromQuery() {
|
|
22
|
+
const id = route.query.application;
|
|
23
|
+
if (typeof id !== "string" || !id) return;
|
|
24
|
+
const application = applications.value.find((item) => item.id === id) ?? await oauthApplicationsApi.get(id).catch(() => void 0);
|
|
25
|
+
if (application) void actions.edit(application);
|
|
26
|
+
}
|
|
27
|
+
onMounted(openFromQuery);
|
|
28
|
+
watch(() => route.query.application, openFromQuery);
|
|
13
29
|
</script>
|
|
14
30
|
|
|
15
31
|
<template>
|
|
@@ -36,69 +52,111 @@ const columns = computed(() => [
|
|
|
36
52
|
:title="t('admin.forbiddenTitle', 'You are not allowed to manage OAuth applications')"
|
|
37
53
|
:description="t('admin.forbiddenDescription', 'Ask an administrator for access.')"
|
|
38
54
|
/>
|
|
39
|
-
<
|
|
40
|
-
v-else
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
<UEmpty
|
|
56
|
+
v-else-if="failed"
|
|
57
|
+
icon="i-lucide-circle-alert"
|
|
58
|
+
:title="t('admin.loadFailed', 'OAuth applications could not be loaded')"
|
|
59
|
+
:description="errorMessage(error, '')"
|
|
60
|
+
:actions="[{ label: t('admin.tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
61
|
+
/>
|
|
62
|
+
<template v-else>
|
|
63
|
+
<PageToolbar>
|
|
64
|
+
<UInput
|
|
65
|
+
v-model="search"
|
|
66
|
+
class="w-full max-w-sm"
|
|
67
|
+
icon="i-lucide-search"
|
|
68
|
+
:loading="status === 'pending'"
|
|
69
|
+
:placeholder="t('admin.search', 'Search name or client ID...')"
|
|
70
|
+
/>
|
|
71
|
+
</PageToolbar>
|
|
72
|
+
|
|
73
|
+
<TableCard
|
|
74
|
+
:loading="loading"
|
|
75
|
+
:refreshing="refreshing"
|
|
49
76
|
>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
<UTable
|
|
78
|
+
v-model:sorting="sorting"
|
|
79
|
+
:data="applications"
|
|
80
|
+
:columns="columns"
|
|
81
|
+
:sorting-options="{ manualSorting: true }"
|
|
82
|
+
:ui="tableUi()"
|
|
83
|
+
@select="(_event, row2) => actions.edit(row2.original)"
|
|
84
|
+
>
|
|
85
|
+
<template #name-cell="{ row }">
|
|
86
|
+
<TableIdentity
|
|
87
|
+
:title="row.original.name"
|
|
88
|
+
:description="row.original.websiteUrl"
|
|
89
|
+
:avatar="{ icon: row.original.icon || 'i-lucide-globe' }"
|
|
90
|
+
>
|
|
91
|
+
<template #badges>
|
|
92
|
+
<UBadge
|
|
93
|
+
v-if="row.original.clientType === 'public'"
|
|
94
|
+
color="info"
|
|
95
|
+
variant="subtle"
|
|
96
|
+
size="sm"
|
|
97
|
+
:label="t('admin.public', 'Public')"
|
|
98
|
+
/>
|
|
99
|
+
<UBadge
|
|
100
|
+
v-if="!row.original.isActive"
|
|
101
|
+
color="neutral"
|
|
102
|
+
variant="subtle"
|
|
103
|
+
size="sm"
|
|
104
|
+
:label="t('admin.disabled', 'Disabled')"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
</TableIdentity>
|
|
108
|
+
</template>
|
|
109
|
+
<template #access-cell="{ row }">
|
|
110
|
+
<div class="flex flex-wrap gap-1">
|
|
111
|
+
<UBadge
|
|
112
|
+
v-for="role in row.original.allowedRoles"
|
|
113
|
+
:key="role"
|
|
114
|
+
variant="subtle"
|
|
115
|
+
:label="role"
|
|
116
|
+
/>
|
|
57
117
|
<UBadge
|
|
58
|
-
v-if="!row.original.
|
|
118
|
+
v-if="!row.original.allowedRoles.length"
|
|
59
119
|
color="neutral"
|
|
60
120
|
variant="subtle"
|
|
61
|
-
|
|
62
|
-
:label="t('admin.disabled', 'Disabled')"
|
|
121
|
+
:label="t('admin.everyone', 'Every signed-in user')"
|
|
63
122
|
/>
|
|
64
|
-
</
|
|
65
|
-
</
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
:
|
|
72
|
-
|
|
73
|
-
:label="role"
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|
|
125
|
+
<template #grants-cell="{ row }">
|
|
126
|
+
{{ row.original.activeGrants ?? 0 }}
|
|
127
|
+
</template>
|
|
128
|
+
<template #actions-cell="{ row }">
|
|
129
|
+
<RowMenu
|
|
130
|
+
:items="actions.menu(row.original)"
|
|
131
|
+
:subject="row.original.name"
|
|
74
132
|
/>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
</template>
|
|
134
|
+
<template #empty>
|
|
135
|
+
<UEmpty
|
|
136
|
+
v-if="filtered"
|
|
137
|
+
variant="naked"
|
|
138
|
+
icon="i-lucide-search"
|
|
139
|
+
:title="t('admin.noMatches', 'No applications match')"
|
|
140
|
+
:actions="[{ label: t('admin.clearSearch', 'Clear search'), color: 'neutral', variant: 'outline', onClick: reset }]"
|
|
80
141
|
/>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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() }]"
|
|
142
|
+
<UEmpty
|
|
143
|
+
v-else
|
|
144
|
+
variant="naked"
|
|
145
|
+
icon="i-lucide-app-window"
|
|
146
|
+
:title="t('admin.emptyTitle', 'No applications yet')"
|
|
147
|
+
:description="t('admin.emptyDescription', 'Add an application to let users sign in to it with their account.')"
|
|
148
|
+
:actions="[{ label: t('admin.new', 'Add application'), icon: 'i-lucide-plus', onClick: () => actions.create() }]"
|
|
149
|
+
/>
|
|
150
|
+
</template>
|
|
151
|
+
</UTable>
|
|
152
|
+
<template #footer>
|
|
153
|
+
<TablePagination
|
|
154
|
+
v-model:page="page"
|
|
155
|
+
v-model:page-size="pageSize"
|
|
156
|
+
:total="total"
|
|
99
157
|
/>
|
|
100
158
|
</template>
|
|
101
|
-
</
|
|
102
|
-
</
|
|
159
|
+
</TableCard>
|
|
160
|
+
</template>
|
|
103
161
|
</div>
|
|
104
162
|
</template>
|
|
@@ -1,38 +1,73 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthApplicationsPage" });
|
|
3
3
|
const t = useModuleT("oauthServer");
|
|
4
|
-
const { applications,
|
|
4
|
+
const { applications, loading, error, refresh } = await useMyApplications();
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
8
|
<div class="flex flex-col gap-4">
|
|
9
|
+
<!-- The same keys as the account menu item, so the label and the title cannot drift. -->
|
|
10
|
+
<PageHeading
|
|
11
|
+
:title="t('menu.applications.label', 'Applications')"
|
|
12
|
+
:description="t('menu.applications.description', 'Applications you can sign in to.')"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<SectionCard
|
|
16
|
+
v-if="loading"
|
|
17
|
+
loading
|
|
18
|
+
aria-busy="true"
|
|
19
|
+
/>
|
|
9
20
|
<UEmpty
|
|
10
|
-
v-if="
|
|
11
|
-
icon="i-lucide-
|
|
12
|
-
:title="t('applications.
|
|
13
|
-
:description="
|
|
21
|
+
v-else-if="error && !applications.length"
|
|
22
|
+
icon="i-lucide-circle-alert"
|
|
23
|
+
:title="t('applications.loadFailed', 'Your applications could not be loaded')"
|
|
24
|
+
:description="errorMessage(error, '')"
|
|
25
|
+
:actions="[{ label: t('applications.tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
14
26
|
/>
|
|
15
|
-
<
|
|
27
|
+
<SectionCard
|
|
16
28
|
v-else
|
|
17
|
-
|
|
29
|
+
flush
|
|
18
30
|
>
|
|
19
|
-
<
|
|
20
|
-
v-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
:title="
|
|
24
|
-
:description="
|
|
25
|
-
|
|
31
|
+
<UEmpty
|
|
32
|
+
v-if="!applications.length"
|
|
33
|
+
variant="naked"
|
|
34
|
+
icon="i-lucide-layout-grid"
|
|
35
|
+
:title="t('applications.emptyTitle', 'No applications yet')"
|
|
36
|
+
:description="t('applications.emptyDescription', 'There are no applications you can sign in to yet.')"
|
|
37
|
+
/>
|
|
38
|
+
<ul
|
|
39
|
+
v-else
|
|
40
|
+
class="divide-default divide-y"
|
|
26
41
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
<li
|
|
43
|
+
v-for="application in applications"
|
|
44
|
+
:key="application.id"
|
|
45
|
+
class="flex items-center gap-3 px-4 py-3"
|
|
46
|
+
>
|
|
47
|
+
<UAvatar
|
|
48
|
+
:icon="application.icon || 'i-lucide-globe'"
|
|
49
|
+
size="md"
|
|
50
|
+
/>
|
|
51
|
+
<div class="min-w-0 flex-1">
|
|
52
|
+
<p class="text-highlighted truncate text-sm font-medium">
|
|
53
|
+
{{ application.name }}
|
|
54
|
+
</p>
|
|
55
|
+
<p class="text-muted truncate text-sm">
|
|
56
|
+
{{ application.description ?? application.websiteUrl }}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
<UButton
|
|
60
|
+
:to="application.loginUrl"
|
|
61
|
+
external
|
|
62
|
+
color="neutral"
|
|
63
|
+
variant="outline"
|
|
64
|
+
trailing-icon="i-lucide-arrow-up-right"
|
|
65
|
+
size="sm"
|
|
66
|
+
:label="t('applications.open', 'Open')"
|
|
67
|
+
:aria-label="t('applications.openApplication', 'Open {name}', { name: application.name })"
|
|
68
|
+
/>
|
|
69
|
+
</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</SectionCard>
|
|
37
72
|
</div>
|
|
38
73
|
</template>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { oauthServerApi, oidc } from "../../utils/api";
|
|
2
3
|
defineOptions({ name: "OAuthAuthorizePage" });
|
|
3
4
|
const t = useModuleT("oauthServer");
|
|
4
5
|
const route = useRoute();
|
|
5
6
|
const signInPath = useAuthPages().login;
|
|
6
7
|
const showError = useErrorToast();
|
|
7
|
-
const { data: consent, error } = await useFetch(
|
|
8
|
+
const { data: consent, error } = await useFetch(oauthServerApi.consent, {
|
|
8
9
|
query: route.query,
|
|
9
10
|
key: `oauth-server:consent:${route.fullPath}`
|
|
10
11
|
});
|
|
@@ -23,7 +24,7 @@ const pending = ref(null);
|
|
|
23
24
|
async function decide(approved) {
|
|
24
25
|
pending.value = approved ? "approve" : "deny";
|
|
25
26
|
try {
|
|
26
|
-
const { redirectUrl } = await $fetch(
|
|
27
|
+
const { redirectUrl } = await $fetch(oidc.authorize, {
|
|
27
28
|
method: "POST",
|
|
28
29
|
body: { ...route.query, approved }
|
|
29
30
|
});
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
"new": "Tilføj applikation",
|
|
17
17
|
"forbiddenTitle": "Du har ikke adgang til at administrere OAuth-applikationer",
|
|
18
18
|
"forbiddenDescription": "Bed en administrator om adgang.",
|
|
19
|
+
"loadFailed": "OAuth-applikationer kunne ikke indlæses",
|
|
20
|
+
"tryAgain": "Prøv igen",
|
|
19
21
|
"application": "Applikation",
|
|
20
22
|
"access": "Adgang",
|
|
21
23
|
"signedIn": "Logget ind",
|
|
24
|
+
"public": "Offentlig",
|
|
22
25
|
"disabled": "Deaktiveret",
|
|
23
26
|
"callback": "Callback",
|
|
24
27
|
"everyone": "Alle indloggede brugere",
|
|
@@ -43,7 +46,10 @@
|
|
|
43
46
|
"enabled": "Applikationen er aktiveret",
|
|
44
47
|
"disabledToast": "Applikationen er deaktiveret",
|
|
45
48
|
"secretReplaced": "Klienthemmeligheden er skiftet",
|
|
46
|
-
"signedOut": "Alle er logget ud"
|
|
49
|
+
"signedOut": "Alle er logget ud",
|
|
50
|
+
"search": "Søg på navn eller klient-ID...",
|
|
51
|
+
"noMatches": "Ingen applikationer matcher",
|
|
52
|
+
"clearSearch": "Ryd søgning"
|
|
47
53
|
},
|
|
48
54
|
"form": {
|
|
49
55
|
"createTitle": "Tilføj applikation",
|
|
@@ -66,7 +72,14 @@
|
|
|
66
72
|
"tags": "Tags",
|
|
67
73
|
"description": "Beskrivelse",
|
|
68
74
|
"icon": "Ikon",
|
|
69
|
-
"priority": "Prioritet"
|
|
75
|
+
"priority": "Prioritet",
|
|
76
|
+
"clientType": "Klienttype",
|
|
77
|
+
"clientTypeConfidential": "Fortrolig (serverside, har en klienthemmelighed)",
|
|
78
|
+
"clientTypePublic": "Offentlig (native app eller single-page-app, ingen hemmelighed)",
|
|
79
|
+
"clientTypeHint": "Kan ikke ændres senere. Offentlige klienter logger kun ind med PKCE og får derfor aldrig en hemmelighed.",
|
|
80
|
+
"redirectUris": "Redirect-URI'er",
|
|
81
|
+
"redirectUrisHint": "Ekstra præcise redirect-URI'er. Offentlige klienter må bruge URI'er med eget skema, fx com.example.app:/callback.",
|
|
82
|
+
"redirectUriInvalid": "Angiv en https-URL eller, for offentlige klienter, en URI med eget skema."
|
|
70
83
|
},
|
|
71
84
|
"credentials": {
|
|
72
85
|
"title": "Legitimationsoplysninger for {name}",
|
|
@@ -75,13 +88,17 @@
|
|
|
75
88
|
"clientSecret": "Klienthemmelighed",
|
|
76
89
|
"copy": "Kopiér",
|
|
77
90
|
"copiedClientId": "Klient-id kopieret",
|
|
78
|
-
"copiedClientSecret": "Klienthemmelighed kopieret"
|
|
91
|
+
"copiedClientSecret": "Klienthemmelighed kopieret",
|
|
92
|
+
"publicDescription": "Dette er en offentlig klient: den har ingen klienthemmelighed og logger kun ind med PKCE.",
|
|
93
|
+
"noSecret": "Ingen (offentlig klient)"
|
|
79
94
|
},
|
|
80
95
|
"applications": {
|
|
81
96
|
"open": "Åbn",
|
|
82
97
|
"openApplication": "Åbn {name}",
|
|
83
98
|
"emptyTitle": "Ingen applikationer endnu",
|
|
84
|
-
"emptyDescription": "Der er endnu ingen applikationer, du kan logge ind på."
|
|
99
|
+
"emptyDescription": "Der er endnu ingen applikationer, du kan logge ind på.",
|
|
100
|
+
"loadFailed": "Dine applikationer kunne ikke indlæses",
|
|
101
|
+
"tryAgain": "Prøv igen"
|
|
85
102
|
},
|
|
86
103
|
"authorize": {
|
|
87
104
|
"title": "Log ind på {name}",
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
"new": "Add application",
|
|
17
17
|
"forbiddenTitle": "You are not allowed to manage OAuth applications",
|
|
18
18
|
"forbiddenDescription": "Ask an administrator for access.",
|
|
19
|
+
"loadFailed": "OAuth applications could not be loaded",
|
|
20
|
+
"tryAgain": "Try again",
|
|
19
21
|
"application": "Application",
|
|
20
22
|
"access": "Access",
|
|
21
23
|
"signedIn": "Signed in",
|
|
24
|
+
"public": "Public",
|
|
22
25
|
"disabled": "Disabled",
|
|
23
26
|
"callback": "Callback",
|
|
24
27
|
"everyone": "Every signed-in user",
|
|
@@ -43,7 +46,10 @@
|
|
|
43
46
|
"enabled": "Application enabled",
|
|
44
47
|
"disabledToast": "Application disabled",
|
|
45
48
|
"secretReplaced": "Client secret replaced",
|
|
46
|
-
"signedOut": "Everyone signed out"
|
|
49
|
+
"signedOut": "Everyone signed out",
|
|
50
|
+
"search": "Search name or client ID...",
|
|
51
|
+
"noMatches": "No applications match",
|
|
52
|
+
"clearSearch": "Clear search"
|
|
47
53
|
},
|
|
48
54
|
"form": {
|
|
49
55
|
"createTitle": "Add application",
|
|
@@ -66,7 +72,14 @@
|
|
|
66
72
|
"tags": "Tags",
|
|
67
73
|
"description": "Description",
|
|
68
74
|
"icon": "Icon",
|
|
69
|
-
"priority": "Priority"
|
|
75
|
+
"priority": "Priority",
|
|
76
|
+
"clientType": "Client type",
|
|
77
|
+
"clientTypeConfidential": "Confidential (server-side, has a client secret)",
|
|
78
|
+
"clientTypePublic": "Public (native or single-page app, no secret)",
|
|
79
|
+
"clientTypeHint": "Cannot be changed later. Public clients sign in with PKCE only, so they never get a secret.",
|
|
80
|
+
"redirectUris": "Redirect URIs",
|
|
81
|
+
"redirectUrisHint": "Extra exact redirect URIs. Public clients may use custom-scheme URIs such as com.example.app:/callback.",
|
|
82
|
+
"redirectUriInvalid": "Enter a https URL or, for public clients, a custom-scheme URI."
|
|
70
83
|
},
|
|
71
84
|
"credentials": {
|
|
72
85
|
"title": "Credentials for {name}",
|
|
@@ -75,13 +88,17 @@
|
|
|
75
88
|
"clientSecret": "Client secret",
|
|
76
89
|
"copy": "Copy",
|
|
77
90
|
"copiedClientId": "Client ID copied",
|
|
78
|
-
"copiedClientSecret": "Client secret copied"
|
|
91
|
+
"copiedClientSecret": "Client secret copied",
|
|
92
|
+
"publicDescription": "This is a public client: it has no client secret and signs in with PKCE only.",
|
|
93
|
+
"noSecret": "None (public client)"
|
|
79
94
|
},
|
|
80
95
|
"applications": {
|
|
81
96
|
"open": "Open",
|
|
82
97
|
"openApplication": "Open {name}",
|
|
83
98
|
"emptyTitle": "No applications yet",
|
|
84
|
-
"emptyDescription": "There are no applications you can sign in to yet."
|
|
99
|
+
"emptyDescription": "There are no applications you can sign in to yet.",
|
|
100
|
+
"loadFailed": "Your applications could not be loaded",
|
|
101
|
+
"tryAgain": "Try again"
|
|
85
102
|
},
|
|
86
103
|
"authorize": {
|
|
87
104
|
"title": "Sign in to {name}",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { UserApplicationDto } from '../../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The applications the signed-in user may sign in to: the body of `GET /api/oauth-server/me/applications` and the
|
|
5
|
+
* account bootstrap's `oauth-server:mine` (the key `useMyApplications()` reads), so both always return the same thing.
|
|
6
|
+
*/
|
|
7
|
+
export default function myApplications(event: H3Event): Promise<UserApplicationDto[]>;
|
|
@@ -20,6 +20,23 @@ export declare const oauthApplications: import("drizzle-orm/pg-core").PgTableWit
|
|
|
20
20
|
identity: undefined;
|
|
21
21
|
generated: undefined;
|
|
22
22
|
}, {}, {}>;
|
|
23
|
+
clientType: import("drizzle-orm/pg-core").PgColumn<{
|
|
24
|
+
name: "clientType";
|
|
25
|
+
tableName: "oauth_applications";
|
|
26
|
+
dataType: "string";
|
|
27
|
+
columnType: "PgText";
|
|
28
|
+
data: "confidential" | "public";
|
|
29
|
+
driverParam: string;
|
|
30
|
+
notNull: true;
|
|
31
|
+
hasDefault: true;
|
|
32
|
+
isPrimaryKey: false;
|
|
33
|
+
isAutoincrement: false;
|
|
34
|
+
hasRuntimeDefault: false;
|
|
35
|
+
enumValues: ["confidential", "public"];
|
|
36
|
+
baseColumn: never;
|
|
37
|
+
identity: undefined;
|
|
38
|
+
generated: undefined;
|
|
39
|
+
}, {}, {}>;
|
|
23
40
|
secretHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
24
41
|
name: "secretHash";
|
|
25
42
|
tableName: "oauth_applications";
|
|
@@ -2,7 +2,13 @@ import { pgTable, text, integer, boolean, jsonb, timestamp, index } from "drizzl
|
|
|
2
2
|
export const oauthApplications = pgTable("oauth_applications", {
|
|
3
3
|
/** `app_` + random id; it is the OAuth `client_id`. */
|
|
4
4
|
id: text().primaryKey(),
|
|
5
|
-
/**
|
|
5
|
+
/** `confidential` clients authenticate with a secret; `public` ones (native apps, SPAs) rely on PKCE alone. */
|
|
6
|
+
clientType: text({ enum: ["confidential", "public"] }).notNull().default("confidential"),
|
|
7
|
+
/**
|
|
8
|
+
* SHA-256 hex of the client secret. The secret itself is only shown once. Public clients have none: they get the hash
|
|
9
|
+
* of a random value nobody ever sees, so the column stays `NOT NULL` (relaxing it would make SQLite rebuild this table,
|
|
10
|
+
* which can cascade-delete the refresh tokens that reference it) and `clientType` decides whether it is ever checked.
|
|
11
|
+
*/
|
|
6
12
|
secretHash: text().notNull(),
|
|
7
13
|
name: text().notNull(),
|
|
8
14
|
description: text(),
|
|
@@ -22,6 +22,25 @@ export declare const oauthApplications: import("drizzle-orm/sqlite-core").SQLite
|
|
|
22
22
|
}, {}, {
|
|
23
23
|
length: undefined;
|
|
24
24
|
}>;
|
|
25
|
+
clientType: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
26
|
+
name: "clientType";
|
|
27
|
+
tableName: "oauth_applications";
|
|
28
|
+
dataType: "string";
|
|
29
|
+
columnType: "SQLiteText";
|
|
30
|
+
data: "confidential" | "public";
|
|
31
|
+
driverParam: string;
|
|
32
|
+
notNull: true;
|
|
33
|
+
hasDefault: true;
|
|
34
|
+
isPrimaryKey: false;
|
|
35
|
+
isAutoincrement: false;
|
|
36
|
+
hasRuntimeDefault: false;
|
|
37
|
+
enumValues: ["confidential", "public"];
|
|
38
|
+
baseColumn: never;
|
|
39
|
+
identity: undefined;
|
|
40
|
+
generated: undefined;
|
|
41
|
+
}, {}, {
|
|
42
|
+
length: number | undefined;
|
|
43
|
+
}>;
|
|
25
44
|
secretHash: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
26
45
|
name: "secretHash";
|
|
27
46
|
tableName: "oauth_applications";
|
|
@@ -2,7 +2,13 @@ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
|
|
2
2
|
export const oauthApplications = sqliteTable("oauth_applications", {
|
|
3
3
|
/** `app_` + random id; it is the OAuth `client_id`. */
|
|
4
4
|
id: text().primaryKey(),
|
|
5
|
-
/**
|
|
5
|
+
/** `confidential` clients authenticate with a secret; `public` ones (native apps, SPAs) rely on PKCE alone. */
|
|
6
|
+
clientType: text({ enum: ["confidential", "public"] }).notNull().default("confidential"),
|
|
7
|
+
/**
|
|
8
|
+
* SHA-256 hex of the client secret. The secret itself is only shown once. Public clients have none: they get the hash
|
|
9
|
+
* of a random value nobody ever sees, so the column stays `NOT NULL` (relaxing it would make SQLite rebuild this table,
|
|
10
|
+
* which can cascade-delete the refresh tokens that reference it) and `clientType` decides whether it is ever checked.
|
|
11
|
+
*/
|
|
6
12
|
secretHash: text().notNull(),
|
|
7
13
|
name: text().notNull(),
|
|
8
14
|
description: text(),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { oidc } from "../../utils/api.js";
|
|
2
|
+
const METHODS = {
|
|
3
|
+
[oidc.token]: "POST, OPTIONS",
|
|
4
|
+
[oidc.revoke]: "POST, OPTIONS",
|
|
5
|
+
[oidc.userinfo]: "GET, OPTIONS",
|
|
6
|
+
[oidc.configuration]: "GET, OPTIONS",
|
|
7
|
+
[oidc.jwks]: "GET, OPTIONS"
|
|
8
|
+
};
|
|
9
|
+
export default defineEventHandler((event) => {
|
|
10
|
+
const methods = METHODS[getRequestURL(event).pathname];
|
|
11
|
+
if (!methods) return;
|
|
12
|
+
appendResponseHeader(event, "Vary", "Origin");
|
|
13
|
+
const origin = getRequestHeader(event, "origin");
|
|
14
|
+
if (!origin || !getOAuthCorsOrigins().includes(origin)) return;
|
|
15
|
+
setResponseHeaders(event, {
|
|
16
|
+
"Access-Control-Allow-Origin": origin,
|
|
17
|
+
"Access-Control-Allow-Methods": methods,
|
|
18
|
+
"Access-Control-Allow-Headers": "authorization, content-type",
|
|
19
|
+
"Access-Control-Max-Age": "600"
|
|
20
|
+
});
|
|
21
|
+
if (event.method === "OPTIONS") {
|
|
22
|
+
setResponseStatus(event, 204);
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
await requireOAuthAdmin(event);
|
|
3
|
-
const id =
|
|
4
|
-
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
6
|
+
const application = await requireApplication(id);
|
|
5
7
|
await revokeApplicationGrants(id);
|
|
6
|
-
return
|
|
8
|
+
return toAdminApplicationDto(application, 0);
|
|
7
9
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
3
|
await requireOAuthAdmin(event);
|
|
3
|
-
const {
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
const { secret } = await rotateApplicationSecret(id);
|
|
4
6
|
return { clientSecret: secret };
|
|
5
7
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { idParams, noContent } from "@frigear-nu/module-kit/server";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
3
|
await requireOAuthAdmin(event);
|
|
3
|
-
await
|
|
4
|
-
|
|
5
|
-
return
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
await deleteApplication(id);
|
|
6
|
+
return noContent(event);
|
|
6
7
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
await requireOAuthAdmin(event);
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
6
|
+
const application = await requireApplication(id);
|
|
7
|
+
const grants = await countActiveGrants([id]);
|
|
8
|
+
return toAdminApplicationDto(application, grants[id] ?? 0);
|
|
6
9
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { applicationPatchSchema, toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
4
|
await requireOAuthAdmin(event);
|
|
4
|
-
const id =
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
6
|
const patch = await readValidatedBody(event, applicationPatchSchema.parse);
|
|
6
7
|
const application = await updateApplication(id, patch);
|
|
7
8
|
if (patch.isActive === false) await revokeApplicationGrants(id);
|
|
8
|
-
return application;
|
|
9
|
+
return toAdminApplicationDto(application);
|
|
9
10
|
});
|