@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.
Files changed (55) hide show
  1. package/dist/module.d.mts +27 -3
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +39 -20
  4. package/dist/runtime/app/components/OAuthApplicationSlideover.vue +41 -0
  5. package/dist/runtime/app/components/OAuthCredentialsModal.d.vue.ts +2 -1
  6. package/dist/runtime/app/components/OAuthCredentialsModal.vue +10 -3
  7. package/dist/runtime/app/components/OAuthCredentialsModal.vue.d.ts +2 -1
  8. package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +3 -0
  9. package/dist/runtime/app/composables/useOAuthApplicationActions.js +32 -15
  10. package/dist/runtime/app/composables/useOAuthApplications.d.ts +20 -20
  11. package/dist/runtime/app/composables/useOAuthApplications.js +35 -18
  12. package/dist/runtime/app/pages/admin.vue +116 -58
  13. package/dist/runtime/app/pages/applications.vue +59 -24
  14. package/dist/runtime/app/pages/authorize.vue +3 -2
  15. package/dist/runtime/i18n/lang/da.json +21 -4
  16. package/dist/runtime/i18n/lang/en.json +21 -4
  17. package/dist/runtime/server/bootstrap/my-applications.d.ts +7 -0
  18. package/dist/runtime/server/bootstrap/my-applications.js +4 -0
  19. package/dist/runtime/server/db/schema.postgresql.d.ts +17 -0
  20. package/dist/runtime/server/db/schema.postgresql.js +7 -1
  21. package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
  22. package/dist/runtime/server/db/schema.sqlite.js +7 -1
  23. package/dist/runtime/server/middleware/oauth-cors.d.ts +2 -0
  24. package/dist/runtime/server/middleware/oauth-cors.js +25 -0
  25. package/dist/runtime/server/routes/api/admin/application-revoke.post.js +5 -3
  26. package/dist/runtime/server/routes/api/admin/application-secret.post.js +3 -1
  27. package/dist/runtime/server/routes/api/admin/application.delete.js +4 -3
  28. package/dist/runtime/server/routes/api/admin/application.get.js +6 -3
  29. package/dist/runtime/server/routes/api/admin/application.patch.js +4 -3
  30. package/dist/runtime/server/routes/api/admin/applications.get.js +20 -8
  31. package/dist/runtime/server/routes/api/admin/applications.post.js +3 -3
  32. package/dist/runtime/server/routes/api/consent.get.js +4 -3
  33. package/dist/runtime/server/routes/api/me-applications.get.js +2 -4
  34. package/dist/runtime/server/routes/oidc/authorize.get.js +5 -6
  35. package/dist/runtime/server/routes/oidc/authorize.post.js +1 -2
  36. package/dist/runtime/server/routes/oidc/openid-configuration.get.js +8 -6
  37. package/dist/runtime/server/routes/oidc/revoke.post.js +19 -7
  38. package/dist/runtime/server/routes/oidc/token.post.js +24 -9
  39. package/dist/runtime/server/utils/oauth-access.d.ts +2 -0
  40. package/dist/runtime/server/utils/oauth-access.js +7 -1
  41. package/dist/runtime/server/utils/oauth-applications.d.ts +30 -4
  42. package/dist/runtime/server/utils/oauth-applications.js +77 -17
  43. package/dist/runtime/server/utils/oauth-authorize.d.ts +8 -4
  44. package/dist/runtime/server/utils/oauth-authorize.js +11 -9
  45. package/dist/runtime/server/utils/oauth-config.d.ts +7 -0
  46. package/dist/runtime/server/utils/oauth-config.js +10 -0
  47. package/dist/runtime/server/utils/oauth-crypto.d.ts +9 -2
  48. package/dist/runtime/server/utils/oauth-crypto.js +10 -4
  49. package/dist/runtime/server/utils/oauth-keys.js +3 -2
  50. package/dist/runtime/utils/api.d.ts +28 -0
  51. package/dist/runtime/utils/api.js +27 -0
  52. package/dist/runtime/utils/index.d.ts +15 -1
  53. package/dist/runtime/utils/index.js +33 -1
  54. package/dist/types.d.mts +1 -1
  55. 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 { applications, status, error } = useOAuthApplications();
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
- <TableCard
40
- v-else
41
- :loading="status === 'pending' && !applications.length"
42
- :refreshing="status === 'pending' && !!applications.length"
43
- >
44
- <UTable
45
- :data="applications"
46
- :columns="columns"
47
- :ui="tableUi()"
48
- @select="(_event, row2) => actions.edit(row2.original)"
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
- <template #name-cell="{ row }">
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>
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.isActive"
118
+ v-if="!row.original.allowedRoles.length"
59
119
  color="neutral"
