@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
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { H3Event } from 'h3';
|
|
3
3
|
|
|
4
|
+
/** `confidential` applications authenticate with a client secret; `public` ones (native apps, SPAs) rely on PKCE alone. */
|
|
5
|
+
type ClientType = 'confidential' | 'public';
|
|
4
6
|
type ModuleOptions = {
|
|
5
7
|
/**
|
|
6
8
|
* The `iss` claim and the discovery issuer, e.g. `https://id.example.com`. Defaults to the origin of the
|
|
@@ -19,6 +21,14 @@ type ModuleOptions = {
|
|
|
19
21
|
callbackPath?: string;
|
|
20
22
|
/** Extra scopes advertised and accepted next to `openid profile email`. Enrich their claims with `oauth:claims`. */
|
|
21
23
|
scopes?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Browser apps on another origin (for example the webview of a Capacitor app at `capacitor://localhost`) may call the
|
|
26
|
+
* token, revocation, userinfo and discovery endpoints. Exact origins only, never `*`. The authorization endpoint is
|
|
27
|
+
* never opened up: it is a top-level navigation. Cookies are not allowed cross-origin either.
|
|
28
|
+
*/
|
|
29
|
+
cors?: {
|
|
30
|
+
origins?: string[];
|
|
31
|
+
};
|
|
22
32
|
settings?: {
|
|
23
33
|
mcp?: boolean;
|
|
24
34
|
pages?: {
|
|
@@ -52,6 +62,8 @@ type OAuthServerRuntimeConfig = {
|
|
|
52
62
|
codeTtl: number;
|
|
53
63
|
callbackPath: string;
|
|
54
64
|
scopes: string[];
|
|
65
|
+
/** Allowed CORS origins (`NUXT_OAUTH_SERVER_CORS_ORIGINS` takes a comma separated list). */
|
|
66
|
+
corsOrigins: string[] | string;
|
|
55
67
|
authorizePath: string;
|
|
56
68
|
signInPath?: string;
|
|
57
69
|
};
|
|
@@ -73,6 +85,8 @@ type OAuthUser = {
|
|
|
73
85
|
type OAuthApplication = {
|
|
74
86
|
/** `app_` + random id; it is the OAuth `client_id`. */
|
|
75
87
|
id: string;
|
|
88
|
+
/** `public` applications have no client secret; they must use PKCE and may register native redirect URIs. */
|
|
89
|
+
clientType: ClientType;
|
|
76
90
|
name: string;
|
|
77
91
|
description: string | null;
|
|
78
92
|
icon: string | null;
|
|
@@ -80,7 +94,7 @@ type OAuthApplication = {
|
|
|
80
94
|
websiteUrl: string;
|
|
81
95
|
loginUrl: string | null;
|
|
82
96
|
previewUrlPattern: string | null;
|
|
83
|
-
/** Extra exact redirect URIs next to the derived `websiteUrl + callbackPath`. */
|
|
97
|
+
/** Extra exact redirect URIs next to the derived `websiteUrl + callbackPath`. Public clients may list native (custom scheme) URIs. */
|
|
84
98
|
redirectUris: string[];
|
|
85
99
|
/** Roles allowed to sign in. Empty = every signed-in user. */
|
|
86
100
|
allowedRoles: string[];
|
|
@@ -95,6 +109,8 @@ type UserApplicationDto = Pick<OAuthApplication, 'id' | 'name' | 'description' |
|
|
|
95
109
|
loginUrl: string;
|
|
96
110
|
};
|
|
97
111
|
type ApplicationInput = {
|
|
112
|
+
/** Fixed at creation. Defaults to `confidential`. */
|
|
113
|
+
clientType?: ClientType;
|
|
98
114
|
name: string;
|
|
99
115
|
websiteUrl: string;
|
|
100
116
|
description?: string | null;
|
|
@@ -159,10 +175,18 @@ type AdminApplicationDto = Omit<OAuthApplication, 'createdAt' | 'updatedAt'> & {
|
|
|
159
175
|
/** The redirect URI the application has to accept. */
|
|
160
176
|
callbackUrl: string;
|
|
161
177
|
};
|
|
178
|
+
/** One page of `GET /api/admin/oauth-server/applications` (the shape `useTableQuery` speaks). */
|
|
179
|
+
type AdminApplicationsPage = {
|
|
180
|
+
items: AdminApplicationDto[];
|
|
181
|
+
total: number;
|
|
182
|
+
page: number;
|
|
183
|
+
pageSize: number;
|
|
184
|
+
};
|
|
162
185
|
/** Returned once when an application is created. */
|
|
163
186
|
type CreatedApplicationDto = AdminApplicationDto & {
|
|
164
187
|
clientId: string;
|
|
165
|
-
|
|
188
|
+
/** `null` for public clients: they have no secret. */
|
|
189
|
+
clientSecret: string | null;
|
|
166
190
|
};
|
|
167
191
|
type ConsentDto = {
|
|
168
192
|
application: Pick<OAuthApplication, 'id' | 'name' | 'description' | 'icon' | 'websiteUrl'>;
|
|
@@ -204,4 +228,4 @@ declare module 'nitropack' {
|
|
|
204
228
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
205
229
|
|
|
206
230
|
export { _default as default };
|
|
207
|
-
export type { AdminApplicationDto, ApplicationAccessContext, ApplicationInput, AuthorizeContext, ClaimsContext, ConsentDto, CreatedApplicationDto, ModuleOptions, OAuthApplication, OAuthServerRuntimeConfig, OAuthUser, PublicModuleOptions, TokenIssuedContext, TokenRevokedContext, TokenType, UserApplicationDto, UserRolesContext };
|
|
231
|
+
export type { AdminApplicationDto, AdminApplicationsPage, ApplicationAccessContext, ApplicationInput, AuthorizeContext, ClaimsContext, ClientType, ConsentDto, CreatedApplicationDto, ModuleOptions, OAuthApplication, OAuthServerRuntimeConfig, OAuthUser, PublicModuleOptions, TokenIssuedContext, TokenRevokedContext, TokenType, UserApplicationDto, UserRolesContext };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports } from '@nuxt/kit';
|
|
2
|
-
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, inlineModuleRuntime, addModulePages, addOptionalMenuSource
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, addModuleRoutes, registerBootstrapProvider, inlineModuleRuntime, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
3
3
|
import { adminRoute } from '@frigear-nu/admin/utils';
|
|
4
4
|
import { addAuthScope, addAuthResolver } from '@frigear-nu/auth/utils';
|
|
5
5
|
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
6
|
+
import { oidc } from '../dist/runtime/utils/api.js';
|
|
6
7
|
|
|
7
8
|
const MODULE_NAME = "@frigear-nu/oauth-server";
|
|
8
|
-
const API_NAME = "oauthServer";
|
|
9
9
|
const module$1 = defineNuxtModule({
|
|
10
10
|
meta: { name: MODULE_NAME, configKey: "oauthServer" },
|
|
11
11
|
defaults: {
|
|
@@ -15,6 +15,7 @@ const module$1 = defineNuxtModule({
|
|
|
15
15
|
codeTtl: 600,
|
|
16
16
|
callbackPath: "/auth/frigear",
|
|
17
17
|
scopes: [],
|
|
18
|
+
cors: { origins: [] },
|
|
18
19
|
settings: { pages: {} }
|
|
19
20
|
},
|
|
20
21
|
moduleDependencies: {
|
|
@@ -44,6 +45,13 @@ const module$1 = defineNuxtModule({
|
|
|
44
45
|
const authorizePath = settings.pages?.authorize === false ? "/authorize" : resolvePagePath(settings.pages?.authorize ?? true, "/authorize");
|
|
45
46
|
const adminPath = hasNuxtModule("@frigear-nu/admin") ? resolvePagePath(settings.pages?.admin, adminRoute(nuxt, "oauth")) : void 0;
|
|
46
47
|
const applicationsPath = resolvePagePath(settings.pages?.applications, "/account/applications");
|
|
48
|
+
const corsOrigins = (options.cors?.origins ?? []).map((origin) => origin.trim()).filter(Boolean);
|
|
49
|
+
for (const origin of corsOrigins) {
|
|
50
|
+
const url = URL.canParse(origin) ? new URL(origin) : void 0;
|
|
51
|
+
if (!url || origin === "*" || origin !== `${url.protocol}//${url.host}`) {
|
|
52
|
+
throw new Error(`[${MODULE_NAME}] cors.origins must be exact origins such as "https://app.example.com" or "capacitor://localhost", got "${origin}".`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
47
55
|
const existing = nuxt.options.runtimeConfig.oauthServer;
|
|
48
56
|
nuxt.options.runtimeConfig.oauthServer = {
|
|
49
57
|
privateKey: "",
|
|
@@ -57,6 +65,7 @@ const module$1 = defineNuxtModule({
|
|
|
57
65
|
codeTtl: options.codeTtl ?? 600,
|
|
58
66
|
callbackPath: options.callbackPath ?? "/auth/frigear",
|
|
59
67
|
scopes: options.scopes ?? [],
|
|
68
|
+
corsOrigins,
|
|
60
69
|
authorizePath,
|
|
61
70
|
signInPath: settings.signInPath
|
|
62
71
|
};
|
|
@@ -71,24 +80,34 @@ const module$1 = defineNuxtModule({
|
|
|
71
80
|
resolver,
|
|
72
81
|
schema: (dialect) => `./runtime/server/db/schema.${dialect}`
|
|
73
82
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
addServerHandler({ middleware: true, handler: resolver.resolve("./runtime/server/middleware/oauth-cors") });
|
|
84
|
+
addModuleRoutes(resolver, [
|
|
85
|
+
{ method: "get", path: oidc.configuration, file: "oidc/openid-configuration.get" },
|
|
86
|
+
{ method: "get", path: oidc.jwks, file: "oidc/jwks.get" },
|
|
87
|
+
{ method: "get", path: oidc.authorize, file: "oidc/authorize.get" },
|
|
88
|
+
{ method: "post", path: oidc.authorize, file: "oidc/authorize.post" },
|
|
89
|
+
{ method: "post", path: oidc.token, file: "oidc/token.post" },
|
|
90
|
+
{ method: "post", path: oidc.revoke, file: "oidc/revoke.post" },
|
|
91
|
+
{ method: "get", path: oidc.userinfo, file: "oidc/userinfo.get" }
|
|
92
|
+
], { base: "/" });
|
|
93
|
+
addModuleRoutes(resolver, [
|
|
94
|
+
{ method: "get", path: "consent", file: "api/consent.get" },
|
|
95
|
+
{ method: "get", path: "me/applications", file: "api/me-applications.get" }
|
|
96
|
+
], { base: "/api/oauth-server" });
|
|
97
|
+
registerBootstrapProvider(nuxt, {
|
|
98
|
+
scope: "account",
|
|
99
|
+
key: "oauth-server:mine",
|
|
100
|
+
handler: resolver.resolve("./runtime/server/bootstrap/my-applications")
|
|
101
|
+
});
|
|
102
|
+
addModuleRoutes(resolver, [
|
|
103
|
+
{ method: "get", path: "applications", file: "api/admin/applications.get" },
|
|
104
|
+
{ method: "post", path: "applications", file: "api/admin/applications.post" },
|
|
105
|
+
{ method: "get", path: "applications/:id", file: "api/admin/application.get" },
|
|
106
|
+
{ method: "patch", path: "applications/:id", file: "api/admin/application.patch" },
|
|
107
|
+
{ method: "delete", path: "applications/:id", file: "api/admin/application.delete" },
|
|
108
|
+
{ method: "post", path: "applications/:id/secret", file: "api/admin/application-secret.post" },
|
|
109
|
+
{ method: "post", path: "applications/:id/revoke", file: "api/admin/application-revoke.post" }
|
|
110
|
+
], { base: "/api/admin/oauth-server" });
|
|
92
111
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
93
112
|
inlineModuleRuntime(nuxt, resolver);
|
|
94
113
|
if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { isNativeRedirectUri, isSecureOrigin } from "../../utils";
|
|
3
4
|
defineOptions({ name: "OAuthApplicationSlideover" });
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
application: { type: Object, required: false },
|
|
@@ -8,7 +9,14 @@ const props = defineProps({
|
|
|
8
9
|
const emit = defineEmits(["close"]);
|
|
9
10
|
const t = useModuleT("oauthServer");
|
|
10
11
|
const { busy, run } = useAction();
|
|
12
|
+
const isAllowedRedirect = (value, clientType) => {
|
|
13
|
+
if (!URL.canParse(value)) return false;
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return !url.hash && (isSecureOrigin(url) || clientType === "public" && isNativeRedirectUri(url));
|
|
16
|
+
};
|
|
11
17
|
const schema = z.object({
|
|
18
|
+
clientType: z.enum(["confidential", "public"]),
|
|
19
|
+
redirectUris: z.array(z.string()),
|
|
12
20
|
name: z.string().trim().min(1, t("form.nameRequired", "Enter a name.")),
|
|
13
21
|
websiteUrl: z.url(t("form.websiteUrlInvalid", "Enter a full URL, for example https://app.example.com.")),
|
|
14
22
|
loginUrl: z.string(),
|
|
@@ -20,6 +28,8 @@ const schema = z.object({
|
|
|
20
28
|
tags: z.array(z.string())
|
|
21
29
|
});
|
|
22
30
|
const state = reactive({
|
|
31
|
+
clientType: props.application?.clientType ?? "confidential",
|
|
32
|
+
redirectUris: [...props.application?.redirectUris ?? []],
|
|
23
33
|
name: props.application?.name ?? "",
|
|
24
34
|
websiteUrl: props.application?.websiteUrl ?? "",
|
|
25
35
|
loginUrl: props.application?.loginUrl ?? "",
|
|
@@ -31,9 +41,17 @@ const state = reactive({
|
|
|
31
41
|
tags: [...props.application?.tags ?? []]
|
|
32
42
|
});
|
|
33
43
|
const formId = "oauth-application-form";
|
|
44
|
+
const clientTypeItems = computed(() => [
|
|
45
|
+
{ label: t("form.clientTypeConfidential", "Confidential (server-side, has a client secret)"), value: "confidential" },
|
|
46
|
+
{ label: t("form.clientTypePublic", "Public (native or single-page app, no secret)"), value: "public" }
|
|
47
|
+
]);
|
|
48
|
+
const redirectUriError = computed(() => state.redirectUris.find((uri) => !isAllowedRedirect(uri, state.clientType)));
|
|
34
49
|
async function onSubmit(event) {
|
|
35
50
|
const value = event.data;
|
|
51
|
+
if (redirectUriError.value) return;
|
|
36
52
|
const saved = await run(() => props.save({
|
|
53
|
+
clientType: value.clientType,
|
|
54
|
+
redirectUris: value.redirectUris,
|
|
37
55
|
name: value.name,
|
|
38
56
|
websiteUrl: value.websiteUrl,
|
|
39
57
|
loginUrl: value.loginUrl || null,
|
|
@@ -64,6 +82,18 @@ async function onSubmit(event) {
|
|
|
64
82
|
class="grid gap-4"
|
|
65
83
|
@submit="onSubmit"
|
|
66
84
|
>
|
|
85
|
+
<UFormField
|
|
86
|
+
name="clientType"
|
|
87
|
+
:label="t('form.clientType', 'Client type')"
|
|
88
|
+
:description="t('form.clientTypeHint', 'Cannot be changed later. Public clients sign in with PKCE only, so they never get a secret.')"
|
|
89
|
+
>
|
|
90
|
+
<USelect
|
|
91
|
+
v-model="state.clientType"
|
|
92
|
+
:items="clientTypeItems"
|
|
93
|
+
:disabled="!!application"
|
|
94
|
+
class="w-full"
|
|
95
|
+
/>
|
|
96
|
+
</UFormField>
|
|
67
97
|
<UFormField
|
|
68
98
|
name="name"
|
|
69
99
|
:label="t('form.name', 'Name')"
|
|
@@ -106,6 +136,17 @@ async function onSubmit(event) {
|
|
|
106
136
|
class="w-full"
|
|
107
137
|
/>
|
|
108
138
|
</UFormField>
|
|
139
|
+
<UFormField
|
|
140
|
+
name="redirectUris"
|
|
141
|
+
:label="t('form.redirectUris', 'Redirect URIs')"
|
|
142
|
+
:description="t('form.redirectUrisHint', 'Extra exact redirect URIs. Public clients may use custom-scheme URIs such as com.example.app:/callback.')"
|
|
143
|
+
:error="redirectUriError ? t('form.redirectUriInvalid', 'Enter a https URL or, for public clients, a custom-scheme URI.') : void 0"
|
|
144
|
+
>
|
|
145
|
+
<UInputTags
|
|
146
|
+
v-model="state.redirectUris"
|
|
147
|
+
class="w-full"
|
|
148
|
+
/>
|
|
149
|
+
</UFormField>
|
|
109
150
|
<UFormField
|
|
110
151
|
name="allowedRoles"
|
|
111
152
|
:label="t('form.allowedRoles', 'Allowed roles')"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
name: string;
|
|
4
4
|
clientId: string;
|
|
5
|
-
|
|
5
|
+
/** `null` for a public client: it has no secret. */
|
|
6
|
+
clientSecret: string | null;
|
|
6
7
|
};
|
|
7
8
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
9
|
declare const _default: typeof __VLS_export;
|
|
@@ -3,7 +3,7 @@ defineOptions({ name: "OAuthCredentialsModal" });
|
|
|
3
3
|
defineProps({
|
|
4
4
|
name: { type: String, required: true },
|
|
5
5
|
clientId: { type: String, required: true },
|
|
6
|
-
clientSecret: { type: String, required: true }
|
|
6
|
+
clientSecret: { type: [String, null], required: true }
|
|
7
7
|
});
|
|
8
8
|
const t = useModuleT("oauthServer");
|
|
9
9
|
const { copy } = useCopyToClipboard();
|
|
@@ -12,7 +12,7 @@ const { copy } = useCopyToClipboard();
|
|
|
12
12
|
<template>
|
|
13
13
|
<UModal
|
|
14
14
|
:title="t('credentials.title', 'Credentials for {name}', { name })"
|
|
15
|
-
:description="t('credentials.description', 'Copy the client secret now. It is only shown once.')"
|
|
15
|
+
:description="clientSecret ? t('credentials.description', 'Copy the client secret now. It is only shown once.') : t('credentials.publicDescription', 'This is a public client: it has no client secret and signs in with PKCE only.')"
|
|
16
16
|
>
|
|
17
17
|
<template #body>
|
|
18
18
|
<DescriptionList
|
|
@@ -35,7 +35,14 @@ const { copy } = useCopyToClipboard();
|
|
|
35
35
|
</div>
|
|
36
36
|
</template>
|
|
37
37
|
<template #clientSecret>
|
|
38
|
-
<
|
|
38
|
+
<span
|
|
39
|
+
v-if="!clientSecret"
|
|
40
|
+
class="text-muted"
|
|
41
|
+
>{{ t("credentials.noSecret", "None (public client)") }}</span>
|
|
42
|
+
<div
|
|
43
|
+
v-else
|
|
44
|
+
class="flex items-center gap-2"
|
|
45
|
+
>
|
|
39
46
|
<code class="min-w-0 break-all">{{ clientSecret }}</code>
|
|
40
47
|
<UButton
|
|
41
48
|
size="xs"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
name: string;
|
|
4
4
|
clientId: string;
|
|
5
|
-
|
|
5
|
+
/** `null` for a public client: it has no secret. */
|
|
6
|
+
clientSecret: string | null;
|
|
6
7
|
};
|
|
7
8
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
9
|
declare const _default: typeof __VLS_export;
|
|
@@ -5,6 +5,9 @@ type SavedApplication = AdminApplicationDto | CreatedApplicationDto | false | un
|
|
|
5
5
|
* Everything you can do to an application: `create` / `edit` (a `FormSlideover`), `remove`, `toggle`, `rotateSecret`,
|
|
6
6
|
* `signOutEveryone`, `copyClientId` and `menu(application)` for the row menu. Toasts, confirmations and the refresh
|
|
7
7
|
* of the admin list are handled here.
|
|
8
|
+
*
|
|
9
|
+
* `edit` owns the `?application=<id>` query parameter of the page it is opened from: it is set while the form is open
|
|
10
|
+
* and cleared when it closes, so an edit can be linked to and survives a reload (the admin page opens it again).
|
|
8
11
|
*/
|
|
9
12
|
export declare function useOAuthApplicationActions(): {
|
|
10
13
|
busy: any;
|
|
@@ -8,7 +8,14 @@ export function useOAuthApplicationActions() {
|
|
|
8
8
|
const toast = useToast();
|
|
9
9
|
const { busy, run } = useAction();
|
|
10
10
|
const { copy } = useCopyToClipboard();
|
|
11
|
+
const route = useRoute();
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const editing = ref(null);
|
|
11
14
|
const refresh = () => refreshNuxtData(OAUTH_APPLICATIONS_KEY);
|
|
15
|
+
function setQuery(id) {
|
|
16
|
+
const { application: _previous, ...rest } = route.query;
|
|
17
|
+
return router.replace({ query: id ? { ...rest, application: id } : rest });
|
|
18
|
+
}
|
|
12
19
|
function showCredentials(credentials) {
|
|
13
20
|
overlay.create(OAuthCredentialsModal, { destroyOnClose: true, props: credentials }).open();
|
|
14
21
|
}
|
|
@@ -30,19 +37,28 @@ export function useOAuthApplicationActions() {
|
|
|
30
37
|
return saved;
|
|
31
38
|
}
|
|
32
39
|
async function edit(application) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
if (editing.value === application.id) return void 0;
|
|
41
|
+
editing.value = application.id;
|
|
42
|
+
const path = route.path;
|
|
43
|
+
await setQuery(application.id);
|
|
44
|
+
try {
|
|
45
|
+
const saved = await overlay.create(OAuthApplicationSlideover, {
|
|
46
|
+
destroyOnClose: true,
|
|
47
|
+
props: {
|
|
48
|
+
application,
|
|
49
|
+
save: async (input) => {
|
|
50
|
+
const updated = await oauthApplicationsApi.update(application.id, input);
|
|
51
|
+
await refresh();
|
|
52
|
+
return updated;
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
}).open();
|
|
56
|
+
if (saved) toast.add({ title: t("admin.updated", "Application updated"), description: saved.name, color: "success", icon: "i-lucide-check" });
|
|
57
|
+
return saved;
|
|
58
|
+
} finally {
|
|
59
|
+
editing.value = null;
|
|
60
|
+
if (route.path === path && route.query.application === application.id) await setQuery(null);
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
async function remove(application) {
|
|
48
64
|
const confirmed = await confirm({
|
|
@@ -108,14 +124,15 @@ export function useOAuthApplicationActions() {
|
|
|
108
124
|
void toggle(application);
|
|
109
125
|
}
|
|
110
126
|
},
|
|
111
|
-
|
|
127
|
+
// Public clients have no secret to replace.
|
|
128
|
+
...application.clientType === "public" ? [] : [{ label: t("admin.newSecret", "New client secret"), icon: "i-lucide-key-round", onSelect: () => {
|
|
112
129
|
void rotateSecret(application);
|
|
113
|
-
} },
|
|
130
|
+
} }],
|
|
114
131
|
{ label: t("admin.signOutEveryone", "Sign everyone out"), icon: "i-lucide-log-out", onSelect: () => {
|
|
115
132
|
void signOutEveryone(application);
|
|
116
133
|
} }
|
|
117
134
|
], [
|
|
118
|
-
{ label: t("admin.delete", "Delete application"), icon: "i-lucide-trash", color: "error", onSelect: () => {
|
|
135
|
+
{ label: t("admin.delete", "Delete application"), icon: "i-lucide-trash-2", color: "error", onSelect: () => {
|
|
119
136
|
void remove(application);
|
|
120
137
|
} }
|
|
121
138
|
]];
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
1
2
|
import type { AdminApplicationDto, ApplicationInput, CreatedApplicationDto } from '../../../types.js';
|
|
2
3
|
/** The `useFetch` key of the admin list; `refreshNuxtData(OAUTH_APPLICATIONS_KEY)` reloads it from anywhere. */
|
|
3
4
|
export declare const OAUTH_APPLICATIONS_KEY = "oauth-server:applications";
|
|
4
5
|
/** The admin endpoints as plain calls (no refresh). Every call needs the host's `oauth:authorize` hook to allow the user. */
|
|
5
6
|
export declare const oauthApplicationsApi: {
|
|
7
|
+
get: (id: string) => Promise<AdminApplicationDto>;
|
|
6
8
|
create: (input: ApplicationInput) => Promise<CreatedApplicationDto>;
|
|
7
9
|
update: (id: string, patch: Partial<ApplicationInput>) => Promise<AdminApplicationDto>;
|
|
8
|
-
|
|
10
|
+
/** 204: resolves with `null`. */
|
|
11
|
+
remove: (id: string) => Promise<null>;
|
|
9
12
|
/** Returns the new client secret; the old one stops working. */
|
|
10
13
|
rotateSecret: (id: string) => Promise<string>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/** Admin CRUD for applications. Every call needs the host's `oauth:authorize` hook to allow the user. */
|
|
14
|
-
export declare function useOAuthApplications(): {
|
|
15
|
-
applications: any;
|
|
16
|
-
status: any;
|
|
17
|
-
error: any;
|
|
18
|
-
refresh: any;
|
|
19
|
-
create: (input: ApplicationInput) => Promise<CreatedApplicationDto>;
|
|
20
|
-
update: (id: string, patch: Partial<ApplicationInput>) => Promise<AdminApplicationDto>;
|
|
21
|
-
remove: (id: string) => Promise<void>;
|
|
22
|
-
rotateSecret: (id: string) => Promise<string>;
|
|
23
|
-
revokeGrants: (id: string) => Promise<void>;
|
|
24
|
-
};
|
|
25
|
-
/** The applications the signed-in user may sign in to. */
|
|
26
|
-
export declare function useMyApplications(): {
|
|
27
|
-
applications: any;
|
|
28
|
-
status: any;
|
|
29
|
-
refresh: any;
|
|
14
|
+
/** Signs everyone out of the application; resolves with the application. */
|
|
15
|
+
revokeGrants: (id: string) => Promise<AdminApplicationDto>;
|
|
30
16
|
};
|
|
17
|
+
/** The list query: `page`, `pageSize`, `q` (name or client id), `sort` (`createdAt`, `name`, `priority`) and `order`. */
|
|
18
|
+
export type OAuthApplicationsQuery = Record<string, string | number | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Admin CRUD for applications, with one page of the list. Awaitable: `await useOAuthApplications(query)` in a page
|
|
21
|
+
* renders the list on the server; pass `useTableQuery().query` to page, search and sort it. `applications` is `[]`
|
|
22
|
+
* until loaded and after an error: check `loading` and `error` first. Every call needs the host's `oauth:authorize`
|
|
23
|
+
* hook to allow the user.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useOAuthApplications(query?: MaybeRefOrGetter<OAuthApplicationsQuery>): any;
|
|
26
|
+
/**
|
|
27
|
+
* The applications the signed-in user may sign in to. Awaitable: `await useMyApplications()` in a page renders them on
|
|
28
|
+
* the server. `applications` is `[]` until loaded and after an error: check `loading` and `error` first.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useMyApplications(): any;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import { oauthServerApi } from "../../utils/api.js";
|
|
2
2
|
export const OAUTH_APPLICATIONS_KEY = "oauth-server:applications";
|
|
3
3
|
export const oauthApplicationsApi = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
get: (id) => $fetch(oauthServerApi.admin.application(id)),
|
|
5
|
+
create: (input) => $fetch(oauthServerApi.admin.applications, { method: "POST", body: input }),
|
|
6
|
+
update: (id, patch) => $fetch(oauthServerApi.admin.application(id), { method: "PATCH", body: patch }),
|
|
7
|
+
/** 204: resolves with `null`. */
|
|
8
|
+
remove: async (id) => {
|
|
9
|
+
await $fetch(oauthServerApi.admin.application(id), { method: "DELETE" });
|
|
10
|
+
return null;
|
|
11
|
+
},
|
|
7
12
|
/** Returns the new client secret; the old one stops working. */
|
|
8
|
-
rotateSecret: async (id) => (await $fetch(
|
|
9
|
-
|
|
13
|
+
rotateSecret: async (id) => (await $fetch(oauthServerApi.admin.rotateSecret(id), { method: "POST" })).clientSecret,
|
|
14
|
+
/** Signs everyone out of the application; resolves with the application. */
|
|
15
|
+
revokeGrants: (id) => $fetch(oauthServerApi.admin.revoke(id), { method: "POST" })
|
|
10
16
|
};
|
|
11
|
-
export function useOAuthApplications() {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
export function useOAuthApplications(query = {}) {
|
|
18
|
+
const fetched = useFetch(oauthServerApi.admin.applications, { key: OAUTH_APPLICATIONS_KEY, query });
|
|
19
|
+
const { data, status, error, refresh } = fetched;
|
|
20
|
+
const applications = computed(() => data.value?.items ?? []);
|
|
21
|
+
const total = computed(() => data.value?.total ?? 0);
|
|
22
|
+
const loading = computed(() => status.value === "pending" && !data.value);
|
|
23
|
+
const refreshing = computed(() => status.value === "pending" && !!data.value);
|
|
16
24
|
async function create(input) {
|
|
17
25
|
const created = await oauthApplicationsApi.create(input);
|
|
18
26
|
await refresh();
|
|
@@ -26,6 +34,7 @@ export function useOAuthApplications() {
|
|
|
26
34
|
async function remove(id) {
|
|
27
35
|
await oauthApplicationsApi.remove(id);
|
|
28
36
|
await refresh();
|
|
37
|
+
return null;
|
|
29
38
|
}
|
|
30
39
|
async function rotateSecret(id) {
|
|
31
40
|
const clientSecret = await oauthApplicationsApi.rotateSecret(id);
|
|
@@ -33,18 +42,26 @@ export function useOAuthApplications() {
|
|
|
33
42
|
return clientSecret;
|
|
34
43
|
}
|
|
35
44
|
async function revokeGrants(id) {
|
|
36
|
-
await oauthApplicationsApi.revokeGrants(id);
|
|
45
|
+
const application = await oauthApplicationsApi.revokeGrants(id);
|
|
37
46
|
await refresh();
|
|
47
|
+
return application;
|
|
38
48
|
}
|
|
39
|
-
|
|
49
|
+
const api = { applications, total, data, status, error, loading, refreshing, refresh, create, update, remove, rotateSecret, revokeGrants };
|
|
50
|
+
return Object.assign(fetched.then(() => api), api);
|
|
40
51
|
}
|
|
41
52
|
export function useMyApplications() {
|
|
42
53
|
const { loggedIn } = useUserSession();
|
|
43
|
-
const
|
|
54
|
+
const fetched = useCachedFetch(oauthServerApi.me.applications, {
|
|
44
55
|
key: "oauth-server:mine",
|
|
45
|
-
|
|
46
|
-
watch: false
|
|
47
|
-
|
|
56
|
+
ttl: 5 * 60 * 1e3,
|
|
57
|
+
watch: false
|
|
58
|
+
});
|
|
59
|
+
const { status, error, loading, refresh, clear } = fetched;
|
|
60
|
+
watch(loggedIn, (value) => {
|
|
61
|
+
if (value) refresh();
|
|
62
|
+
else clear();
|
|
48
63
|
});
|
|
49
|
-
|
|
64
|
+
const applications = computed(() => fetched.data.value ?? []);
|
|
65
|
+
const api = { applications, status, error, loading, refresh };
|
|
66
|
+
return Object.assign(fetched.then(() => api), api);
|
|
50
67
|
}
|