@meistrari/auth-nuxt 2.1.4 → 2.2.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 CHANGED
@@ -14,7 +14,7 @@ interface ModuleOptions {
14
14
  dashboardUrl: string;
15
15
  /** The ID of the application to authenticate with */
16
16
  applicationId: string;
17
- /** The redirect URI to redirect to after authentication. Must be registered in the application's settings. */
17
+ /** The authentication callback URL. Usually `{your-app-url}/auth/callback` */
18
18
  redirectUri: string;
19
19
  /** Path to redirect to when authentication is required (default: '/login') */
20
20
  loginPath?: string;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
3
  "configKey": "telaAuth",
4
- "version": "2.1.4",
4
+ "version": "2.2.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, addComponent, addServerImportsDir, addServerHandler, addPlugin } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -18,14 +18,6 @@ const module$1 = defineNuxtModule({
18
18
  as: "useTelaApplicationAuth",
19
19
  from: resolver.resolve("runtime/composables/application-auth")
20
20
  });
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
21
  addComponent({
30
22
  name: "TelaRole",
31
23
  filePath: resolver.resolve("./runtime/components/tela-role.vue"),
@@ -37,8 +29,6 @@ const module$1 = defineNuxtModule({
37
29
  path: resolver.resolve("./runtime/types/page-meta.d.ts")
38
30
  });
39
31
  });
