@meistrari/auth-nuxt 1.0.5 → 1.1.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/README.md +2 -2
- package/dist/module.d.mts +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -2
- package/dist/runtime/composables/application-auth.d.ts +69 -0
- package/dist/runtime/composables/application-auth.js +171 -0
- package/dist/runtime/composables/organization.d.ts +2 -3
- package/dist/runtime/composables/organization.js +2 -13
- package/dist/runtime/composables/state.d.ts +34 -7
- package/dist/runtime/composables/state.js +8 -0
- package/dist/runtime/plugins/application-token-refresh.js +134 -0
- package/dist/runtime/plugins/handshake.d.ts +2 -0
- package/dist/runtime/{plugin.js → plugins/handshake.js} +3 -3
- package/dist/runtime/shared.d.ts +1 -1
- package/dist/runtime/shared.js +10 -1
- package/package.json +2 -2
- /package/dist/runtime/{plugin.d.ts → plugins/application-token-refresh.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ await signInWithEmailAndPassword({
|
|
|
99
99
|
```typescript
|
|
100
100
|
await signInWithSocialProvider({
|
|
101
101
|
provider: 'google', // or 'microsoft'
|
|
102
|
-
callbackURL: '/
|
|
102
|
+
callbackURL: '/',
|
|
103
103
|
errorCallbackURL: '/login?error=true'
|
|
104
104
|
})
|
|
105
105
|
```
|
|
@@ -108,7 +108,7 @@ await signInWithSocialProvider({
|
|
|
108
108
|
```typescript
|
|
109
109
|
await signInWithSaml({
|
|
110
110
|
email: 'user@example.com',
|
|
111
|
-
callbackURL: '/
|
|
111
|
+
callbackURL: '/',
|
|
112
112
|
errorCallbackURL: '/login?error=true'
|
|
113
113
|
})
|
|
114
114
|
```
|
package/dist/module.d.mts
CHANGED
|
@@ -7,6 +7,16 @@ interface ModuleOptions {
|
|
|
7
7
|
jwtCookieName: string;
|
|
8
8
|
/** Skip default server middleware */
|
|
9
9
|
skipServerMiddleware: boolean;
|
|
10
|
+
application?: {
|
|
11
|
+
/** Whether to enable application authentication */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Auth Dashboard URL */
|
|
14
|
+
dashboardUrl: string;
|
|
15
|
+
/** The ID of the application to authenticate with */
|
|
16
|
+
applicationId: string;
|
|
17
|
+
/** The redirect URI to redirect to after authentication. Must be registered in the application's settings. */
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
};
|
|
10
20
|
}
|
|
11
21
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
12
22
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver,
|
|
1
|
+
import { defineNuxtModule, createResolver, addImports, addPlugin, addServerHandler } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const module$1 = defineNuxtModule({
|
|
4
4
|
meta: {
|
|
@@ -12,6 +12,15 @@ const module$1 = defineNuxtModule({
|
|
|
12
12
|
setup(options, nuxt) {
|
|
13
13
|
const resolver = createResolver(import.meta.url);
|
|
14
14
|
nuxt.options.runtimeConfig.public.telaAuth = options;
|
|
15
|
+
if (options.application?.enabled) {
|
|
16
|
+
addImports({
|
|
17
|
+
name: "useTelaApplicationAuth",
|
|
18
|
+
as: "useTelaApplicationAuth",
|
|
19
|
+
from: resolver.resolve("runtime/composables/application-auth")
|
|
20
|
+
});
|
|
21
|
+
addPlugin(resolver.resolve("./runtime/plugins/application-token-refresh"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
15
24
|
if (!options.skipServerMiddleware) {
|
|
16
25
|
addServerHandler({
|
|
17
26
|
route: "",
|
|
@@ -33,7 +42,7 @@ const module$1 = defineNuxtModule({
|
|
|
33
42
|
as: "useTelaApiKey",
|
|
34
43
|
from: resolver.resolve("runtime/composables/api-key")
|
|
35
44
|
});
|
|
36
|
-
addPlugin(resolver.resolve("./runtime/
|
|
45
|
+
addPlugin(resolver.resolve("./runtime/plugins/handshake"));
|
|
37
46
|
}
|
|
38
47
|
});
|
|
39
48
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for managing Tela application authentication with OAuth 2.0 PKCE flow
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to:
|
|
5
|
+
* - Initiate OAuth login flow with PKCE
|
|
6
|
+
* - Exchange authorization codes for tokens
|
|
7
|
+
* - Automatically refresh access tokens
|
|
8
|
+
* - Manage user session state
|
|
9
|
+
* - Handle logout
|
|
10
|
+
*
|
|
11
|
+
* @returns An object containing authentication state and methods
|
|
12
|
+
* @throws {Error} If auth dashboard URL is not configured
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const auth = useTelaApplicationAuth()
|
|
17
|
+
*
|
|
18
|
+
* // Initiate login
|
|
19
|
+
* await auth.login({
|
|
20
|
+
* applicationId: 'app-123',
|
|
21
|
+
* redirectUri: 'https://example.com/callback'
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Initialize session (handles OAuth callback and token refresh)
|
|
25
|
+
* await auth.initSession()
|
|
26
|
+
*
|
|
27
|
+
* // Access user state
|
|
28
|
+
* console.log(auth.user.value)
|
|
29
|
+
* console.log(auth.activeOrganization.value)
|
|
30
|
+
*
|
|
31
|
+
* // Logout
|
|
32
|
+
* await auth.logout()
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function useTelaApplicationAuth(): {
|
|
36
|
+
login: () => Promise<void>;
|
|
37
|
+
logout: () => Promise<void>;
|
|
38
|
+
initSession: () => Promise<void>;
|
|
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>;
|
|
69
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { navigateTo, useCookie, useRoute, useRuntimeConfig } from "#app";
|
|
2
|
+
import { createNuxtAuthClient } from "../shared.js";
|
|
3
|
+
import { useApplicationSessionState } from "./state.js";
|
|
4
|
+
import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError } from "@meistrari/auth-core";
|
|
5
|
+
const SEVEN_DAYS = 60 * 60 * 24 * 7;
|
|
6
|
+
const FIFTEEN_MINUTES = 60 * 15;
|
|
7
|
+
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
|
+
function mapOrganization(organization) {
|
|
37
|
+
return {
|
|
38
|
+
name: organization.title,
|
|
39
|
+
id: organization.id,
|
|
40
|
+
createdAt: organization.createdAt,
|
|
41
|
+
logo: organization.avatarUrl,
|
|
42
|
+
metadata: organization.metadata,
|
|
43
|
+
slug: organization.slug ?? ""
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function useTelaApplicationAuth() {
|
|
47
|
+
const appConfig = useRuntimeConfig().public.telaAuth;
|
|
48
|
+
const query = useRoute().query;
|
|
49
|
+
const accessTokenCookie = useCookie("tela-access-token", {
|
|
50
|
+
secure: true,
|
|
51
|
+
sameSite: "lax",
|
|
52
|
+
maxAge: FIFTEEN_MINUTES
|
|
53
|
+
});
|
|
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
|
+
const state = useApplicationSessionState();
|
|
61
|
+
if (!appConfig.application?.dashboardUrl) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"[Tela Auth SDK] Auth dashboard URL is not configured, but it is required to use application authentication."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!appConfig.application?.applicationId) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"[Tela Auth SDK] Application ID is not configured, but it is required to use application authentication."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!appConfig.application?.redirectUri) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"[Tela Auth SDK] Redirect URI is not configured, but it is required to use application authentication."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const { applicationId, redirectUri } = appConfig.application;
|
|
77
|
+
async function login() {
|
|
78
|
+
if (import.meta.server) {
|
|
79
|
+
throw new AuthorizationFlowError("The login function can only be called on the client side.");
|
|
80
|
+
}
|
|
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);
|
|
88
|
+
const url = new URL("/applications/login", appConfig.application?.dashboardUrl);
|
|
89
|
+
url.searchParams.set("application_id", applicationId);
|
|
90
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
91
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
92
|
+
url.searchParams.set("state", stateKey);
|
|
93
|
+
navigateTo(url.toString(), { external: true });
|
|
94
|
+
}
|
|
95
|
+
async function logout() {
|
|
96
|
+
accessTokenCookie.value = null;
|
|
97
|
+
refreshTokenCookie.value = null;
|
|
98
|
+
state.user.value = null;
|
|
99
|
+
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
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function refreshToken() {
|
|
139
|
+
if (!refreshTokenCookie.value) {
|
|
140
|
+
throw new RefreshTokenExpiredError();
|
|
141
|
+
}
|
|
142
|
+
const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.refreshAccessToken();
|
|
143
|
+
accessTokenCookie.value = accessToken;
|
|
144
|
+
refreshTokenCookie.value = refreshToken2;
|
|
145
|
+
state.user.value = user;
|
|
146
|
+
state.activeOrganization.value = mapOrganization(organization);
|
|
147
|
+
}
|
|
148
|
+
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();
|
|
155
|
+
}
|
|
156
|
+
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) {
|
|
162
|
+
await refreshToken();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
login,
|
|
168
|
+
logout,
|
|
169
|
+
initSession
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { Ref } from 'vue';
|
|
2
2
|
import type { CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateOrganizationPayload, UpdateTeamPayload } from '@meistrari/auth-core';
|
|
3
|
-
import type { FullOrganizationWithRelations } from './state.js';
|
|
4
3
|
export interface UseTelaOrganizationReturn {
|
|
5
4
|
/** Reactive reference to the active organization with members, invitations, and teams. */
|
|
6
|
-
activeOrganization: Ref<
|
|
5
|
+
activeOrganization: Ref<FullOrganization | null>;
|
|
7
6
|
/** Reactive reference to the current user's membership in the active organization. */
|
|
8
7
|
activeMember: Ref<Member | null>;
|
|
9
8
|
/**
|
|
10
9
|
* Retrieves the active organization for the current user session and updates the active organization state.
|
|
11
10
|
* @returns The active organization with members, invitations, and teams
|
|
12
11
|
*/
|
|
13
|
-
getActiveOrganization: () => Promise<
|
|
12
|
+
getActiveOrganization: () => Promise<FullOrganization | undefined>;
|
|
14
13
|
/**
|
|
15
14
|
* Lists all organizations for the current user session.
|
|
16
15
|
* @returns A list of organizations
|
|
@@ -10,14 +10,7 @@ export function useTelaOrganization() {
|
|
|
10
10
|
if (!session.value?.activeOrganizationId) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
const organization = await authClient.organization.getOrganization(
|
|
14
|
-
session.value?.activeOrganizationId,
|
|
15
|
-
{
|
|
16
|
-
includeMembers: true,
|
|
17
|
-
includeInvitations: true,
|
|
18
|
-
includeTeams: true
|
|
19
|
-
}
|
|
20
|
-
);
|
|
13
|
+
const organization = await authClient.organization.getOrganization(session.value?.activeOrganizationId);
|
|
21
14
|
activeOrganization.value = organization;
|
|
22
15
|
return organization;
|
|
23
16
|
}
|
|
@@ -27,11 +20,7 @@ export function useTelaOrganization() {
|
|
|
27
20
|
async function setActiveOrganization(id) {
|
|
28
21
|
await authClient.organization.setActiveOrganization(id);
|
|
29
22
|
const [organization, { token }] = await Promise.all([
|
|
30
|
-
authClient.organization.getOrganization(id,
|
|
31
|
-
includeMembers: true,
|
|
32
|
-
includeInvitations: true,
|
|
33
|
-
includeTeams: true
|
|
34
|
-
}),
|
|
23
|
+
authClient.organization.getOrganization(id),
|
|
35
24
|
authClient.session.getToken()
|
|
36
25
|
]);
|
|
37
26
|
useCookie(jwtCookieName).value = token;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type { FullOrganization,
|
|
2
|
-
export type FullOrganizationWithRelations = FullOrganization & {
|
|
3
|
-
members: Member[];
|
|
4
|
-
invitations: Invitation[];
|
|
5
|
-
teams: Team[];
|
|
6
|
-
};
|
|
1
|
+
import type { FullOrganization, Member } from '@meistrari/auth-core';
|
|
7
2
|
/**
|
|
8
3
|
* Shared state for session management.
|
|
9
4
|
* This module provides access to session-related state without creating circular dependencies.
|
|
@@ -69,6 +64,38 @@ export declare function useSessionState(): {
|
|
|
69
64
|
* This module provides access to organization-related state without creating circular dependencies.
|
|
70
65
|
*/
|
|
71
66
|
export declare function useOrganizationState(): {
|
|
72
|
-
activeOrganization: import("vue").Ref<
|
|
67
|
+
activeOrganization: import("vue").Ref<FullOrganization | null, FullOrganization | null>;
|
|
73
68
|
activeMember: import("vue").Ref<Member | null, Member | null>;
|
|
74
69
|
};
|
|
70
|
+
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>;
|
|
100
|
+
activeOrganization: import("vue").Ref<Omit<FullOrganization, "members" | "invitations" | "teams"> | null, Omit<FullOrganization, "members" | "invitations" | "teams"> | null>;
|
|
101
|
+
};
|
|
@@ -15,3 +15,11 @@ export function useOrganizationState() {
|
|
|
15
15
|
activeMember
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
export function useApplicationSessionState() {
|
|
19
|
+
const user = useState("user", () => null);
|
|
20
|
+
const activeOrganization = useState("activeOrganization", () => null);
|
|
21
|
+
return {
|
|
22
|
+
user,
|
|
23
|
+
activeOrganization
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useCookie, useRuntimeConfig } from "#app";
|
|
2
|
+
import { isTokenExpired } from "@meistrari/auth-core";
|
|
3
|
+
import { useApplicationSessionState } from "../composables/state.js";
|
|
4
|
+
import { createNuxtAuthClient } from "../shared.js";
|
|
5
|
+
import { watch } from "vue";
|
|
6
|
+
const SEVEN_DAYS = 60 * 60 * 24 * 7;
|
|
7
|
+
const FIFTEEN_MINUTES = 60 * 15;
|
|
8
|
+
const TWO_MINUTES = 2 * 60 * 1e3;
|
|
9
|
+
function parseTokenExpiry(token) {
|
|
10
|
+
try {
|
|
11
|
+
const tokenParts = token.split(".");
|
|
12
|
+
const payloadPart = tokenParts[1];
|
|
13
|
+
if (!payloadPart) return null;
|
|
14
|
+
const payload = JSON.parse(atob(payloadPart));
|
|
15
|
+
if (!payload.exp) return null;
|
|
16
|
+
return payload.exp * 1e3;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function mapOrganization(organization) {
|
|
22
|
+
return {
|
|
23
|
+
name: organization.title,
|
|
24
|
+
id: organization.id,
|
|
25
|
+
createdAt: organization.createdAt,
|
|
26
|
+
logo: organization.avatarUrl,
|
|
27
|
+
metadata: organization.metadata,
|
|
28
|
+
slug: organization.slug ?? ""
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export default defineNuxtPlugin({
|
|
32
|
+
name: "tela-application-token-refresh",
|
|
33
|
+
enforce: "post",
|
|
34
|
+
env: {
|
|
35
|
+
islands: false
|
|
36
|
+
},
|
|
37
|
+
async setup() {
|
|
38
|
+
const appConfig = useRuntimeConfig().public.telaAuth;
|
|
39
|
+
const state = useApplicationSessionState();
|
|
40
|
+
const accessTokenCookie = useCookie("tela-access-token", {
|
|
41
|
+
secure: true,
|
|
42
|
+
sameSite: "lax",
|
|
43
|
+
maxAge: FIFTEEN_MINUTES
|
|
44
|
+
});
|
|
45
|
+
const refreshTokenCookie = useCookie("tela-refresh-token", {
|
|
46
|
+
secure: true,
|
|
47
|
+
sameSite: "lax",
|
|
48
|
+
maxAge: SEVEN_DAYS
|
|
49
|
+
});
|
|
50
|
+
const authClient = createNuxtAuthClient(appConfig.apiUrl, () => null, () => refreshTokenCookie.value ?? null);
|
|
51
|
+
let tokenRefreshInterval = null;
|
|
52
|
+
let isRefreshing = false;
|
|
53
|
+
let retryCount = 0;
|
|
54
|
+
const MAX_RETRIES = 3;
|
|
55
|
+
async function refreshToken() {
|
|
56
|
+
if (isRefreshing) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
isRefreshing = true;
|
|
60
|
+
try {
|
|
61
|
+
const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.refreshAccessToken();
|
|
62
|
+
accessTokenCookie.value = accessToken;
|
|
63
|
+
refreshTokenCookie.value = refreshToken2;
|
|
64
|
+
state.user.value = user;
|
|
65
|
+
state.activeOrganization.value = mapOrganization(organization);
|
|
66
|
+
} finally {
|
|
67
|
+
isRefreshing = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function logout() {
|
|
71
|
+
accessTokenCookie.value = null;
|
|
72
|
+
refreshTokenCookie.value = null;
|
|
73
|
+
state.user.value = null;
|
|
74
|
+
state.activeOrganization.value = null;
|
|
75
|
+
}
|
|
76
|
+
async function scheduleTokenRefresh() {
|
|
77
|
+
if (tokenRefreshInterval) {
|
|
78
|
+
clearTimeout(tokenRefreshInterval);
|
|
79
|
+
tokenRefreshInterval = null;
|
|
80
|
+
}
|
|
81
|
+
if (!accessTokenCookie.value) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (isTokenExpired(accessTokenCookie.value, TWO_MINUTES)) {
|
|
85
|
+
await refreshToken();
|
|
86
|
+
}
|
|
87
|
+
const expiry = parseTokenExpiry(accessTokenCookie.value);
|
|
88
|
+
if (!expiry) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const nextRefresh = Math.max(expiry - TWO_MINUTES - Date.now(), 0);
|
|
92
|
+
tokenRefreshInterval = window.setTimeout(refreshToken, nextRefresh);
|
|
93
|
+
}
|
|
94
|
+
if (import.meta.server) {
|
|
95
|
+
if (accessTokenCookie.value) {
|
|
96
|
+
try {
|
|
97
|
+
const data = await authClient.application.whoAmI(accessTokenCookie.value);
|
|
98
|
+
state.user.value = data.user;
|
|
99
|
+
state.activeOrganization.value = mapOrganization(data.organization);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[Tela Auth SDK] Failed to get user and organization:`, error.message);
|
|
102
|
+
if (!refreshTokenCookie.value) {
|
|
103
|
+
console.error(`[Tela Auth SDK] Missing refresh token, logging out...`);
|
|
104
|
+
logout();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const refreshTokenValue = refreshTokenCookie.value;
|
|
108
|
+
try {
|
|
109
|
+
await refreshToken();
|
|
110
|
+
} catch (error2) {
|
|
111
|
+
console.error(`[Tela Auth SDK] Failed to refresh token ${refreshTokenValue}...:`, error2.message);
|
|
112
|
+
logout();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!accessTokenCookie.value && refreshTokenCookie.value) {
|
|
117
|
+
try {
|
|
118
|
+
await refreshToken();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`[Tela Auth SDK] Failed to refresh token ${refreshTokenCookie.value}...:`, error.message);
|
|
121
|
+
logout();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (import.meta.client) {
|
|
127
|
+
watch(refreshTokenCookie, async (newVal) => {
|
|
128
|
+
if (newVal) {
|
|
129
|
+
await scheduleTokenRefresh();
|
|
130
|
+
}
|
|
131
|
+
}, { immediate: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineNuxtPlugin, useCookie, useRuntimeConfig, useRequestURL } from "#app";
|
|
2
2
|
import { watch } from "vue";
|
|
3
|
-
import { useTelaSession } from "
|
|
4
|
-
import { createNuxtAuthClient } from "
|
|
3
|
+
import { useTelaSession } from "../composables/session.js";
|
|
4
|
+
import { createNuxtAuthClient } from "../shared.js";
|
|
5
5
|
import { useRoute, navigateTo } from "#imports";
|
|
6
6
|
export default defineNuxtPlugin({
|
|
7
|
-
name: "tela-auth",
|
|
7
|
+
name: "tela-auth-handshake",
|
|
8
8
|
enforce: "pre",
|
|
9
9
|
env: {
|
|
10
10
|
islands: false
|
package/dist/runtime/shared.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { AuthClient } from '@meistrari/auth-core';
|
|
2
|
-
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null): AuthClient;
|
|
2
|
+
export declare function createNuxtAuthClient(apiUrl: string, getAuthToken: () => string | null, getRefreshToken?: () => string | null): AuthClient;
|
package/dist/runtime/shared.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthClient } from "@meistrari/auth-core";
|
|
2
2
|
import { version } from "../../package.json";
|
|
3
|
-
export function createNuxtAuthClient(apiUrl, getAuthToken) {
|
|
3
|
+
export function createNuxtAuthClient(apiUrl, getAuthToken, getRefreshToken = () => null) {
|
|
4
4
|
const serviceName = typeof process !== "undefined" ? process.env.SERVICE_NAME : "";
|
|
5
5
|
const userAgent = `auth-sdk:nuxt:${version}${serviceName ? `@${serviceName}` : ""}`;
|
|
6
6
|
return new AuthClient(apiUrl, {
|
|
@@ -14,6 +14,15 @@ export function createNuxtAuthClient(apiUrl, getAuthToken) {
|
|
|
14
14
|
if (token && !isTokenRequest) {
|
|
15
15
|
context.headers.set("Authorization", `Bearer ${token}`);
|
|
16
16
|
}
|
|
17
|
+
const isRefreshTokenRequest = requestUrl.pathname.endsWith("/api/auth/applications/token/refresh");
|
|
18
|
+
if (isRefreshTokenRequest) {
|
|
19
|
+
const refreshToken = getRefreshToken();
|
|
20
|
+
if (refreshToken) {
|
|
21
|
+
const cookie = context.headers.get("Cookie");
|
|
22
|
+
const newCookie = cookie ? `${cookie}; tela-refresh-token=${refreshToken}` : `tela-refresh-token=${refreshToken}`;
|
|
23
|
+
context.headers.set("Cookie", newCookie);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
});
|
|
19
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/auth-nuxt",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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.
|
|
34
|
+
"@meistrari/auth-core": "1.6.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"nuxt": "^3.0.0 || ^4.0.0"
|
|
File without changes
|