@frigear-nu/oauth-server 0.1.2 → 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 +8 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +24 -20
- package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +3 -0
- package/dist/runtime/app/composables/useOAuthApplicationActions.js +29 -13
- package/dist/runtime/app/composables/useOAuthApplications.d.ts +15 -14
- package/dist/runtime/app/composables/useOAuthApplications.js +25 -14
- package/dist/runtime/app/pages/admin.vue +113 -62
- package/dist/runtime/app/pages/applications.vue +51 -34
- package/dist/runtime/app/pages/authorize.vue +3 -2
- package/dist/runtime/i18n/lang/da.json +6 -1
- package/dist/runtime/i18n/lang/en.json +6 -1
- package/dist/runtime/server/middleware/oauth-cors.js +6 -5
- 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/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 +6 -5
- package/dist/runtime/server/routes/oidc/revoke.post.js +15 -3
- package/dist/runtime/server/routes/oidc/token.post.js +20 -5
- 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 +7 -2
- package/dist/runtime/server/utils/oauth-applications.js +39 -12
- package/dist/runtime/server/utils/oauth-authorize.d.ts +8 -4
- package/dist/runtime/server/utils/oauth-authorize.js +10 -8
- package/dist/runtime/server/utils/oauth-config.d.ts +5 -0
- package/dist/runtime/server/utils/oauth-config.js +5 -0
- 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 +1 -0
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +14 -9
package/dist/module.d.mts
CHANGED
|
@@ -175,6 +175,13 @@ type AdminApplicationDto = Omit<OAuthApplication, 'createdAt' | 'updatedAt'> & {
|
|
|
175
175
|
/** The redirect URI the application has to accept. */
|
|
176
176
|
callbackUrl: string;
|
|
177
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
|
+
};
|
|
178
185
|
/** Returned once when an application is created. */
|
|
179
186
|
type CreatedApplicationDto = AdminApplicationDto & {
|
|
180
187
|
clientId: string;
|
|
@@ -221,4 +228,4 @@ declare module 'nitropack' {
|
|
|
221
228
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
222
229
|
|
|
223
230
|
export { _default as default };
|
|
224
|
-
export type { AdminApplicationDto, ApplicationAccessContext, ApplicationInput, AuthorizeContext, ClaimsContext, ClientType, 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, registerBootstrapProvider, 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: {
|
|
@@ -81,29 +81,33 @@ const module$1 = defineNuxtModule({
|
|
|
81
81
|
schema: (dialect) => `./runtime/server/db/schema.${dialect}`
|
|
82
82
|
});
|
|
83
83
|
addServerHandler({ middleware: true, handler: resolver.resolve("./runtime/server/middleware/oauth-cors") });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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" });
|
|
95
97
|
registerBootstrapProvider(nuxt, {
|
|
96
98
|
scope: "account",
|
|
97
99
|
key: "oauth-server:mine",
|
|
98
100
|
handler: resolver.resolve("./runtime/server/bootstrap/my-applications")
|
|
99
101
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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" });
|
|
107
111
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
108
112
|
inlineModuleRuntime(nuxt, resolver);
|
|
109
113
|
if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
@@ -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({
|
|
@@ -116,7 +132,7 @@ export function useOAuthApplicationActions() {
|
|
|
116
132
|
void signOutEveryone(application);
|
|
117
133
|
} }
|
|
118
134
|
], [
|
|
119
|
-
{ 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: () => {
|
|
120
136
|
void remove(application);
|
|
121
137
|
} }
|
|
122
138
|
]];
|
|
@@ -1,27 +1,28 @@
|
|
|
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>;
|
|
14
|
+
/** Signs everyone out of the application; resolves with the application. */
|
|
15
|
+
revokeGrants: (id: string) => Promise<AdminApplicationDto>;
|
|
24
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;
|
|
25
26
|
/**
|
|
26
27
|
* The applications the signed-in user may sign in to. Awaitable: `await useMyApplications()` in a page renders them on
|
|
27
28
|
* the server. `applications` is `[]` until loaded and after an error: check `loading` and `error` first.
|
|
@@ -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,14 +42,16 @@ 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 fetched = useCachedFetch(
|
|
54
|
+
const fetched = useCachedFetch(oauthServerApi.me.applications, {
|
|
44
55
|
key: "oauth-server:mine",
|
|
45
56
|
ttl: 5 * 60 * 1e3,
|
|
46
57
|
watch: false
|
|
@@ -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,76 +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">
|
|
57
111
|
<UBadge
|
|
58
|
-
v-
|
|
59
|
-
|
|
112
|
+
v-for="role in row.original.allowedRoles"
|
|
113
|
+
:key="role"
|
|
60
114
|
variant="subtle"
|
|
61
|
-
|
|
62
|
-
:label="t('admin.public', 'Public')"
|
|
115
|
+
:label="role"
|
|
63
116
|
/>
|
|
64
117
|
<UBadge
|
|
65
|
-
v-if="!row.original.
|
|
118
|
+
v-if="!row.original.allowedRoles.length"
|
|
66
119
|
color="neutral"
|
|
67
120
|
variant="subtle"
|
|
68
|
-
|
|
69
|
-
:label="t('admin.disabled', 'Disabled')"
|
|
121
|
+
:label="t('admin.everyone', 'Every signed-in user')"
|
|
70
122
|
/>
|
|
71
|
-
</
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
: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"
|
|
81
132
|
/>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 }]"
|
|
87
141
|
/>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
:title="t('admin.emptyTitle', 'No applications yet')"
|
|
104
|
-
:description="t('admin.emptyDescription', 'Add an application to let users sign in to it with their account.')"
|
|
105
|
-
: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"
|
|
106
157
|
/>
|
|
107
158
|
</template>
|
|
108
|
-
</
|
|
109
|
-
</
|
|
159
|
+
</TableCard>
|
|
160
|
+
</template>
|
|
110
161
|
</div>
|
|
111
162
|
</template>
|
|
@@ -6,17 +6,17 @@ const { applications, loading, error, refresh } = await useMyApplications();
|
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
8
|
<div class="flex flex-col gap-4">
|
|
9
|
-
|
|
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
|
|
10
16
|
v-if="loading"
|
|
11
|
-
|
|
17
|
+
loading
|
|
12
18
|
aria-busy="true"
|
|
13
|
-
|
|
14
|
-
<SectionCard
|
|
15
|
-
v-for="index in 2"
|
|
16
|
-
:key="index"
|
|
17
|
-
loading
|
|
18
|
-
/>
|
|
19
|
-
</div>
|
|
19
|
+
/>
|
|
20
20
|
<UEmpty
|
|
21
21
|
v-else-if="error && !applications.length"
|
|
22
22
|
icon="i-lucide-circle-alert"
|
|
@@ -24,33 +24,50 @@ const { applications, loading, error, refresh } = await useMyApplications();
|
|
|
24
24
|
:description="errorMessage(error, '')"
|
|
25
25
|
:actions="[{ label: t('applications.tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
26
26
|
/>
|
|
27
|
-
<
|
|
28
|
-
v-else-if="!applications.length"
|
|
29
|
-
icon="i-lucide-layout-grid"
|
|
30
|
-
:title="t('applications.emptyTitle', 'No applications yet')"
|
|
31
|
-
:description="t('applications.emptyDescription', 'There are no applications you can sign in to yet.')"
|
|
32
|
-
/>
|
|
33
|
-
<div
|
|
27
|
+
<SectionCard
|
|
34
28
|
v-else
|
|
35
|
-
|
|
29
|
+
flush
|
|
36
30
|
>
|
|
37
|
-
<
|
|
38
|
-
v-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
:title="
|
|
42
|
-
:description="
|
|
43
|
-
|
|
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"
|
|
44
41
|
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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>
|
|
55
72
|
</div>
|
|
56
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
|
});
|