@meistrari/auth-nuxt 1.1.0 → 2.0.0
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 +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -1
- package/dist/runtime/components/tela-role.d.vue.ts +23 -0
- package/dist/runtime/components/tela-role.vue +15 -0
- package/dist/runtime/components/tela-role.vue.d.ts +23 -0
- package/dist/runtime/composables/application-auth.d.ts +2 -30
- package/dist/runtime/composables/state.d.ts +5 -86
- package/dist/runtime/pages/callback.d.vue.ts +2 -0
- package/dist/runtime/pages/callback.vue +27 -0
- package/dist/runtime/pages/callback.vue.d.ts +2 -0
- package/dist/runtime/plugins/auth-guard.d.ts +22 -0
- package/dist/runtime/plugins/auth-guard.js +39 -0
- package/dist/runtime/plugins/directives.d.ts +20 -0
- package/dist/runtime/plugins/directives.js +63 -0
- package/dist/runtime/server/middleware/application-auth.d.ts +4 -0
- package/dist/runtime/server/middleware/application-auth.js +49 -0
- package/dist/runtime/server/types/h3.d.ts +2 -0
- package/dist/runtime/server/utils/require-auth.d.ts +27 -0
- package/dist/runtime/server/utils/require-auth.js +69 -0
- package/dist/runtime/shared.d.ts +1 -2
- package/dist/runtime/types/page-meta.d.ts +15 -0
- package/package.json +51 -49
package/dist/module.d.mts
CHANGED
|
@@ -16,6 +16,10 @@ interface ModuleOptions {
|
|
|
16
16
|
applicationId: string;
|
|
17
17
|
/** The redirect URI to redirect to after authentication. Must be registered in the application's settings. */
|
|
18
18
|
redirectUri: string;
|
|
19
|
+
/** Path to redirect to when authentication is required (default: '/login') */
|
|
20
|
+
loginPath?: string;
|
|
21
|
+
/** Path to redirect to when user lacks required role (default: '/unauthorized') */
|
|
22
|
+
unauthorizedPath?: string;
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
25
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addImports, addPlugin, addServerHandler } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, addImports, addPlugin, extendPages, addComponent, addServerImportsDir, addServerHandler } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const module$1 = defineNuxtModule({
|
|
4
4
|
meta: {
|
|
@@ -19,6 +19,33 @@ const module$1 = defineNuxtModule({
|
|
|
19
19
|
from: resolver.resolve("runtime/composables/application-auth")
|
|
20
20
|
});
|
|
21
21
|
addPlugin(resolver.resolve("./runtime/plugins/application-token-refresh"));
|
|
22
|
+
extendPages((pages) => {
|
|
23
|
+
pages.unshift({
|
|
24
|
+
name: "auth-callback",
|
|
25
|
+
path: "/auth/callback",
|
|
26
|
+
file: resolver.resolve("./runtime/pages/callback.vue")
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
addComponent({
|
|
30
|
+
name: "TelaRole",
|
|
31
|
+
filePath: resolver.resolve("./runtime/components/tela-role.vue"),
|
|
32
|
+
global: true,
|
|
33
|
+
export: "default"
|
|
34
|
+
});
|
|
35
|
+
nuxt.hook("prepare:types", ({ references }) => {
|
|
36
|
+
references.push({
|
|
37
|
+
path: resolver.resolve("./runtime/types/page-meta.d.ts")
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
addPlugin(resolver.resolve("./runtime/plugins/auth-guard"));
|
|
41
|
+
addPlugin(resolver.resolve("./runtime/plugins/directives"));
|
|
42
|
+
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
43
|
+
if (!options.skipServerMiddleware) {
|
|
44
|
+
addServerHandler({
|
|
45
|
+
route: "",
|
|
46
|
+
handler: resolver.resolve("./runtime/server/middleware/application-auth")
|
|
47
|
+
});
|
|
48
|
+
}
|
|
22
49
|
return;
|
|
23
50
|
}
|
|
24
51
|
if (!options.skipServerMiddleware) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
allowedRoles: string[];
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_1: {
|
|
5
|
+
role: any;
|
|
6
|
+
}, __VLS_3: {
|
|
7
|
+
role: any;
|
|
8
|
+
}, __VLS_5: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
default?: (props: typeof __VLS_1) => any;
|
|
11
|
+
} & {
|
|
12
|
+
'not-authorized'?: (props: typeof __VLS_3) => any;
|
|
13
|
+
} & {
|
|
14
|
+
'logged-out'?: (props: typeof __VLS_5) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_component: 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>;
|
|
17
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
18
|
+
export default _default;
|
|
19
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
20
|
+
new (): {
|
|
21
|
+
$slots: S;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useTelaApplicationAuth } from "../composables/application-auth";
|
|
3
|
+
const { user } = useTelaApplicationAuth();
|
|
4
|
+
defineProps({
|
|
5
|
+
allowedRoles: { type: Array, required: true }
|
|
6
|
+
});
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<template v-if="user">
|
|
11
|
+
<slot v-if="user.role && allowedRoles.includes(user.role)" :role="user.role" />
|
|
12
|
+
<slot v-else name="not-authorized" :role="user.role" />
|
|
13
|
+
</template>
|
|
14
|
+
<slot v-else name="logged-out" />
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
allowedRoles: string[];
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_1: {
|
|
5
|
+
role: any;
|
|
6
|
+
}, __VLS_3: {
|
|
7
|
+
role: any;
|
|
8
|
+
}, __VLS_5: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
default?: (props: typeof __VLS_1) => any;
|
|
11
|
+
} & {
|
|
12
|
+
'not-authorized'?: (props: typeof __VLS_3) => any;
|
|
13
|
+
} & {
|
|
14
|
+
'logged-out'?: (props: typeof __VLS_5) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_component: 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>;
|
|
17
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
18
|
+
export default _default;
|
|
19
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
20
|
+
new (): {
|
|
21
|
+
$slots: S;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -36,34 +36,6 @@ export declare function useTelaApplicationAuth(): {
|
|
|
36
36
|
login: () => Promise<void>;
|
|
37
37
|
logout: () => Promise<void>;
|
|
38
38
|
initSession: () => Promise<void>;
|
|
39
|
-
user: import("vue").Ref<
|
|
40
|
-
|
|
41
|
-
createdAt: Date;
|
|
42
|
-
updatedAt: Date;
|
|
43
|
-
email: string;
|
|
44
|
-
emailVerified: boolean;
|
|
45
|
-
name: string;
|
|
46
|
-
image?: string | null | undefined;
|
|
47
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
48
|
-
banned: boolean | null | undefined;
|
|
49
|
-
role?: string | null | undefined;
|
|
50
|
-
banReason?: string | null | undefined;
|
|
51
|
-
banExpires?: Date | null | undefined;
|
|
52
|
-
lastActiveAt?: Date | null | undefined;
|
|
53
|
-
} | null, {
|
|
54
|
-
id: string;
|
|
55
|
-
createdAt: Date;
|
|
56
|
-
updatedAt: Date;
|
|
57
|
-
email: string;
|
|
58
|
-
emailVerified: boolean;
|
|
59
|
-
name: string;
|
|
60
|
-
image?: string | null | undefined;
|
|
61
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
62
|
-
banned: boolean | null | undefined;
|
|
63
|
-
role?: string | null | undefined;
|
|
64
|
-
banReason?: string | null | undefined;
|
|
65
|
-
banExpires?: Date | null | undefined;
|
|
66
|
-
lastActiveAt?: Date | null | undefined;
|
|
67
|
-
} | null>;
|
|
68
|
-
activeOrganization: import("vue").Ref<Omit<import("@meistrari/auth-core").FullOrganization, "members" | "invitations" | "teams"> | null, Omit<import("@meistrari/auth-core").FullOrganization, "members" | "invitations" | "teams"> | null>;
|
|
39
|
+
user: import("vue").Ref<any, any>;
|
|
40
|
+
activeOrganization: import("vue").Ref<Omit<FullOrganization, "members" | "invitations" | "teams"> | null, Omit<FullOrganization, "members" | "invitations" | "teams"> | null>;
|
|
69
41
|
};
|
|
@@ -1,101 +1,20 @@
|
|
|
1
|
-
import type { FullOrganization, Member } from '@meistrari/auth-core';
|
|
2
1
|
/**
|
|
3
2
|
* Shared state for session management.
|
|
4
3
|
* This module provides access to session-related state without creating circular dependencies.
|
|
5
4
|
*/
|
|
6
5
|
export declare function useSessionState(): {
|
|
7
|
-
user: import("vue").Ref<
|
|
8
|
-
|
|
9
|
-
createdAt: Date;
|
|
10
|
-
updatedAt: Date;
|
|
11
|
-
email: string;
|
|
12
|
-
emailVerified: boolean;
|
|
13
|
-
name: string;
|
|
14
|
-
image?: string | null | undefined;
|
|
15
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
16
|
-
banned: boolean | null | undefined;
|
|
17
|
-
role?: string | null | undefined;
|
|
18
|
-
banReason?: string | null | undefined;
|
|
19
|
-
banExpires?: Date | null | undefined;
|
|
20
|
-
lastActiveAt?: Date | null | undefined;
|
|
21
|
-
} | null, {
|
|
22
|
-
id: string;
|
|
23
|
-
createdAt: Date;
|
|
24
|
-
updatedAt: Date;
|
|
25
|
-
email: string;
|
|
26
|
-
emailVerified: boolean;
|
|
27
|
-
name: string;
|
|
28
|
-
image?: string | null | undefined;
|
|
29
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
30
|
-
banned: boolean | null | undefined;
|
|
31
|
-
role?: string | null | undefined;
|
|
32
|
-
banReason?: string | null | undefined;
|
|
33
|
-
banExpires?: Date | null | undefined;
|
|
34
|
-
lastActiveAt?: Date | null | undefined;
|
|
35
|
-
} | null>;
|
|
36
|
-
session: import("vue").Ref<{
|
|
37
|
-
id: string;
|
|
38
|
-
createdAt: Date;
|
|
39
|
-
updatedAt: Date;
|
|
40
|
-
userId: string;
|
|
41
|
-
expiresAt: Date;
|
|
42
|
-
token: string;
|
|
43
|
-
ipAddress?: string | null | undefined;
|
|
44
|
-
userAgent?: string | null | undefined;
|
|
45
|
-
activeOrganizationId?: string | null | undefined;
|
|
46
|
-
activeTeamId?: string | null | undefined;
|
|
47
|
-
impersonatedBy?: string | null | undefined;
|
|
48
|
-
} | null, {
|
|
49
|
-
id: string;
|
|
50
|
-
createdAt: Date;
|
|
51
|
-
updatedAt: Date;
|
|
52
|
-
userId: string;
|
|
53
|
-
expiresAt: Date;
|
|
54
|
-
token: string;
|
|
55
|
-
ipAddress?: string | null | undefined;
|
|
56
|
-
userAgent?: string | null | undefined;
|
|
57
|
-
activeOrganizationId?: string | null | undefined;
|
|
58
|
-
activeTeamId?: string | null | undefined;
|
|
59
|
-
impersonatedBy?: string | null | undefined;
|
|
60
|
-
} | null>;
|
|
6
|
+
user: import("vue").Ref<any, any>;
|
|
7
|
+
session: import("vue").Ref<any, any>;
|
|
61
8
|
};
|
|
62
9
|
/**
|
|
63
10
|
* Shared state for organization management.
|
|
64
11
|
* This module provides access to organization-related state without creating circular dependencies.
|
|
65
12
|
*/
|
|
66
13
|
export declare function useOrganizationState(): {
|
|
67
|
-
activeOrganization: import("vue").Ref<
|
|
68
|
-
activeMember: import("vue").Ref<
|
|
14
|
+
activeOrganization: import("vue").Ref<any, any>;
|
|
15
|
+
activeMember: import("vue").Ref<any, any>;
|
|
69
16
|
};
|
|
70
17
|
export declare function useApplicationSessionState(): {
|
|
71
|
-
user: import("vue").Ref<
|
|
72
|
-
id: string;
|
|
73
|
-
createdAt: Date;
|
|
74
|
-
updatedAt: Date;
|
|
75
|
-
email: string;
|
|
76
|
-
emailVerified: boolean;
|
|
77
|
-
name: string;
|
|
78
|
-
image?: string | null | undefined;
|
|
79
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
80
|
-
banned: boolean | null | undefined;
|
|
81
|
-
role?: string | null | undefined;
|
|
82
|
-
banReason?: string | null | undefined;
|
|
83
|
-
banExpires?: Date | null | undefined;
|
|
84
|
-
lastActiveAt?: Date | null | undefined;
|
|
85
|
-
} | null, {
|
|
86
|
-
id: string;
|
|
87
|
-
createdAt: Date;
|
|
88
|
-
updatedAt: Date;
|
|
89
|
-
email: string;
|
|
90
|
-
emailVerified: boolean;
|
|
91
|
-
name: string;
|
|
92
|
-
image?: string | null | undefined;
|
|
93
|
-
twoFactorEnabled: boolean | null | undefined;
|
|
94
|
-
banned: boolean | null | undefined;
|
|
95
|
-
role?: string | null | undefined;
|
|
96
|
-
banReason?: string | null | undefined;
|
|
97
|
-
banExpires?: Date | null | undefined;
|
|
98
|
-
lastActiveAt?: Date | null | undefined;
|
|
99
|
-
} | null>;
|
|
18
|
+
user: import("vue").Ref<any, any>;
|
|
100
19
|
activeOrganization: import("vue").Ref<Omit<FullOrganization, "members" | "invitations" | "teams"> | null, Omit<FullOrganization, "members" | "invitations" | "teams"> | null>;
|
|
101
20
|
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { navigateTo, useRuntimeConfig } from "#app";
|
|
3
|
+
import { onMounted } from "vue";
|
|
4
|
+
import { useTelaApplicationAuth } from "../composables/application-auth";
|
|
5
|
+
const { initSession } = useTelaApplicationAuth();
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const authConfig = config.public.telaAuth;
|
|
8
|
+
const loginPath = authConfig.application?.loginPath ?? "/login";
|
|
9
|
+
onMounted(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await initSession();
|
|
12
|
+
navigateTo("/");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Session initialization failed:", error);
|
|
15
|
+
navigateTo({
|
|
16
|
+
path: loginPath,
|
|
17
|
+
query: { error: "session_failed" }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="flex items-center justify-center min-h-screen">
|
|
25
|
+
<h1>Loading...</h1>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side authentication guard plugin
|
|
3
|
+
*
|
|
4
|
+
* This plugin provides UX-level route protection by checking JWT tokens client-side
|
|
5
|
+
* and redirecting users before they navigate to protected routes. This creates a smooth
|
|
6
|
+
* user experience by avoiding unnecessary page loads.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY NOTE: This is NOT the primary security layer. The client-side JWT decoding
|
|
9
|
+
* is for UX purposes only. Actual cryptographic verification happens server-side in:
|
|
10
|
+
* - application-auth.ts: Server middleware that verifies JWTs for all /api routes
|
|
11
|
+
* - require-auth.ts: Server utility for protecting individual API route handlers
|
|
12
|
+
*
|
|
13
|
+
* An attacker could bypass this client-side check, but they cannot bypass the server-side
|
|
14
|
+
* verification which cryptographically validates the JWT signature using JWKS.
|
|
15
|
+
*
|
|
16
|
+
* This follows a defense-in-depth approach:
|
|
17
|
+
* 1. Client-side (this file): Fast UX-level routing decisions
|
|
18
|
+
* 2. Server middleware: Automatic JWT verification for all API routes
|
|
19
|
+
* 3. Route handlers: Explicit role checks in sensitive endpoints
|
|
20
|
+
*/
|
|
21
|
+
declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { addRouteMiddleware, defineNuxtPlugin, navigateTo, useCookie, useRuntimeConfig } from "#app";
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
const config = useRuntimeConfig();
|
|
5
|
+
const authConfig = config.public.telaAuth;
|
|
6
|
+
const loginPath = authConfig.application?.loginPath ?? "/login";
|
|
7
|
+
const unauthorizedPath = authConfig.application?.unauthorizedPath ?? "/unauthorized";
|
|
8
|
+
addRouteMiddleware("auth-guard", (to) => {
|
|
9
|
+
const authMeta = to.meta?.auth;
|
|
10
|
+
if (!authMeta || authMeta.required !== true) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const token = useCookie("tela-access-token");
|
|
14
|
+
if (!token.value) {
|
|
15
|
+
return navigateTo({
|
|
16
|
+
path: loginPath,
|
|
17
|
+
query: { redirect: to.fullPath }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (authMeta.roles && authMeta.roles.length > 0) {
|
|
21
|
+
try {
|
|
22
|
+
const payload = decodeJwt(token.value);
|
|
23
|
+
const userRole = payload.user?.role;
|
|
24
|
+
if (!userRole || !authMeta.roles.includes(userRole)) {
|
|
25
|
+
return navigateTo({
|
|
26
|
+
path: unauthorizedPath,
|
|
27
|
+
query: { redirect: to.fullPath }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("Failed to decode token:", error);
|
|
32
|
+
return navigateTo({
|
|
33
|
+
path: loginPath,
|
|
34
|
+
query: { redirect: to.fullPath }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, { global: true });
|
|
39
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin to register custom role-based directives
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - v-if-role: Conditionally renders element (like v-if) based on user role
|
|
6
|
+
* - v-show-role: Conditionally shows element (like v-show) based on user role
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```vue
|
|
10
|
+
* <!-- Single role -->
|
|
11
|
+
* <button v-if-role="'admin'">Admin Only</button>
|
|
12
|
+
* <div v-show-role="'moderator'">Moderator Panel</div>
|
|
13
|
+
*
|
|
14
|
+
* <!-- Multiple roles -->
|
|
15
|
+
* <button v-if-role="['admin', 'moderator']">Admin or Moderator</button>
|
|
16
|
+
* <div v-show-role="['editor', 'writer']">Content Panel</div>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from "#app";
|
|
2
|
+
import { useApplicationSessionState } from "../composables/state.js";
|
|
3
|
+
function hasRole(allowedRoles) {
|
|
4
|
+
const { user } = useApplicationSessionState();
|
|
5
|
+
if (!user.value?.role) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
|
9
|
+
return roles.includes(user.value.role);
|
|
10
|
+
}
|
|
11
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
12
|
+
nuxtApp.vueApp.directive("if-role", {
|
|
13
|
+
mounted(el, binding) {
|
|
14
|
+
const shouldRender = hasRole(binding.value);
|
|
15
|
+
if (!shouldRender) {
|
|
16
|
+
const comment = document.createComment("v-if-role");
|
|
17
|
+
el.parentNode?.replaceChild(comment, el);
|
|
18
|
+
comment.__vIfRoleElement = el;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
updated(el, binding) {
|
|
22
|
+
const shouldRender = hasRole(binding.value);
|
|
23
|
+
const isComment = el.nodeType === 8;
|
|
24
|
+
if (shouldRender && isComment) {
|
|
25
|
+
const originalElement = el.__vIfRoleElement;
|
|
26
|
+
if (originalElement && originalElement.parentNode === null) {
|
|
27
|
+
el.parentNode?.replaceChild(originalElement, el);
|
|
28
|
+
}
|
|
29
|
+
} else if (!shouldRender && !isComment) {
|
|
30
|
+
const comment = document.createComment("v-if-role");
|
|
31
|
+
el.parentNode?.replaceChild(comment, el);
|
|
32
|
+
comment.__vIfRoleElement = el;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
unmounted(el) {
|
|
36
|
+
if (el.nodeType === 8) {
|
|
37
|
+
delete el.__vIfRoleElement;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
nuxtApp.vueApp.directive("show-role", {
|
|
42
|
+
mounted(el, binding) {
|
|
43
|
+
const shouldShow = hasRole(binding.value);
|
|
44
|
+
if (!shouldShow) {
|
|
45
|
+
;
|
|
46
|
+
el.__vOriginalDisplay = el.style.display;
|
|
47
|
+
el.style.display = "none";
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
updated(el, binding) {
|
|
51
|
+
const shouldShow = hasRole(binding.value);
|
|
52
|
+
if (shouldShow) {
|
|
53
|
+
el.style.display = el.__vOriginalDisplay || "";
|
|
54
|
+
} else {
|
|
55
|
+
if (!el.__vOriginalDisplay && el.style.display !== "none") {
|
|
56
|
+
;
|
|
57
|
+
el.__vOriginalDisplay = el.style.display;
|
|
58
|
+
}
|
|
59
|
+
el.style.display = "none";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AuthenticatedH3Event } from '../types/h3.js';
|
|
2
|
+
export declare function meistrariApplicationAuthMiddleware(callback: (event: AuthenticatedH3Event) => void | Promise<void>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
3
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
4
|
+
export default _default;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#build/types/nitro-imports";
|
|
2
|
+
import { defineEventHandler, getCookie } from "h3";
|
|
3
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
4
|
+
async function setApplicationAuthContext(event) {
|
|
5
|
+
event.context.auth = {
|
|
6
|
+
user: null,
|
|
7
|
+
workspace: null,
|
|
8
|
+
token: null
|
|
9
|
+
};
|
|
10
|
+
const token = getCookie(event, "tela-access-token");
|
|
11
|
+
if (!token) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const authConfig = useRuntimeConfig(event).public.telaAuth;
|
|
15
|
+
const { apiUrl, application } = authConfig;
|
|
16
|
+
const { applicationId } = application ?? {};
|
|
17
|
+
if (!applicationId || !apiUrl) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const payload = await jwtVerify(
|
|
22
|
+
token,
|
|
23
|
+
createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)),
|
|
24
|
+
{
|
|
25
|
+
issuer: apiUrl,
|
|
26
|
+
audience: applicationId,
|
|
27
|
+
algorithms: ["RS256"]
|
|
28
|
+
}
|
|
29
|
+
).then(({ payload: payload2 }) => payload2);
|
|
30
|
+
event.context.auth = {
|
|
31
|
+
user: { ...payload.user, email: payload.email },
|
|
32
|
+
workspace: payload.workspace,
|
|
33
|
+
token
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function meistrariApplicationAuthMiddleware(callback) {
|
|
39
|
+
return defineEventHandler(async (event) => {
|
|
40
|
+
await setApplicationAuthContext(event);
|
|
41
|
+
await callback(event);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export default defineEventHandler(async (event) => {
|
|
45
|
+
if (!event.path.startsWith("/api")) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await setApplicationAuthContext(event);
|
|
49
|
+
});
|
|
@@ -6,6 +6,7 @@ declare module 'h3' {
|
|
|
6
6
|
auth?: {
|
|
7
7
|
user: { email: string } & JWTTokenPayload['user'] | null
|
|
8
8
|
workspace: JWTTokenPayload['workspace'] | null
|
|
9
|
+
token?: string
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -14,6 +15,7 @@ export interface AuthenticatedH3EventContext extends H3EventContext {
|
|
|
14
15
|
auth: {
|
|
15
16
|
user: { email: string } & JWTTokenPayload['user'] | null
|
|
16
17
|
workspace: JWTTokenPayload['workspace'] | null
|
|
18
|
+
token?: string
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EventHandlerRequest } from 'h3';
|
|
2
|
+
import type { AuthenticatedH3Event } from '../types/h3.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps an event handler to require authentication.
|
|
5
|
+
* Throws 401 if no valid token is present, 403 if user lacks required role.
|
|
6
|
+
*
|
|
7
|
+
* This utility works in two modes:
|
|
8
|
+
* 1. If application-auth middleware has already verified the token, reuses that context
|
|
9
|
+
* 2. If middleware is skipped (skipServerMiddleware: true), performs JWT verification
|
|
10
|
+
*
|
|
11
|
+
* Use this when you need explicit role-based access control on specific API routes,
|
|
12
|
+
* or when you've disabled the global server middleware for performance reasons.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* export default requireAuth(async (event) => {
|
|
16
|
+
* const token = event.context.auth.token
|
|
17
|
+
* return { data: 'protected' }
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* @example With roles
|
|
21
|
+
* export default requireAuth(async (event) => {
|
|
22
|
+
* return { data: 'admin only' }
|
|
23
|
+
* }, { roles: ['admin'] })
|
|
24
|
+
*/
|
|
25
|
+
export declare function requireAuth<T>(handler: (event: AuthenticatedH3Event) => T | Promise<T>, options?: {
|
|
26
|
+
roles?: string[];
|
|
27
|
+
}): import("h3").EventHandler<EventHandlerRequest, any>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#build/types/nitro-imports";
|
|
2
|
+
import { createError, defineEventHandler, getCookie } from "h3";
|
|
3
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
4
|
+
export function requireAuth(handler, options) {
|
|
5
|
+
return defineEventHandler(async (event) => {
|
|
6
|
+
if (event.context.auth?.user && event.context.auth?.token) {
|
|
7
|
+
const user = event.context.auth.user;
|
|
8
|
+
if (options?.roles && !options.roles.includes(user.role ?? "")) {
|
|
9
|
+
throw createError({
|
|
10
|
+
statusCode: 403,
|
|
11
|
+
statusMessage: "Forbidden",
|
|
12
|
+
message: "User is not authorized to access this resource"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return handler(event);
|
|
16
|
+
}
|
|
17
|
+
const token = getCookie(event, "tela-access-token");
|
|
18
|
+
const authConfig = useRuntimeConfig(event).public.telaAuth;
|
|
19
|
+
const { apiUrl, application } = authConfig;
|
|
20
|
+
const { applicationId } = application ?? {};
|
|
21
|
+
if (!applicationId) {
|
|
22
|
+
throw createError({
|
|
23
|
+
statusCode: 500,
|
|
24
|
+
statusMessage: "Internal Server Error",
|
|
25
|
+
message: "Application ID is not configured"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!apiUrl) {
|
|
29
|
+
throw createError({
|
|
30
|
+
statusCode: 500,
|
|
31
|
+
statusMessage: "Internal Server Error",
|
|
32
|
+
message: "API URL is not configured"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw createError({
|
|
37
|
+
statusCode: 401,
|
|
38
|
+
statusMessage: "Unauthorized",
|
|
39
|
+
message: "Authentication required"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const payload = await jwtVerify(token, createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)), {
|
|
44
|
+
issuer: apiUrl,
|
|
45
|
+
audience: applicationId,
|
|
46
|
+
algorithms: ["RS256"]
|
|
47
|
+
}).then(({ payload: payload2 }) => payload2);
|
|
48
|
+
const user = payload.user;
|
|
49
|
+
if (options?.roles && !options.roles.includes(user.role ?? "")) {
|
|
50
|
+
throw createError({
|
|
51
|
+
statusCode: 403,
|
|
52
|
+
statusMessage: "Forbidden",
|
|
53
|
+
message: "User is not authorized to access this resource"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
event.context.auth = { user: { ...payload.user, email: payload.email }, workspace: payload.workspace, token };
|
|
57
|
+
return handler(event);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error && typeof error === "object" && "statusCode" in error) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
throw createError({
|
|
63
|
+
statusCode: 401,
|
|
64
|
+
statusMessage: "Unauthorized",
|
|
65
|
+
message: "Invalid or expired token"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/runtime/shared.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): AuthClient;
|
|
1
|
+
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): any;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare type Roles = 'meistrari:admin' | (string & {})
|
|
2
|
+
declare module '#app' {
|
|
3
|
+
interface PageMeta {
|
|
4
|
+
auth?: {
|
|
5
|
+
required: false
|
|
6
|
+
} | {
|
|
7
|
+
required: true
|
|
8
|
+
roles?: Roles[]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// It is always important to ensure you import/export something when augmenting a type
|
|
14
|
+
export { }
|
|
15
|
+
|
package/package.json
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "@meistrari/auth-nuxt",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types.d.mts",
|
|
8
|
+
"import": "./dist/module.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./server/middleware/auth": {
|
|
11
|
+
"types": "./dist/runtime/server/middleware/auth.d.ts",
|
|
12
|
+
"import": "./dist/runtime/server/middleware/auth.js"
|
|
13
|
+
},
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./dist/core.d.mts",
|
|
16
|
+
"import": "./dist/core.mjs"
|
|
17
|
+
}
|
|
9
18
|
},
|
|
10
|
-
"./
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
"main": "./dist/module.mjs",
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {
|
|
22
|
+
".": [
|
|
23
|
+
"./dist/types.d.mts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
13
26
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@meistrari/auth-core": "workspace:*",
|
|
35
|
+
"jose": "6.1.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"nuxt": "^3.0.0 || ^4.0.0",
|
|
39
|
+
"vue": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@nuxt/devtools": "2.6.3",
|
|
43
|
+
"@nuxt/eslint-config": "1.9.0",
|
|
44
|
+
"@nuxt/kit": "4.0.3",
|
|
45
|
+
"@nuxt/module-builder": "1.0.2",
|
|
46
|
+
"@nuxt/schema": "4.0.3",
|
|
47
|
+
"@nuxt/test-utils": "3.19.2",
|
|
48
|
+
"@types/node": "latest",
|
|
49
|
+
"changelogen": "0.6.2",
|
|
50
|
+
"nuxt": "4.0.3",
|
|
51
|
+
"typescript": "5.9.2",
|
|
52
|
+
"unbuild": "3.6.1",
|
|
53
|
+
"vitest": "3.2.4",
|
|
54
|
+
"vue-tsc": "3.0.6"
|
|
25
55
|
}
|
|
26
|
-
},
|
|
27
|
-
"files": [
|
|
28
|
-
"dist"
|
|
29
|
-
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@meistrari/auth-core": "1.6.0"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"nuxt": "^3.0.0 || ^4.0.0"
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@nuxt/devtools": "2.6.3",
|
|
41
|
-
"@nuxt/eslint-config": "1.9.0",
|
|
42
|
-
"@nuxt/kit": "4.0.3",
|
|
43
|
-
"@nuxt/module-builder": "1.0.2",
|
|
44
|
-
"@nuxt/schema": "4.0.3",
|
|
45
|
-
"@nuxt/test-utils": "3.19.2",
|
|
46
|
-
"@types/node": "latest",
|
|
47
|
-
"changelogen": "0.6.2",
|
|
48
|
-
"nuxt": "4.0.3",
|
|
49
|
-
"typescript": "5.9.2",
|
|
50
|
-
"unbuild": "3.6.1",
|
|
51
|
-
"vitest": "3.2.4",
|
|
52
|
-
"vue-tsc": "3.0.6"
|
|
53
|
-
}
|
|
54
56
|
}
|