@meistrari/auth-nuxt 2.1.3 → 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/README.md +34 -30
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +34 -11
- package/dist/runtime/components/tela-role.vue +6 -6
- package/dist/runtime/composables/application-auth.d.ts +11 -1
- package/dist/runtime/composables/application-auth.js +37 -112
- package/dist/runtime/composables/organization.d.ts +1 -1
- package/dist/runtime/composables/organization.js +2 -1
- package/dist/runtime/composables/state.d.ts +56 -22
- package/dist/runtime/plugins/application-token-refresh.js +19 -22
- package/dist/runtime/plugins/auth-guard.js +4 -4
- package/dist/runtime/plugins/handshake.js +4 -4
- package/dist/runtime/server/middleware/auth.js +2 -2
- package/dist/runtime/server/routes/auth/callback.d.ts +12 -0
- package/dist/runtime/server/routes/auth/callback.js +49 -0
- package/dist/runtime/server/routes/auth/login.d.ts +18 -0
- package/dist/runtime/server/routes/auth/login.js +41 -0
- package/dist/runtime/server/routes/auth/logout.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout.js +26 -0
- package/dist/runtime/server/routes/auth/organizations.d.ts +16 -0
- package/dist/runtime/server/routes/auth/organizations.js +40 -0
- package/dist/runtime/server/routes/auth/refresh.d.ts +39 -0
- package/dist/runtime/server/routes/auth/refresh.js +54 -0
- package/dist/runtime/server/routes/auth/switch-organization.d.ts +40 -0
- package/dist/runtime/server/routes/auth/switch-organization.js +59 -0
- package/dist/runtime/server/utils/require-auth.js +2 -2
- package/dist/runtime/types/page-meta.d.ts +7 -8
- package/package.json +51 -51
- package/dist/runtime/pages/callback.d.vue.ts +0 -2
- package/dist/runtime/pages/callback.vue +0 -35
- package/dist/runtime/pages/callback.vue.d.ts +0 -2
package/README.md
CHANGED
|
@@ -54,18 +54,20 @@ const token = await getToken()
|
|
|
54
54
|
|
|
55
55
|
// Sign out
|
|
56
56
|
await signOut(() => {
|
|
57
|
-
|
|
57
|
+
console.log('User signed out')
|
|
58
58
|
})
|
|
59
59
|
</script>
|
|
60
60
|
|
|
61
61
|
<template>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
<div v-if="user">
|
|
63
|
+
Welcome, {{ user.name }}!
|
|
64
|
+
<button @click="signOut">
|
|
65
|
+
Sign Out
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div v-else>
|
|
69
|
+
Please sign in
|
|
70
|
+
</div>
|
|
69
71
|
</template>
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -120,11 +122,11 @@ Manages organizations, members, invitations, and teams.
|
|
|
120
122
|
```vue
|
|
121
123
|
<script setup>
|
|
122
124
|
const {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
activeOrganization,
|
|
126
|
+
activeMember,
|
|
127
|
+
getActiveOrganization,
|
|
128
|
+
setActiveOrganization,
|
|
129
|
+
inviteUserToOrganization
|
|
128
130
|
} = useTelaOrganization()
|
|
129
131
|
|
|
130
132
|
// Get the active organization
|
|
@@ -135,16 +137,16 @@ await setActiveOrganization('org-id')
|
|
|
135
137
|
|
|
136
138
|
// Invite a user
|
|
137
139
|
await inviteUserToOrganization({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
userEmail: 'user@example.com',
|
|
141
|
+
role: 'member'
|
|
140
142
|
})
|
|
141
143
|
</script>
|
|
142
144
|
|
|
143
145
|
<template>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
<div v-if="activeOrganization">
|
|
147
|
+
<h2>{{ activeOrganization.name }}</h2>
|
|
148
|
+
<p>{{ activeOrganization.members.length }} members</p>
|
|
149
|
+
</div>
|
|
148
150
|
</template>
|
|
149
151
|
```
|
|
150
152
|
|
|
@@ -216,9 +218,9 @@ Manages API keys for programmatic access.
|
|
|
216
218
|
```vue
|
|
217
219
|
<script setup>
|
|
218
220
|
const {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
listApiKeys,
|
|
222
|
+
createApiKey,
|
|
223
|
+
deleteApiKey
|
|
222
224
|
} = useTelaApiKey()
|
|
223
225
|
|
|
224
226
|
// List all API keys
|
|
@@ -226,10 +228,10 @@ const apiKeys = await listApiKeys()
|
|
|
226
228
|
|
|
227
229
|
// Create a new API key
|
|
228
230
|
const newKey = await createApiKey({
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
name: 'Production API Key',
|
|
232
|
+
expiresIn: '90d',
|
|
233
|
+
prefix: 'prod',
|
|
234
|
+
metadata: { environment: 'production' }
|
|
233
235
|
})
|
|
234
236
|
|
|
235
237
|
// Delete an API key
|
|
@@ -237,10 +239,12 @@ await deleteApiKey('key-id')
|
|
|
237
239
|
</script>
|
|
238
240
|
|
|
239
241
|
<template>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
<div v-for="key in apiKeys" :key="key.id">
|
|
243
|
+
<span>{{ key.name }}</span>
|
|
244
|
+
<button @click="deleteApiKey(key.id)">
|
|
245
|
+
Delete
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
244
248
|
</template>
|
|
245
249
|
```
|
|
246
250
|
|
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
|
|
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
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addImports,
|
|
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,15 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useTelaApplicationAuth } from "../composables/application-auth";
|
|
3
|
-
const { user } = useTelaApplicationAuth();
|
|
4
3
|
defineProps({
|
|
5
4
|
allowedRoles: { type: Array, required: true }
|
|
6
5
|
});
|
|
6
|
+
const { user } = useTelaApplicationAuth();
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<template>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
15
|
</template>
|
|
@@ -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<
|
|
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,
|
|
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:
|
|
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,100 +41,61 @@ 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
|
-
|
|
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);
|
|
91
48
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
92
49
|
url.searchParams.set("state", stateKey);
|
|
93
|
-
navigateTo(url.toString(), { external: true });
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import type { Ref } from 'vue';
|
|
2
1
|
import type { CreateTeamPayload, FullOrganization, Invitation, InviteUserToOrganizationOptions, ListMembersOptions, Member, RemoveUserFromOrganizationOptions, Team, TeamMember, UpdateMemberRoleOptions, UpdateOrganizationPayload, UpdateTeamPayload } from '@meistrari/auth-core';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
3
|
export interface UseTelaOrganizationReturn {
|
|
4
4
|
/** Reactive reference to the active organization with members, invitations, and teams. */
|
|
5
5
|
activeOrganization: Ref<FullOrganization | null>;
|
|
@@ -10,7 +10,8 @@ export function useTelaOrganization() {
|
|
|
10
10
|
if (!session.value?.activeOrganizationId) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
13
|
+
const activeOrganizationId = session.value?.activeOrganizationId;
|
|
14
|
+
const organization = await authClient.organization.getOrganization(activeOrganizationId);
|
|
14
15
|
activeOrganization.value = organization;
|
|
15
16
|
return organization;
|
|
16
17
|
}
|
|
@@ -34,29 +34,63 @@ export declare function useSessionState(): {
|
|
|
34
34
|
lastActiveAt?: Date | null | undefined;
|
|
35
35
|
} | null>;
|
|
36
36
|
session: import("vue").Ref<{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
user: {
|
|
38
|
+
id: string;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
updatedAt: Date;
|
|
41
|
+
email: string;
|
|
42
|
+
emailVerified: boolean;
|
|
43
|
+
name: string;
|
|
44
|
+
image?: string | null | undefined;
|
|
45
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
46
|
+
banned: boolean | null | undefined;
|
|
47
|
+
role?: string | null | undefined;
|
|
48
|
+
banReason?: string | null | undefined;
|
|
49
|
+
banExpires?: Date | null | undefined;
|
|
50
|
+
lastActiveAt?: Date | null | undefined;
|
|
51
|
+
};
|
|
52
|
+
session: {
|
|
53
|
+
id: string;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
updatedAt: Date;
|
|
56
|
+
userId: string;
|
|
57
|
+
expiresAt: Date;
|
|
58
|
+
token: string;
|
|
59
|
+
ipAddress?: string | null | undefined;
|
|
60
|
+
userAgent?: string | null | undefined;
|
|
61
|
+
activeOrganizationId?: string | null | undefined;
|
|
62
|
+
activeTeamId?: string | null | undefined;
|
|
63
|
+
impersonatedBy?: string | null | undefined;
|
|
64
|
+
};
|
|
48
65
|
} | null, {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
user: {
|
|
67
|
+
id: string;
|
|
68
|
+
createdAt: Date;
|
|
69
|
+
updatedAt: Date;
|
|
70
|
+
email: string;
|
|
71
|
+
emailVerified: boolean;
|
|
72
|
+
name: string;
|
|
73
|
+
image?: string | null | undefined;
|
|
74
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
75
|
+
banned: boolean | null | undefined;
|
|
76
|
+
role?: string | null | undefined;
|
|
77
|
+
banReason?: string | null | undefined;
|
|
78
|
+
banExpires?: Date | null | undefined;
|
|
79
|
+
lastActiveAt?: Date | null | undefined;
|
|
80
|
+
};
|
|
81
|
+
session: {
|
|
82
|
+
id: string;
|
|
83
|
+
createdAt: Date;
|
|
84
|
+
updatedAt: Date;
|
|
85
|
+
userId: string;
|
|
86
|
+
expiresAt: Date;
|
|
87
|
+
token: string;
|
|
88
|
+
ipAddress?: string | null | undefined;
|
|
89
|
+
userAgent?: string | null | undefined;
|
|
90
|
+
activeOrganizationId?: string | null | undefined;
|
|
91
|
+
activeTeamId?: string | null | undefined;
|
|
92
|
+
impersonatedBy?: string | null | undefined;
|
|
93
|
+
};
|
|
60
94
|
} | null>;
|
|
61
95
|
};
|
|
62
96
|
/**
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { defineNuxtPlugin, useCookie, useRuntimeConfig } from "#app";
|
|
2
|
-
import { isTokenExpired
|
|
3
|
-
import {
|
|
2
|
+
import { isTokenExpired } from "@meistrari/auth-core";
|
|
3
|
+
import { decodeJwt } from "jose";
|
|
4
|
+
import { useTelaApplicationAuth } from "../composables/application-auth.js";
|
|
4
5
|
import { useApplicationSessionState } from "../composables/state.js";
|
|
5
6
|
import { createNuxtAuthClient } from "../shared.js";
|
|
6
|
-
import { useTelaApplicationAuth } from "../composables/application-auth.js";
|
|
7
7
|
const SEVEN_DAYS = 60 * 60 * 24 * 7;
|
|
8
8
|
const FIFTEEN_MINUTES = 60 * 15;
|
|
9
9
|
const TWO_MINUTES = 2 * 60 * 1e3;
|
|
10
10
|
function parseTokenExpiry(token) {
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
64
|
-
|
|
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 {
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|
|
@@ -99,7 +100,7 @@ export default defineNuxtPlugin({
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
const nextRefresh = Math.max(expiry - TWO_MINUTES - Date.now(), 0);
|
|
102
|
-
tokenRefreshInterval = window.setTimeout(refreshToken, nextRefresh);
|
|
103
|
+
tokenRefreshInterval = window.setTimeout(() => void refreshToken(), nextRefresh);
|
|
103
104
|
}
|
|
104
105
|
if (import.meta.server) {
|
|
105
106
|
if (accessTokenCookie.value) {
|
|
@@ -134,11 +135,7 @@ export default defineNuxtPlugin({
|
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
137
|
if (import.meta.client) {
|
|
137
|
-
|
|
138
|
-
if (newVal) {
|
|
139
|
-
await scheduleTokenRefresh();
|
|
140
|
-
}
|
|
141
|
-
}, { immediate: true });
|
|
138
|
+
void scheduleTokenRefresh();
|
|
142
139
|
}
|
|
143
140
|
}
|
|
144
141
|
});
|