40
- addPlugin(resolver.resolve("./runtime/plugins/auth-guard"));
41
- addPlugin(resolver.resolve("./runtime/plugins/directives"));
42
32
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
43
33
  if (!options.skipServerMiddleware) {
44
34
  addServerHandler({
@@ -46,6 +36,39 @@ const module$1 = defineNuxtModule({
46
36
  handler: resolver.resolve("./runtime/server/middleware/application-auth")
47
37
  });
48
38
  }
39
+ addServerHandler({
40
+ route: "/auth/callback",
41
+ handler: resolver.resolve("./runtime/server/routes/auth/callback"),
42
+ method: "get"
43
+ });
44
+ addServerHandler({
45
+ route: "/auth/login",
46
+ handler: resolver.resolve("./runtime/server/routes/auth/login"),
47
+ method: "post"
48
+ });
49
+ addServerHandler({
50
+ route: "/auth/refresh",
51
+ handler: resolver.resolve("./runtime/server/routes/auth/refresh"),
52
+ method: "post"
53
+ });
54
+ addServerHandler({
55
+ route: "/auth/logout",
56
+ handler: resolver.resolve("./runtime/server/routes/auth/logout"),
57
+ method: "post"
58
+ });
59
+ addServerHandler({
60
+ route: "/auth/organizations",
61
+ handler: resolver.resolve("./runtime/server/routes/auth/organizations"),
62
+ method: "get"
63
+ });
64
+ addServerHandler({
65
+ route: "/auth/switch-organization",
66
+ handler: resolver.resolve("./runtime/server/routes/auth/switch-organization"),
67
+ method: "post"
68
+ });
69
+ addPlugin(resolver.resolve("./runtime/plugins/application-token-refresh"));
70
+ addPlugin(resolver.resolve("./runtime/plugins/auth-guard"));
71
+ addPlugin(resolver.resolve("./runtime/plugins/directives"));
49
72
  return;
50
73
  }
51
74
  if (!options.skipServerMiddleware) {
@@ -1,3 +1,11 @@
1
+ type RawOrganization = {
2
+ title: string;
3
+ id: string;
4
+ createdAt: Date;
5
+ avatarUrl: string | null;
6
+ metadata: string | null;
7
+ slug: string | null;
8
+ };
1
9
  /**
2
10
  * Composable for managing Tela application authentication with OAuth 2.0 PKCE flow
3
11
  *
@@ -36,8 +44,9 @@ export declare function useTelaApplicationAuth(): {
36
44
  login: () => Promise<void>;
37
45
  logout: () => Promise<void>;
38
46
  initSession: () => Promise<void>;
39
- getAvailableOrganizations: () => Promise<import("@meistrari/auth-core").Organization[]>;
47
+ getAvailableOrganizations: () => Promise<RawOrganization[]>;
40
48
  switchOrganization: (organizationId: string) => Promise<void>;
49
+ refreshToken: () => Promise<void>;
41
50
  user: import("vue").Ref<{
42
51
  id: string;
43
52
  createdAt: Date;
@@ -69,3 +78,4 @@ export declare function useTelaApplicationAuth(): {
69
78
  } | null>;
70
79
  activeOrganization: import("vue").Ref<Omit<import("@meistrari/auth-core").FullOrganization, "members" | "invitations" | "teams"> | null, Omit<import("@meistrari/auth-core").FullOrganization, "members" | "invitations" | "teams"> | null>;
71
80
  };
81
+ export {};
@@ -1,38 +1,8 @@
1
- import { navigateTo, useCookie, useRoute, useRuntimeConfig } from "#app";
2
- import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError } from "@meistrari/auth-core";
3
- import { createNuxtAuthClient } from "../shared.js";
1
+ import { navigateTo, useCookie, useRuntimeConfig } from "#app";
2
+ import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError, UserNotLoggedInError } from "@meistrari/auth-core";
4
3
  import { useApplicationSessionState } from "./state.js";
5
- const SEVEN_DAYS = 60 * 60 * 24 * 7;
6
4
  const FIFTEEN_MINUTES = 60 * 15;
7
5
  const ONE_MINUTE = 60 * 1e3;
8
- function verifier(stateKey) {
9
- return `code_verifier_${stateKey}`;
10
- }
11
- function hexEncode(bytes) {
12
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
13
- }
14
- function generateStateKey(query) {
15
- if (query.state && typeof query.state === "string") {
16
- return query.state;
17
- }
18
- const array = new Uint8Array(8);
19
- crypto.getRandomValues(array);
20
- return hexEncode(array);
21
- }
22
- function generateCodeVerifier() {
23
- const array = new Uint8Array(32);
24
- crypto.getRandomValues(array);
25
- return base64UrlEncode(array);
26
- }
27
- async function generateCodeChallenge(verifier2) {
28
- const encoder = new TextEncoder();
29
- const data = encoder.encode(verifier2);
30
- const digest = await crypto.subtle.digest("SHA-256", data);
31
- return base64UrlEncode(new Uint8Array(digest));
32
- }
33
- function base64UrlEncode(bytes) {
34
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
35
- }
36
6
  function mapOrganization(organization) {
37
7
  return {
38
8
  name: organization.title,
@@ -45,18 +15,11 @@ function mapOrganization(organization) {
45
15
  }
46
16
  export function useTelaApplicationAuth() {
47
17
  const appConfig = useRuntimeConfig().public.telaAuth;
48
- const query = useRoute().query;
49
18
  const accessTokenCookie = useCookie("tela-access-token", {
50
- secure: true,
19
+ secure: !import.meta.dev,
51
20
  sameSite: "lax",
52
21
  maxAge: FIFTEEN_MINUTES
53
22
  });
54
- const refreshTokenCookie = useCookie("tela-refresh-token", {
55
- secure: true,
56
- sameSite: "lax",
57
- maxAge: SEVEN_DAYS
58
- });
59
- const authClient = createNuxtAuthClient(appConfig.apiUrl ?? "", () => null, () => refreshTokenCookie.value ?? null);
60
23
  const state = useApplicationSessionState();
61
24
  if (!appConfig.application?.dashboardUrl) {
62
25
  throw new Error(
@@ -78,13 +41,7 @@ export function useTelaApplicationAuth() {
78
41
  if (import.meta.server) {
79
42
  throw new AuthorizationFlowError("The login function can only be called on the client side.");
80
43
  }
81
- if (typeof localStorage === "undefined") {
82
- throw new AuthorizationFlowError("localStorage is not available. The login function must be called on the client side.");
83
- }
84
- const codeVerifier = generateCodeVerifier();
85
- const codeChallenge = await generateCodeChallenge(codeVerifier);
86
- const stateKey = generateStateKey(query);
87
- localStorage.setItem(verifier(stateKey), codeVerifier);
44
+ const { state: stateKey, challenge: codeChallenge } = await $fetch("/auth/login", { method: "POST" });
88
45
  const url = new URL("/applications/login", appConfig.application?.dashboardUrl);
89
46
  url.searchParams.set("application_id", applicationId);
90
47
  url.searchParams.set("code_challenge", codeChallenge);
@@ -93,85 +50,52 @@ export function useTelaApplicationAuth() {
93
50
  await navigateTo(url.toString(), { external: true });
94
51
  }
95
52
  async function logout() {
96
- accessTokenCookie.value = null;
97
- refreshTokenCookie.value = null;
98
53
  state.user.value = null;
99
54
  state.activeOrganization.value = null;
100
- if (typeof localStorage !== "undefined") {
101
- for (const key in localStorage) {
102
- if (key.startsWith("code_verifier_")) {
103
- localStorage.removeItem(key);
104
- }
105
- }
106
- }
107
- }
108
- async function exchangeCodeForToken() {
109
- if (import.meta.server) {
110
- throw new AuthorizationFlowError("The exchangeCodeForToken function can only be called on the client side.");
111
- }
112
- const code = query.code;
113
- const stateKey = query.state;
114
- if (!code) {
115
- throw new AuthorizationFlowError("Authorization code not found in query parameters");
116
- }
117
- if (!stateKey) {
118
- throw new AuthorizationFlowError("State parameter not found in query parameters");
119
- }
120
- if (typeof localStorage === "undefined") {
121
- throw new AuthorizationFlowError("localStorage is not available");
122
- }
123
- const codeVerifierKey = verifier(stateKey);
124
- const codeVerifier = localStorage.getItem(codeVerifierKey);
125
- if (!codeVerifier) {
126
- throw new AuthorizationFlowError("Code verifier not found. This may indicate a CSRF attack or expired session.");
127
- }
128
- try {
129
- const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.completeAuthorizationFlow(code, codeVerifier);
130
- accessTokenCookie.value = accessToken;
131
- refreshTokenCookie.value = refreshToken2;
132
- state.user.value = user;
133
- state.activeOrganization.value = mapOrganization(organization);
134
- } finally {
135
- localStorage.removeItem(codeVerifierKey);
136
- }
55
+ await $fetch("/auth/logout", { method: "POST" });
137
56
  }
138
57
  async function refreshToken() {
139
- if (!refreshTokenCookie.value) {
58
+ try {
59
+ const result = await $fetch("/auth/refresh", {
60
+ method: "POST"
61
+ });
62
+ state.user.value = result.user;
63
+ state.activeOrganization.value = mapOrganization(result.organization);
64
+ } catch (error) {
65
+ console.error("[Auth Refresh] Failed to refresh token:", error);
140
66
  throw new RefreshTokenExpiredError();
141
67
  }
142
- const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.refreshAccessToken(refreshTokenCookie.value);
143
- accessTokenCookie.value = accessToken;
144
- refreshTokenCookie.value = refreshToken2;
145
- state.user.value = user;
146
- state.activeOrganization.value = mapOrganization(organization);
147
68
  }
148
69
  async function initSession() {
149
- const code = query.code;
150
- if (code) {
151
- await exchangeCodeForToken();
152
- }
153
- if (!accessTokenCookie.value && !refreshTokenCookie.value) {
154
- throw new RefreshTokenExpiredError();
70
+ if (!accessTokenCookie.value) {
71
+ throw new UserNotLoggedInError("No access token found in cookies");
155
72
  }
156
73
  const isExpiredOrClose = accessTokenCookie.value ? isTokenExpired(accessTokenCookie.value, ONE_MINUTE) : true;
157
- if (isExpiredOrClose && !refreshTokenCookie.value) {
158
- await logout();
159
- throw new RefreshTokenExpiredError();
160
- }
161
- if (isExpiredOrClose && refreshTokenCookie.value) {
74
+ if (isExpiredOrClose) {
162
75
  await refreshToken();
163
76
  }
164
77
  }
165
78
  async function getAvailableOrganizations() {
166
- const { organizations } = await authClient.application.listCandidateOrganizations(applicationId);
167
- return organizations;
79
+ try {
80
+ const result = await $fetch("/auth/organizations", { method: "GET" });
81
+ return result.organizations;
82
+ } catch (error) {
83
+ console.error("[Auth Orgs] Failed to list organizations:", error);
84
+ throw error;
85
+ }
168
86
  }
169
87
  async function switchOrganization(organizationId) {
170
- const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.switchOrganization(organizationId, accessTokenCookie.value ?? "");
171
- accessTokenCookie.value = accessToken;
172
- refreshTokenCookie.value = refreshToken2;
173
- state.user.value = user;
174
- state.activeOrganization.value = mapOrganization(organization);
88
+ try {
89
+ const result = await $fetch("/auth/switch-organization", {
90
+ method: "POST",
91
+ body: { organizationId }
92
+ });
93
+ state.user.value = result.user;
94
+ state.activeOrganization.value = mapOrganization(result.organization);
95
+ } catch (error) {
96
+ console.error("[Auth Switch Org] Failed to switch organization:", error);
97
+ throw error;
98
+ }
175
99
  }
176
100
  return {
177
101
  ...state,
@@ -179,6 +103,7 @@ export function useTelaApplicationAuth() {
179
103
  logout,
180
104
  initSession,
181
105
  getAvailableOrganizations,
182
- switchOrganization
106
+ switchOrganization,
107
+ refreshToken
183
108
  };
184
109
  }
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtPlugin, useCookie, useRuntimeConfig } from "#app";
2
- import { isTokenExpired, RefreshTokenExpiredError } from "@meistrari/auth-core";
3
- import { watch } from "vue";
2
+ import { isTokenExpired } from "@meistrari/auth-core";
3
+ import { decodeJwt } from "jose";
4
4
  import { useTelaApplicationAuth } from "../composables/application-auth.js";
5
5
  import { useApplicationSessionState } from "../composables/state.js";
6
6
  import { createNuxtAuthClient } from "../shared.js";
@@ -9,11 +9,7 @@ const FIFTEEN_MINUTES = 60 * 15;
9
9
  const TWO_MINUTES = 2 * 60 * 1e3;
10
10
  function parseTokenExpiry(token) {
11
11
  try {
12
- const tokenParts = token.split(".");
13
- const payloadPart = tokenParts[1];
14
- if (!payloadPart)
15
- return null;
16
- const payload = JSON.parse(atob(payloadPart));
12
+ const payload = decodeJwt(token);
17
13
  if (!payload.exp)
18
14
  return null;
19
15
  return payload.exp * 1e3;
@@ -42,12 +38,13 @@ export default defineNuxtPlugin({
42
38
  const state = useApplicationSessionState();
43
39
  const { login, logout: sdkLogout } = useTelaApplicationAuth();
44
40
  const accessTokenCookie = useCookie("tela-access-token", {
45
- secure: true,
41
+ secure: !import.meta.dev,
46
42
  sameSite: "lax",
47
43
  maxAge: FIFTEEN_MINUTES
48
44
  });
49
45
  const refreshTokenCookie = useCookie("tela-refresh-token", {
50
- secure: true,
46
+ httpOnly: true,
47
+ secure: !import.meta.dev,
51
48
  sameSite: "lax",
52
49
  maxAge: SEVEN_DAYS
53
50
  });
@@ -60,12 +57,17 @@ export default defineNuxtPlugin({
60
57
  }
61
58
  isRefreshing = true;
62
59
  try {
63
- if (!refreshTokenCookie.value) {
64
- throw new RefreshTokenExpiredError();
60
+ if (import.meta.server) {
61
+ const { accessToken, refreshToken: refreshToken2, user: user2, organization: organization2 } = await authClient.application.refreshAccessToken(refreshTokenCookie.value ?? "");
62
+ accessTokenCookie.value = accessToken;
63
+ refreshTokenCookie.value = refreshToken2;
64
+ state.user.value = user2;
65
+ state.activeOrganization.value = mapOrganization(organization2);
66
+ return;
65
67
  }
66
- const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.refreshAccessToken(refreshTokenCookie.value);
67
- accessTokenCookie.value = accessToken;
68
- refreshTokenCookie.value = refreshToken2;
68
+ const { user, organization } = await $fetch("/auth/refresh", {
69
+ method: "POST"
70
+ });
69
71
  state.user.value = user;
70
72
  state.activeOrganization.value = mapOrganization(organization);
71
73
  } catch {
@@ -79,7 +81,6 @@ export default defineNuxtPlugin({
79
81
  }
80
82
  function logout() {
81
83
  accessTokenCookie.value = null;
82
- refreshTokenCookie.value = null;
83
84
  state.user.value = null;
84
85
  state.activeOrganization.value = null;
85
86
  }
@@ -134,11 +135,7 @@ export default defineNuxtPlugin({
134
135
  return;
135
136
  }
136
137
  if (import.meta.client) {
137
- watch(refreshTokenCookie, async (newVal) => {
138
- if (newVal) {
139
- await scheduleTokenRefresh();
140
- }
141
- }, { immediate: true });
138
+ void scheduleTokenRefresh();
142
139
  }
143
140
  }
144
141
  });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Server route handler for OAuth 2.0 PKCE callback
3
+ *
4
+ * This route:
5
+ * 1. Receives the authorization code and state from the OAuth provider
6
+ * 2. Retrieves the code verifier from a secure cookie
7
+ * 3. Exchanges the code for access and refresh tokens
8
+ * 4. Sets authentication cookies
9
+ * 5. Redirects to the home page or login page on error
10
+ */
11
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
12
+ export default _default;
@@ -0,0 +1,49 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, deleteCookie, getCookie, getQuery, sendRedirect, setCookie } from "h3";
3
+ import { createNuxtAuthClient } from "../../../shared.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const query = getQuery(event);
6
+ const code = query.code;
7
+ const state = query.state;
8
+ const config = useRuntimeConfig();
9
+ const authConfig = config.public.telaAuth;
10
+ const loginPath = authConfig.application?.loginPath ?? "/login";
11
+ if (!code || !state) {
12
+ console.error("[Auth Callback] Missing code or state parameter");
13
+ return await sendRedirect(event, `${loginPath}?error=invalid_callback`);
14
+ }
15
+ const codeVerifier = getCookie(event, `tela-verifier-${state}`);
16
+ if (!codeVerifier) {
17
+ console.error("[Auth Callback] Code verifier not found in cookies");
18
+ return await sendRedirect(event, `${loginPath}?error=verifier_missing`);
19
+ }
20
+ try {
21
+ const authClient = createNuxtAuthClient(
22
+ authConfig.apiUrl,
23
+ () => null,
24
+ () => null
25
+ );
26
+ const { accessToken, refreshToken } = await authClient.application.completeAuthorizationFlow(code, codeVerifier);
27
+ setCookie(event, "tela-access-token", accessToken, {
28
+ secure: !import.meta.dev,
29
+ sameSite: "lax",
30
+ maxAge: 60 * 15,
31
+ // 15 minutes
32
+ path: "/"
33
+ });
34
+ setCookie(event, "tela-refresh-token", refreshToken, {
35
+ secure: !import.meta.dev,
36
+ sameSite: "lax",
37
+ httpOnly: true,
38
+ maxAge: 60 * 60 * 24 * 7,
39
+ // 7 days
40
+ path: "/"
41
+ });
42
+ deleteCookie(event, `tela-verifier-${state}`, { path: "/" });
43
+ return await sendRedirect(event, "/");
44
+ } catch (error) {
45
+ console.error("[Auth Callback] OAuth flow error:", error);
46
+ deleteCookie(event, `tela-verifier-${state}`, { path: "/" });
47
+ return await sendRedirect(event, `${loginPath}?error=auth_failed`);
48
+ }
49
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Server route handler for OAuth 2.0 PKCE initialization
3
+ *
4
+ * This route:
5
+ * 1. Generates a cryptographically secure state key
6
+ * 2. Generates a code verifier for PKCE
7
+ * 3. Generates a code challenge from the verifier
8
+ * 4. Sets the verifier in a secure HTTP-only cookie
9
+ * 5. Returns only the state and challenge in the response body
10
+ *
11
+ * The client never has access to the verifier - it only receives the challenge
12
+ * and state to use in the authorization URL.
13
+ */
14
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
15
+ state: string;
16
+ challenge: string;
17
+ }>>;
18
+ export default _default;
@@ -0,0 +1,41 @@
1
+ import { defineEventHandler, setCookie } from "h3";
2
+ function hexEncode(bytes) {
3
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
4
+ }
5
+ function generateStateKey() {
6
+ const array = new Uint8Array(8);
7
+ crypto.getRandomValues(array);
8
+ return hexEncode(array);
9
+ }
10
+ function generateCodeVerifier() {
11
+ const array = new Uint8Array(32);
12
+ crypto.getRandomValues(array);
13
+ return base64UrlEncode(array);
14
+ }
15
+ function base64UrlEncode(bytes) {
16
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
17
+ }
18
+ async function generateCodeChallenge(verifier) {
19
+ const encoder = new TextEncoder();
20
+ const data = encoder.encode(verifier);
21
+ const digest = await crypto.subtle.digest("SHA-256", data);
22
+ return base64UrlEncode(new Uint8Array(digest));
23
+ }
24
+ export default defineEventHandler(async (event) => {
25
+ const state = generateStateKey();
26
+ const verifier = generateCodeVerifier();
27
+ const challenge = await generateCodeChallenge(verifier);
28
+ setCookie(event, `tela-verifier-${state}`, verifier, {
29
+ secure: !import.meta.dev,
30
+ sameSite: "lax",
31
+ httpOnly: true,
32
+ // Server-only access
33
+ maxAge: 60 * 10,
34
+ // 10 minutes - just long enough for OAuth flow
35
+ path: "/"
36
+ });
37
+ return {
38
+ state,
39
+ challenge
40
+ };
41
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ success: boolean;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,26 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, deleteCookie, getCookie } from "h3";
3
+ import { createNuxtAuthClient } from "../../../shared.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const authConfig = useRuntimeConfig().public.telaAuth;
6
+ const refreshToken = getCookie(event, "tela-refresh-token");
7
+ deleteCookie(event, "tela-access-token", { path: "/" });
8
+ deleteCookie(event, "tela-refresh-token", { path: "/" });
9
+ try {
10
+ const authClient = createNuxtAuthClient(
11
+ authConfig.apiUrl,
12
+ () => null
13
+ );
14
+ if (refreshToken) {
15
+ await authClient.application.logout(refreshToken);
16
+ }
17
+ return {
18
+ success: true
19
+ };
20
+ } catch (error) {
21
+ console.error("[Auth Logout] Failed to logout:", error);
22
+ return {
23
+ success: true
24
+ };
25
+ }
26
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Server route handler for listing available organizations
3
+ *
4
+ * This route:
5
+ * 1. Retrieves the access token from the cookie
6
+ * 2. Calls the auth API to get the list of organizations the user can switch to
7
+ * 3. Returns the organizations data
8
+ *
9
+ * Returns only organizations that are entitled to the application and where
10
+ * the entitlement's access rules permit the current user.
11
+ */
12
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
13
+ success: boolean;
14
+ organizations: import("@meistrari/auth-core").Organization[];
15
+ }>>;
16
+ export default _default;
@@ -0,0 +1,40 @@
1
+ import { createError, useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getCookie } from "h3";
3
+ import { createNuxtAuthClient } from "../../../shared.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const authConfig = config.public.telaAuth;
7
+ const accessToken = getCookie(event, "tela-access-token");
8
+ if (!accessToken) {
9
+ throw createError({
10
+ statusCode: 401,
11
+ statusMessage: "Unauthorized",
12
+ message: "No access token found"
13
+ });
14
+ }
15
+ if (!authConfig.application?.applicationId) {
16
+ throw createError({
17
+ statusCode: 500,
18
+ statusMessage: "Internal Server Error",
19
+ message: "Application ID is not configured"
20
+ });
21
+ }
22
+ try {
23
+ const authClient = createNuxtAuthClient(
24
+ authConfig.apiUrl,
25
+ () => accessToken
26
+ );
27
+ const { organizations } = await authClient.application.listCandidateOrganizations(authConfig.application.applicationId);
28
+ return {
29
+ success: true,
30
+ organizations
31
+ };
32
+ } catch (error) {
33
+ console.error("[Auth Orgs] Failed to list organizations:", error);
34
+ throw createError({
35
+ statusCode: 500,
36
+ statusMessage: "Internal Server Error",
37
+ message: "Failed to list organizations"
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Server route handler for token refresh
3
+ *
4
+ * This route:
5
+ * 1. Retrieves the refresh token from the secure httponly cookie
6
+ * 2. Calls the auth API to exchange it for new tokens
7
+ * 3. Sets the new access and refresh tokens in secure cookies
8
+ * 4. Returns the user and organization data
9
+ *
10
+ * This keeps the refresh token secure by never exposing it to the client.
11
+ */
12
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
13
+ success: boolean;
14
+ user: {
15
+ id: string;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ email: string;
19
+ emailVerified: boolean;
20
+ name: string;
21
+ image?: string | null | undefined;
22
+ twoFactorEnabled: boolean | null | undefined;
23
+ banned: boolean | null | undefined;
24
+ role?: string | null | undefined;
25
+ banReason?: string | null | undefined;
26
+ banExpires?: Date | null | undefined;
27
+ lastActiveAt?: Date | null | undefined;
28
+ };
29
+ organization: {
30
+ id: string;
31
+ title: string;
32
+ slug: string | null;
33
+ avatarUrl: string | null;
34
+ createdAt: Date;
35
+ metadata: string | null;
36
+ settings: unknown;
37
+ };
38
+ }>>;
39
+ export default _default;
@@ -0,0 +1,54 @@
1
+ import { createError, useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, deleteCookie, getCookie, setCookie } from "h3";
3
+ import { createNuxtAuthClient } from "../../../shared.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const authConfig = config.public.telaAuth;
7
+ const refreshToken = getCookie(event, "tela-refresh-token");
8
+ if (!refreshToken) {
9
+ throw createError({
10
+ statusCode: 401,
11
+ statusMessage: "Unauthorized",
12
+ message: "No refresh token found"
13
+ });
14
+ }
15
+ try {
16
+ const authClient = createNuxtAuthClient(
17
+ authConfig.apiUrl,
18
+ () => null,
19
+ () => refreshToken
20
+ );
21
+ const { accessToken, refreshToken: newRefreshToken, user, organization } = await authClient.application.refreshAccessToken(refreshToken);
22
+ setCookie(event, "tela-access-token", accessToken, {
23
+ secure: !import.meta.dev,
24
+ sameSite: "lax",
25
+ maxAge: 60 * 15,
26
+ // 15 minutes
27
+ priority: "high",
28
+ path: "/"
29
+ });
30
+ setCookie(event, "tela-refresh-token", newRefreshToken, {
31
+ secure: !import.meta.dev,
32
+ sameSite: "lax",
33
+ httpOnly: true,
34
+ maxAge: 60 * 60 * 24 * 7,
35
+ // 7 days
36
+ priority: "high",
37
+ path: "/"
38
+ });
39
+ return {
40
+ success: true,
41
+ user,
42
+ organization
43
+ };
44
+ } catch (error) {
45
+ console.error("[Auth Refresh] Token refresh error:", error);
46
+ deleteCookie(event, "tela-access-token", { path: "/" });
47
+ deleteCookie(event, "tela-refresh-token", { path: "/" });
48
+ throw createError({
49
+ statusCode: 401,
50
+ statusMessage: "Unauthorized",
51
+ message: "Failed to refresh access token"
52
+ });
53
+ }
54
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Server route handler for switching organizations
3
+ *
4
+ * This route:
5
+ * 1. Retrieves the access token from the cookie
6
+ * 2. Calls the auth API to switch to the specified organization
7
+ * 3. Sets the new access and refresh tokens in secure cookies
8
+ * 4. Returns the user and organization data
9
+ *
10
+ * The organization must be entitled to the application and the entitlement's
11
+ * access rules must allow the current user.
12
+ */
13
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
14
+ success: boolean;
15
+ user: {
16
+ id: string;
17
+ createdAt: Date;
18
+ updatedAt: Date;
19
+ email: string;
20
+ emailVerified: boolean;
21
+ name: string;
22
+ image?: string | null | undefined;
23
+ twoFactorEnabled: boolean | null | undefined;
24
+ banned: boolean | null | undefined;
25
+ role?: string | null | undefined;
26
+ banReason?: string | null | undefined;
27
+ banExpires?: Date | null | undefined;
28
+ lastActiveAt?: Date | null | undefined;
29
+ };
30
+ organization: {
31
+ id: string;
32
+ title: string;
33
+ slug: string | null;
34
+ avatarUrl: string | null;
35
+ createdAt: Date;
36
+ metadata: string | null;
37
+ settings: unknown;
38
+ };
39
+ }>>;
40
+ export default _default;
@@ -0,0 +1,59 @@
1
+ import { createError, useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getCookie, readBody, setCookie } from "h3";
3
+ import { createNuxtAuthClient } from "../../../shared.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const authConfig = config.public.telaAuth;
7
+ const accessToken = getCookie(event, "tela-access-token");
8
+ if (!accessToken) {
9
+ throw createError({
10
+ statusCode: 401,
11
+ statusMessage: "Unauthorized",
12
+ message: "No access token found"
13
+ });
14
+ }
15
+ const body = await readBody(event);
16
+ if (!body?.organizationId) {
17
+ throw createError({
18
+ statusCode: 400,
19
+ statusMessage: "Bad Request",
20
+ message: "Organization ID is required"
21
+ });
22
+ }
23
+ try {
24
+ const authClient = createNuxtAuthClient(
25
+ authConfig.apiUrl,
26
+ () => accessToken
27
+ );
28
+ const { accessToken: newAccessToken, refreshToken: newRefreshToken, user, organization } = await authClient.application.switchOrganization(body.organizationId, accessToken);
29
+ setCookie(event, "tela-access-token", newAccessToken, {
30
+ secure: !import.meta.dev,
31
+ sameSite: "lax",
32
+ maxAge: 60 * 15,
33
+ // 15 minutes
34
+ priority: "high",
35
+ path: "/"
36
+ });
37
+ setCookie(event, "tela-refresh-token", newRefreshToken, {
38
+ secure: !import.meta.dev,
39
+ sameSite: "lax",
40
+ httpOnly: true,
41
+ maxAge: 60 * 60 * 24 * 7,
42
+ // 7 days
43
+ priority: "high",
44
+ path: "/"
45
+ });
46
+ return {
47
+ success: true,
48
+ user,
49
+ organization
50
+ };
51
+ } catch (error) {
52
+ console.error("[Auth Switch Org] Failed to switch organization:", error);
53
+ throw createError({
54
+ statusCode: 500,
55
+ statusMessage: "Internal Server Error",
56
+ message: "Failed to switch organization"
57
+ });
58
+ }
59
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
- "version": "2.1.4",
3
+ "version": "2.2.0",
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": "1.7.4",
34
+ "@meistrari/auth-core": "1.8.0",
35
35
  "jose": "6.1.3"
36
36
  },
