@meistrari/auth-nuxt 2.0.0 → 2.0.1

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.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
3
  "configKey": "telaAuth",
4
- "version": "2.0.0",
4
+ "version": "1.0.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addImports, addPlugin, extendPages, addComponent, addServerImportsDir, addServerHandler } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImports, addPlugin, extendPages, addComponent, addRouteMiddleware, addServerImportsDir, addServerHandler } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -39,13 +39,12 @@ const module$1 = defineNuxtModule({
39
39
  });
40
40
  addPlugin(resolver.resolve("./runtime/plugins/auth-guard"));
41
41
  addPlugin(resolver.resolve("./runtime/plugins/directives"));
42
+ addRouteMiddleware({
43
+ name: "tela-auth",
44
+ path: resolver.resolve("./runtime/middleware/require-auth"),
45
+ global: true
46
+ });
42
47
  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
- }
49
48
  return;
50
49
  }
51
50
  if (!options.skipServerMiddleware) {
@@ -36,6 +36,34 @@ export declare function useTelaApplicationAuth(): {
36
36
  login: () => Promise<void>;
37
37
  logout: () => Promise<void>;
38
38
  initSession: () => Promise<void>;
39
- user: import("vue").Ref<any, any>;
40
- activeOrganization: import("vue").Ref<Omit<FullOrganization, "members" | "invitations" | "teams"> | null, Omit<FullOrganization, "members" | "invitations" | "teams"> | null>;
39
+ user: import("vue").Ref<{
40
+ id: string;
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>;
41
69
  };
@@ -1,20 +1,101 @@
1
+ import type { FullOrganization, Member } from '@meistrari/auth-core';
1
2
  /**
2
3
  * Shared state for session management.
3
4
  * This module provides access to session-related state without creating circular dependencies.
4
5
  */
5
6
  export declare function useSessionState(): {
6
- user: import("vue").Ref<any, any>;
7
- session: import("vue").Ref<any, any>;
7
+ user: import("vue").Ref<{
8
+ id: string;
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>;
8
61
  };
9
62
  /**
10
63
  * Shared state for organization management.
11
64
  * This module provides access to organization-related state without creating circular dependencies.
12
65
  */
13
66
  export declare function useOrganizationState(): {
14
- activeOrganization: import("vue").Ref<any, any>;
15
- activeMember: import("vue").Ref<any, any>;
67
+ activeOrganization: import("vue").Ref<FullOrganization | null, FullOrganization | null>;
68
+ activeMember: import("vue").Ref<Member | null, Member | null>;
16
69
  };
17
70
  export declare function useApplicationSessionState(): {
18
- user: import("vue").Ref<any, any>;
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>;
19
100
  activeOrganization: import("vue").Ref<Omit<FullOrganization, "members" | "invitations" | "teams"> | null, Omit<FullOrganization, "members" | "invitations" | "teams"> | null>;
20
101
  };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("#app").RouteMiddleware;
2
+ export default _default;
@@ -0,0 +1,42 @@
1
+ import { defineNuxtRouteMiddleware, navigateTo, useCookie, useRuntimeConfig } from "#app";
2
+ import { createRemoteJWKSet, jwtVerify } from "jose";
3
+ export default defineNuxtRouteMiddleware(async (to) => {
4
+ const authMeta = to.meta?.auth;
5
+ if (!authMeta || authMeta.required !== true) {
6
+ return;
7
+ }
8
+ const config = useRuntimeConfig();
9
+ const authConfig = config.public.telaAuth;
10
+ const { apiUrl, application } = authConfig;
11
+ const { applicationId, loginPath = "/login", unauthorizedPath = "/unauthorized" } = application ?? {};
12
+ const token = useCookie("tela-access-token");
13
+ if (!token.value) {
14
+ return navigateTo(loginPath);
15
+ }
16
+ if (!applicationId || !apiUrl) {
17
+ console.error("Application ID or API URL is not configured");
18
+ return;
19
+ }
20
+ try {
21
+ const { payload } = await jwtVerify(
22
+ token.value,
23
+ createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)),
24
+ {
25
+ issuer: apiUrl,
26
+ audience: applicationId,
27
+ algorithms: ["RS256"]
28
+ }
29
+ );
30
+ const tokenPayload = payload;
31
+ const user = tokenPayload.user;
32
+ if (authMeta.roles && authMeta.roles.length > 0) {
33
+ const userRole = user.role ?? "";
34
+ if (!authMeta.roles.includes(userRole)) {
35
+ return navigateTo(unauthorizedPath);
36
+ }
37
+ }
38
+ } catch (error) {
39
+ console.error("Token validation failed:", error);
40
+ return navigateTo(loginPath);
41
+ }
42
+ });
@@ -1,22 +1,11 @@
1
1
  <script setup>
2
- import { navigateTo, useRuntimeConfig } from "#app";
2
+ import { navigateTo } from "#app";
3
3
  import { onMounted } from "vue";
4
4
  import { useTelaApplicationAuth } from "../composables/application-auth";
5
5
  const { initSession } = useTelaApplicationAuth();
6
- const config = useRuntimeConfig();
7
- const authConfig = config.public.telaAuth;
8
- const loginPath = authConfig.application?.loginPath ?? "/login";
9
6
  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
- }
7
+ await initSession();
8
+ navigateTo("/");
20
9
  });