60
120
  variant="subtle"
61
- size="sm"
62
- :label="t('admin.disabled', 'Disabled')"
121
+ :label="t('admin.everyone', 'Every signed-in user')"
63
122
  />
64
- </template>
65
- </TableIdentity>
66
- </template>
67
- <template #access-cell="{ row }">
68
- <div class="flex flex-wrap gap-1">
69
- <UBadge
70
- v-for="role in row.original.allowedRoles"
71
- :key="role"
72
- variant="subtle"
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
- <UBadge
76
- v-if="!row.original.allowedRoles.length"
77
- color="neutral"
78
- variant="subtle"
79
- :label="t('admin.everyone', 'Every signed-in user')"
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
- </div>
82
- </template>
83
- <template #grants-cell="{ row }">
84
- {{ row.original.activeGrants ?? 0 }}
85
- </template>
86
- <template #actions-cell="{ row }">
87
- <RowMenu
88
- :items="actions.menu(row.original)"
89
- :subject="row.original.name"
90
- />
91
- </template>
92
- <template #empty>
93
- <UEmpty
94
- variant="naked"
95
- icon="i-lucide-app-window"
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
- </UTable>
102
- </TableCard>
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, status } = useMyApplications();
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="status !== 'pending' && !applications.length"
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.')"
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
- <div
27
+ <SectionCard
16
28
  v-else
17
- class="grid gap-4 md:grid-cols-2"
29
+ flush
18
30
  >
19
- <UPageCard
20
- v-for="application in applications"
21
- :key="application.id"
22
- variant="outline"
23
- :title="application.name"
24
- :description="application.description ?? application.websiteUrl"
25
- :icon="application.icon || 'i-lucide-globe'"
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
- <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>
36
- </div>
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("/api/oauth-server/consent", {
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("/oauth/authorize", {
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[]>;
@@ -0,0 +1,4 @@
1
+ export default async function myApplications(event) {
2
+ const userId = await requireSessionUserId(event);
3
+ return listUserApplications(userId, event);
4
+ }
@@ -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
- /** SHA-256 hex of the client secret. The secret itself is only shown once. */
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
- /** SHA-256 hex of the client secret. The secret itself is only shown once. */
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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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 = getRouterParam(event, "id") ?? "";
4
- if (!await getApplication(id)) throw createError({ statusCode: 404, statusMessage: "Application not found." });
5
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
6
+ const application = await requireApplication(id);
5
7
  await revokeApplicationGrants(id);
6
- return { revoked: true };
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 { secret } = await rotateApplicationSecret(getRouterParam(event, "id") ?? "");
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 deleteApplication(getRouterParam(event, "id") ?? "");
4
- setResponseStatus(event, 204);
5
- return null;
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 application = await getApplication(getRouterParam(event, "id") ?? "");
4
- if (!application) throw createError({ statusCode: 404, statusMessage: "Application not found." });
5
- return { ...application, callbackUrl: `${application.websiteUrl}${useOAuthConfig().callbackPath}` };
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 { applicationPatchSchema } from "../../../utils/oauth-applications.js";
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 = getRouterParam(event, "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
  });