37
37
  "peerDependencies": {
@@ -1,2 +0,0 @@
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;
@@ -1,35 +0,0 @@
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="callback-screen">
25
- <div class="callback-square">
26
- <div class="callback-border-mask">
27
- <div class="callback-border-traveler" />
28
- </div>
29
- </div>
30
- </div>
31
- </template>
32
-
33
- <style scoped>
34
- .callback-screen{align-items:center;background:#fff;display:flex;height:100dvh;inset:0;justify-content:center;position:fixed;width:100dvw;z-index:9999}.callback-square{background:#fff;border:4px solid #9ca1a9;height:2rem;position:relative;width:2rem}.callback-border-mask{border:4px solid transparent;inset:-4px;mask-clip:padding-box,border-box;-webkit-mask-clip:padding-box,border-box;-webkit-mask-composite:source-in,xor;mask-composite:intersect;-webkit-mask-composite:source-in;mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000);-webkit-mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000);position:absolute}.callback-border-traveler{animation:border-travel .8s ease-in-out infinite;aspect-ratio:1;background:#000;height:1rem;offset-path:rect(0 auto auto 0);position:absolute;width:40px}@keyframes border-travel{0%{offset-distance:0}to{offset-distance:100%}}
35
- </style>
@@ -1,2 +0,0 @@
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;