21
10
  </script>
22
11
 
@@ -1,22 +1,2 @@
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
1
  declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
22
2
  export default _default;
@@ -23,7 +23,7 @@ export default defineNuxtPlugin((nuxtApp) => {
23
23
  const isComment = el.nodeType === 8;
24
24
  if (shouldRender && isComment) {
25
25
  const originalElement = el.__vIfRoleElement;
26
- if (originalElement && originalElement.parentNode === null) {
26
+ if (originalElement) {
27
27
  el.parentNode?.replaceChild(originalElement, el);
28
28
  }
29
29
  } else if (!shouldRender && !isComment) {
@@ -31,11 +31,6 @@ export default defineNuxtPlugin((nuxtApp) => {
31
31
  el.parentNode?.replaceChild(comment, el);
32
32
  comment.__vIfRoleElement = el;
33
33
  }
34
- },
35
- unmounted(el) {
36
- if (el.nodeType === 8) {
37
- delete el.__vIfRoleElement;
38
- }
39
34
  }
40
35
  });
41
36
  nuxtApp.vueApp.directive("show-role", {
@@ -2,14 +2,7 @@ import type { EventHandlerRequest } from 'h3';
2
2
  import type { AuthenticatedH3Event } from '../types/h3.js';
3
3
  /**
4
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.
5
+ * Throws 401 if no valid token is present.
13
6
  *
14
7
  * @example
15
8
  * export default requireAuth(async (event) => {
@@ -19,6 +12,7 @@ import type { AuthenticatedH3Event } from '../types/h3.js';
19
12
  *
20
13
  * @example With roles
21
14
  * export default requireAuth(async (event) => {
15
+ * // TODO: Validate roles from token
22
16
  * return { data: 'admin only' }
23
17
  * }, { roles: ['admin'] })
24
18
  */
@@ -3,17 +3,6 @@ import { createError, defineEventHandler, getCookie } from "h3";
3
3
  import { createRemoteJWKSet, jwtVerify } from "jose";
4
4
  export function requireAuth(handler, options) {
5
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
6
  const token = getCookie(event, "tela-access-token");
18
7
  const authConfig = useRuntimeConfig(event).public.telaAuth;
19
8
  const { apiUrl, application } = authConfig;
@@ -39,31 +28,23 @@ export function requireAuth(handler, options) {
39
28
  message: "Authentication required"
40
29
  });
41
30
  }
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
- }
31
+ const payload = await jwtVerify(token, createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)), {
32
+ issuer: apiUrl,
33
+ audience: applicationId,
34
+ algorithms: ["RS256"]
35
+ }).then(({ payload: payload2 }) => payload2);
36
+ const user = payload.user;
37
+ if (options?.roles && !options.roles.includes(user.role ?? "")) {
62
38
  throw createError({
63
- statusCode: 401,
64
- statusMessage: "Unauthorized",
65
- message: "Invalid or expired token"
39
+ statusCode: 403,
40
+ statusMessage: "Forbidden",
41
+ message: "User is not authorized to access this resource"
66
42
  });
67
43
  }
44
+ if (!event.context.auth) {
45
+ event.context.auth = { user: { ...payload.user, email: payload.email }, workspace: payload.workspace, token };
46
+ }
47
+ event.context.auth.token = token;
48
+ return handler(event);
68
49
  });
69
50
  }
@@ -1 +1,2 @@
1
- export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): any;
1
+ import { AuthClient } from '@meistrari/auth-core';
2
+ export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): AuthClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -31,7 +31,7 @@
31
31
  "build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
32
32
  },
33
33
  "dependencies": {
34
- "@meistrari/auth-core": "workspace:*",
34
+ "@meistrari/auth-core": "1.4.0",
35
35
  "jose": "6.1.3"
36
36
  },
37
37
  "peerDependencies": {
@@ -1,4 +0,0 @@
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;
@@ -1,49 +0,0 @@
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
- });