@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 +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +34 -11
- package/dist/runtime/composables/application-auth.d.ts +11 -1
- package/dist/runtime/composables/application-auth.js +36 -111
- package/dist/runtime/plugins/application-token-refresh.js +17 -20
- 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/package.json +2 -2
- 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/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,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,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
|
-
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
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
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
|
|
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
|
}
|
|
@@ -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
|
});
|
|
@@ -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,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.
|
|
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.
|
|
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;
|