@peterbud/nuxt-aegis 1.1.0-alpha
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 +166 -0
- package/dist/module.d.mts +6 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +354 -0
- package/dist/runtime/app/composables/useAuth.d.ts +85 -0
- package/dist/runtime/app/composables/useAuth.js +187 -0
- package/dist/runtime/app/middleware/auth-logged-in.d.ts +16 -0
- package/dist/runtime/app/middleware/auth-logged-in.js +25 -0
- package/dist/runtime/app/middleware/auth-logged-out.d.ts +20 -0
- package/dist/runtime/app/middleware/auth-logged-out.js +17 -0
- package/dist/runtime/app/pages/AuthCallback.d.vue.ts +3 -0
- package/dist/runtime/app/pages/AuthCallback.vue +92 -0
- package/dist/runtime/app/pages/AuthCallback.vue.d.ts +3 -0
- package/dist/runtime/app/plugins/api.client.d.ts +11 -0
- package/dist/runtime/app/plugins/api.client.js +92 -0
- package/dist/runtime/app/plugins/api.server.d.ts +13 -0
- package/dist/runtime/app/plugins/api.server.js +28 -0
- package/dist/runtime/app/plugins/ssr-state.server.d.ts +2 -0
- package/dist/runtime/app/plugins/ssr-state.server.js +13 -0
- package/dist/runtime/app/router.options.d.ts +12 -0
- package/dist/runtime/app/router.options.js +11 -0
- package/dist/runtime/app/utils/logger.d.ts +18 -0
- package/dist/runtime/app/utils/logger.js +48 -0
- package/dist/runtime/app/utils/redirectValidation.d.ts +18 -0
- package/dist/runtime/app/utils/redirectValidation.js +21 -0
- package/dist/runtime/app/utils/routeMatching.d.ts +13 -0
- package/dist/runtime/app/utils/routeMatching.js +10 -0
- package/dist/runtime/app/utils/tokenStore.d.ts +24 -0
- package/dist/runtime/app/utils/tokenStore.js +14 -0
- package/dist/runtime/app/utils/tokenUtils.d.ts +17 -0
- package/dist/runtime/app/utils/tokenUtils.js +4 -0
- package/dist/runtime/server/middleware/auth.d.ts +6 -0
- package/dist/runtime/server/middleware/auth.js +82 -0
- package/dist/runtime/server/plugins/ssr-auth.d.ts +7 -0
- package/dist/runtime/server/plugins/ssr-auth.js +82 -0
- package/dist/runtime/server/providers/auth0.d.ts +12 -0
- package/dist/runtime/server/providers/auth0.js +57 -0
- package/dist/runtime/server/providers/github.d.ts +12 -0
- package/dist/runtime/server/providers/github.js +44 -0
- package/dist/runtime/server/providers/google.d.ts +12 -0
- package/dist/runtime/server/providers/google.js +46 -0
- package/dist/runtime/server/providers/mock.d.ts +37 -0
- package/dist/runtime/server/providers/mock.js +129 -0
- package/dist/runtime/server/providers/oauthBase.d.ts +72 -0
- package/dist/runtime/server/providers/oauthBase.js +183 -0
- package/dist/runtime/server/routes/impersonate.post.d.ts +21 -0
- package/dist/runtime/server/routes/impersonate.post.js +68 -0
- package/dist/runtime/server/routes/logout.post.d.ts +9 -0
- package/dist/runtime/server/routes/logout.post.js +24 -0
- package/dist/runtime/server/routes/me.get.d.ts +6 -0
- package/dist/runtime/server/routes/me.get.js +11 -0
- package/dist/runtime/server/routes/mock/authorize.get.d.ts +29 -0
- package/dist/runtime/server/routes/mock/authorize.get.js +103 -0
- package/dist/runtime/server/routes/mock/token.post.d.ts +31 -0
- package/dist/runtime/server/routes/mock/token.post.js +88 -0
- package/dist/runtime/server/routes/mock/userinfo.get.d.ts +27 -0
- package/dist/runtime/server/routes/mock/userinfo.get.js +59 -0
- package/dist/runtime/server/routes/password/change.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/change.post.js +108 -0
- package/dist/runtime/server/routes/password/login-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/login-verify.get.js +79 -0
- package/dist/runtime/server/routes/password/login.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/login.post.js +66 -0
- package/dist/runtime/server/routes/password/register-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/register-verify.get.js +86 -0
- package/dist/runtime/server/routes/password/register.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/register.post.js +87 -0
- package/dist/runtime/server/routes/password/reset-complete.post.d.ts +4 -0
- package/dist/runtime/server/routes/password/reset-complete.post.js +75 -0
- package/dist/runtime/server/routes/password/reset-request.post.d.ts +5 -0
- package/dist/runtime/server/routes/password/reset-request.post.js +52 -0
- package/dist/runtime/server/routes/password/reset-verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/password/reset-verify.get.js +50 -0
- package/dist/runtime/server/routes/refresh.post.d.ts +8 -0
- package/dist/runtime/server/routes/refresh.post.js +102 -0
- package/dist/runtime/server/routes/token.post.d.ts +28 -0
- package/dist/runtime/server/routes/token.post.js +90 -0
- package/dist/runtime/server/routes/unimpersonate.post.d.ts +16 -0
- package/dist/runtime/server/routes/unimpersonate.post.js +65 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/auth.d.ts +94 -0
- package/dist/runtime/server/utils/auth.js +54 -0
- package/dist/runtime/server/utils/authCodeStore.d.ts +137 -0
- package/dist/runtime/server/utils/authCodeStore.js +123 -0
- package/dist/runtime/server/utils/cookies.d.ts +15 -0
- package/dist/runtime/server/utils/cookies.js +23 -0
- package/dist/runtime/server/utils/customClaims.d.ts +37 -0
- package/dist/runtime/server/utils/customClaims.js +45 -0
- package/dist/runtime/server/utils/handler.d.ts +77 -0
- package/dist/runtime/server/utils/handler.js +7 -0
- package/dist/runtime/server/utils/impersonation.d.ts +48 -0
- package/dist/runtime/server/utils/impersonation.js +259 -0
- package/dist/runtime/server/utils/jwt.d.ts +24 -0
- package/dist/runtime/server/utils/jwt.js +77 -0
- package/dist/runtime/server/utils/logger.d.ts +18 -0
- package/dist/runtime/server/utils/logger.js +49 -0
- package/dist/runtime/server/utils/magicCodeStore.d.ts +27 -0
- package/dist/runtime/server/utils/magicCodeStore.js +66 -0
- package/dist/runtime/server/utils/mockCodeStore.d.ts +89 -0
- package/dist/runtime/server/utils/mockCodeStore.js +71 -0
- package/dist/runtime/server/utils/password.d.ts +33 -0
- package/dist/runtime/server/utils/password.js +48 -0
- package/dist/runtime/server/utils/refreshToken.d.ts +74 -0
- package/dist/runtime/server/utils/refreshToken.js +108 -0
- package/dist/runtime/server/utils/resetSessionStore.d.ts +12 -0
- package/dist/runtime/server/utils/resetSessionStore.js +29 -0
- package/dist/runtime/tasks/cleanup/magic-codes.d.ts +10 -0
- package/dist/runtime/tasks/cleanup/magic-codes.js +79 -0
- package/dist/runtime/tasks/cleanup/refresh-tokens.d.ts +10 -0
- package/dist/runtime/tasks/cleanup/refresh-tokens.js +55 -0
- package/dist/runtime/tasks/cleanup/reset-sessions.d.ts +8 -0
- package/dist/runtime/tasks/cleanup/reset-sessions.js +45 -0
- package/dist/runtime/types/augmentation.d.ts +73 -0
- package/dist/runtime/types/augmentation.js +0 -0
- package/dist/runtime/types/authCode.d.ts +60 -0
- package/dist/runtime/types/authCode.js +0 -0
- package/dist/runtime/types/callbacks.d.ts +54 -0
- package/dist/runtime/types/callbacks.js +0 -0
- package/dist/runtime/types/config.d.ts +129 -0
- package/dist/runtime/types/config.js +0 -0
- package/dist/runtime/types/hooks.d.ts +118 -0
- package/dist/runtime/types/hooks.js +0 -0
- package/dist/runtime/types/index.d.ts +13 -0
- package/dist/runtime/types/index.js +1 -0
- package/dist/runtime/types/providers.d.ts +212 -0
- package/dist/runtime/types/providers.js +0 -0
- package/dist/runtime/types/refresh.d.ts +61 -0
- package/dist/runtime/types/refresh.js +0 -0
- package/dist/runtime/types/routes.d.ts +30 -0
- package/dist/runtime/types/routes.js +0 -0
- package/dist/runtime/types/token.d.ts +182 -0
- package/dist/runtime/types/token.js +0 -0
- package/dist/types.d.mts +7 -0
- package/package.json +80 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useRuntimeConfig, navigateTo, useState, computed } from "#imports";
|
|
2
|
+
import { clearAccessToken } from "../utils/tokenStore.js";
|
|
3
|
+
import { createLogger } from "../utils/logger.js";
|
|
4
|
+
import { validateRedirectPath } from "../utils/redirectValidation.js";
|
|
5
|
+
import { filterTimeSensitiveClaims } from "../utils/tokenUtils.js";
|
|
6
|
+
const logger = createLogger("Auth");
|
|
7
|
+
export function useAuth() {
|
|
8
|
+
const authState = useState(
|
|
9
|
+
"auth-state",
|
|
10
|
+
() => {
|
|
11
|
+
return { user: null, isLoading: false, error: null };
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
const isLoggedIn = computed(() => authState.value?.user !== null);
|
|
15
|
+
const isImpersonating = computed(() => !!authState.value?.user?.impersonation);
|
|
16
|
+
const originalUser = computed(() => {
|
|
17
|
+
const impersonation = authState.value?.user?.impersonation;
|
|
18
|
+
if (!impersonation) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
originalUserId: impersonation.originalUserId,
|
|
23
|
+
originalUserEmail: impersonation.originalUserEmail,
|
|
24
|
+
originalUserName: impersonation.originalUserName
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const config = useRuntimeConfig();
|
|
28
|
+
const publicConfig = config.public;
|
|
29
|
+
const authPath = publicConfig.nuxtAegis?.authPath;
|
|
30
|
+
const loginPath = publicConfig.nuxtAegis?.loginPath || authPath;
|
|
31
|
+
const logoutPath = publicConfig.nuxtAegis?.logoutPath;
|
|
32
|
+
const refreshPath = publicConfig.nuxtAegis?.refreshPath;
|
|
33
|
+
async function refresh() {
|
|
34
|
+
authState.value.isLoading = true;
|
|
35
|
+
authState.value.error = null;
|
|
36
|
+
logger.debug("Refreshing authentication state...");
|
|
37
|
+
try {
|
|
38
|
+
const response = await $fetch(`${refreshPath}`, {
|
|
39
|
+
method: "POST"
|
|
40
|
+
});
|
|
41
|
+
if (response?.accessToken) {
|
|
42
|
+
const { setAccessToken } = await import("../utils/tokenStore.js");
|
|
43
|
+
setAccessToken(response.accessToken);
|
|
44
|
+
const tokenParts = response.accessToken.split(".");
|
|
45
|
+
if (tokenParts[1]) {
|
|
46
|
+
const payload = JSON.parse(atob(tokenParts[1]));
|
|
47
|
+
authState.value.user = filterTimeSensitiveClaims(payload);
|
|
48
|
+
authState.value.error = null;
|
|
49
|
+
logger.debug("Auth state refreshed successfully");
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
authState.value.user = null;
|
|
53
|
+
clearAccessToken();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
authState.value.user = null;
|
|
57
|
+
authState.value.error = "Failed to refresh authentication";
|
|
58
|
+
clearAccessToken();
|
|
59
|
+
logger.error("Auth refresh failed:", error);
|
|
60
|
+
} finally {
|
|
61
|
+
authState.value.isLoading = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function login(provider = "google", _redirectTo) {
|
|
65
|
+
try {
|
|
66
|
+
authState.value.error = null;
|
|
67
|
+
if (!provider || typeof provider !== "string") {
|
|
68
|
+
throw new Error("Provider must be a non-empty string");
|
|
69
|
+
}
|
|
70
|
+
await navigateTo(`${loginPath}/${provider}`, { external: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
authState.value.error = "Failed to initiate login";
|
|
73
|
+
logger.error("Login error:", error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function logout(redirectTo) {
|
|
78
|
+
const config2 = useRuntimeConfig();
|
|
79
|
+
try {
|
|
80
|
+
authState.value.error = null;
|
|
81
|
+
await $fetch(`${logoutPath}`, { method: "POST" });
|
|
82
|
+
authState.value.user = null;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
authState.value.user = null;
|
|
85
|
+
authState.value.error = "Logout completed with errors";
|
|
86
|
+
logger.error("Logout error:", error);
|
|
87
|
+
} finally {
|
|
88
|
+
clearAccessToken();
|
|
89
|
+
const logoutRedirect = redirectTo ? validateRedirectPath(redirectTo) : validateRedirectPath(config2.public.nuxtAegis.redirect?.logout || "/");
|
|
90
|
+
await navigateTo(logoutRedirect);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function impersonate(targetUserId, reason) {
|
|
94
|
+
try {
|
|
95
|
+
authState.value.error = null;
|
|
96
|
+
authState.value.isLoading = true;
|
|
97
|
+
logger.debug("Starting impersonation...", { targetUserId });
|
|
98
|
+
const { getAccessToken } = await import("../utils/tokenStore.js");
|
|
99
|
+
const currentToken = getAccessToken();
|
|
100
|
+
if (!currentToken) {
|
|
101
|
+
throw new Error("Must be authenticated to impersonate");
|
|
102
|
+
}
|
|
103
|
+
const response = await $fetch(`${authPath}/impersonate`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${currentToken}`
|
|
107
|
+
},
|
|
108
|
+
body: {
|
|
109
|
+
targetUserId,
|
|
110
|
+
reason
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (response?.accessToken) {
|
|
114
|
+
const { setAccessToken } = await import("../utils/tokenStore.js");
|
|
115
|
+
setAccessToken(response.accessToken);
|
|
116
|
+
const tokenParts = response.accessToken.split(".");
|
|
117
|
+
if (tokenParts[1]) {
|
|
118
|
+
const payload = JSON.parse(atob(tokenParts[1]));
|
|
119
|
+
authState.value.user = filterTimeSensitiveClaims(payload);
|
|
120
|
+
authState.value.error = null;
|
|
121
|
+
logger.debug("Impersonation started successfully", {
|
|
122
|
+
targetUser: payload.sub,
|
|
123
|
+
originalUser: payload.impersonation?.originalUserId
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const err = error;
|
|
129
|
+
authState.value.error = err.data?.message || err.message || "Failed to start impersonation";
|
|
130
|
+
logger.error("Impersonation failed:", error);
|
|
131
|
+
throw error;
|
|
132
|
+
} finally {
|
|
133
|
+
authState.value.isLoading = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function stopImpersonation() {
|
|
137
|
+
try {
|
|
138
|
+
authState.value.error = null;
|
|
139
|
+
authState.value.isLoading = true;
|
|
140
|
+
logger.debug("Stopping impersonation...");
|
|
141
|
+
const { getAccessToken } = await import("../utils/tokenStore.js");
|
|
142
|
+
const currentToken = getAccessToken();
|
|
143
|
+
if (!currentToken) {
|
|
144
|
+
throw new Error("No active session");
|
|
145
|
+
}
|
|
146
|
+
const response = await $fetch(`${authPath}/unimpersonate`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${currentToken}`
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (response?.accessToken) {
|
|
153
|
+
const { setAccessToken } = await import("../utils/tokenStore.js");
|
|
154
|
+
setAccessToken(response.accessToken);
|
|
155
|
+
const tokenParts = response.accessToken.split(".");
|
|
156
|
+
if (tokenParts[1]) {
|
|
157
|
+
const payload = JSON.parse(atob(tokenParts[1]));
|
|
158
|
+
authState.value.user = filterTimeSensitiveClaims(payload);
|
|
159
|
+
authState.value.error = null;
|
|
160
|
+
logger.debug("Impersonation stopped, original session restored", {
|
|
161
|
+
restoredUser: payload.sub
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const err = error;
|
|
167
|
+
authState.value.error = err.data?.message || err.message || "Failed to stop impersonation";
|
|
168
|
+
logger.error("Stop impersonation failed:", error);
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
authState.value.isLoading = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
isLoggedIn,
|
|
176
|
+
isLoading: computed(() => authState.value.isLoading),
|
|
177
|
+
user: computed(() => authState.value.user),
|
|
178
|
+
error: computed(() => authState.value.error),
|
|
179
|
+
isImpersonating,
|
|
180
|
+
originalUser,
|
|
181
|
+
login,
|
|
182
|
+
logout,
|
|
183
|
+
refresh,
|
|
184
|
+
impersonate,
|
|
185
|
+
stopImpersonation
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side middleware for protecting routes that require authentication
|
|
3
|
+
*
|
|
4
|
+
* This middleware redirects unauthenticated users to the login page.
|
|
5
|
+
* It can be used globally or on specific pages using definePageMeta.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* - Global: Configure in module options with clientMiddleware.global = true
|
|
9
|
+
* - Per-page: definePageMeta({ middleware: ['auth-logged-in'] })
|
|
10
|
+
*
|
|
11
|
+
* Note: This is client-side only and improves UX. Server-side validation
|
|
12
|
+
* via Nitro routeRules is still required for security.
|
|
13
|
+
*/
|
|
14
|
+
declare const _default: import("#app").RouteMiddleware;
|
|
15
|
+
export default _default;
|
|
16
|
+
export { default as authLoggedIn } from './auth-logged-in.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuth } from "../composables/useAuth.js";
|
|
3
|
+
import { isRouteMatch } from "../utils/routeMatching.js";
|
|
4
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
5
|
+
const { isLoggedIn, isLoading } = useAuth();
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const clientMiddleware = config.public.nuxtAegis?.clientMiddleware;
|
|
8
|
+
if (!clientMiddleware?.enabled) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (isLoading.value) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (clientMiddleware.global) {
|
|
15
|
+
const publicRoutes = clientMiddleware.publicRoutes || [];
|
|
16
|
+
const isPublicRoute = isRouteMatch(to.path, publicRoutes);
|
|
17
|
+
if (isPublicRoute) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!isLoggedIn.value) {
|
|
22
|
+
return navigateTo(clientMiddleware.redirectTo || "/login");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
export { default as authLoggedIn } from "./auth-logged-in.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side middleware for protecting routes that should only be accessible
|
|
3
|
+
* when NOT authenticated (e.g., login, register pages)
|
|
4
|
+
*
|
|
5
|
+
* This middleware redirects authenticated users away from pages that don't
|
|
6
|
+
* make sense for logged-in users.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* - Per-page: definePageMeta({ middleware: ['auth-logged-out'] })
|
|
10
|
+
*
|
|
11
|
+
* Example use cases:
|
|
12
|
+
* - /login - redirect logged-in users to home
|
|
13
|
+
* - /register - redirect logged-in users to dashboard
|
|
14
|
+
* - /forgot-password - redirect logged-in users to home
|
|
15
|
+
*
|
|
16
|
+
* Note: This is client-side only and improves UX.
|
|
17
|
+
*/
|
|
18
|
+
declare const _default: import("#app").RouteMiddleware;
|
|
19
|
+
export default _default;
|
|
20
|
+
export { default as authLoggedOut } from './auth-logged-out.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuth } from "../composables/useAuth.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware(() => {
|
|
4
|
+
const { isLoggedIn, isLoading } = useAuth();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const clientMiddleware = config.public.nuxtAegis?.clientMiddleware;
|
|
7
|
+
if (!clientMiddleware?.enabled) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (isLoading.value) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (isLoggedIn.value) {
|
|
14
|
+
return navigateTo(clientMiddleware.loggedOutRedirectTo || "/");
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
export { default as authLoggedOut } from "./auth-logged-out.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { navigateTo, useRuntimeConfig } from "#app";
|
|
3
|
+
import { ref, onMounted } from "vue";
|
|
4
|
+
import { setAccessToken } from "../utils/tokenStore";
|
|
5
|
+
import { createLogger } from "../utils/logger";
|
|
6
|
+
import { validateRedirectPath } from "../utils/redirectValidation";
|
|
7
|
+
import { filterTimeSensitiveClaims } from "../utils/tokenUtils";
|
|
8
|
+
const logger = createLogger("Callback");
|
|
9
|
+
const config = useRuntimeConfig();
|
|
10
|
+
const processing = ref(true);
|
|
11
|
+
const error = ref(null);
|
|
12
|
+
onMounted(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
15
|
+
const code = urlParams.get("code");
|
|
16
|
+
const errorParam = urlParams.get("error");
|
|
17
|
+
if (errorParam) {
|
|
18
|
+
error.value = "Authentication failed. Please try again.";
|
|
19
|
+
logger.error("Authentication error:", errorParam);
|
|
20
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
21
|
+
processing.value = false;
|
|
22
|
+
const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
|
|
23
|
+
const errorDescription = urlParams.get("error_description") || "Authentication failed";
|
|
24
|
+
await navigateTo(`${errorUrl}?error=${encodeURIComponent(errorParam)}&error_description=${encodeURIComponent(errorDescription)}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!code) {
|
|
28
|
+
error.value = "Authentication failed. Please try again.";
|
|
29
|
+
logger.error("No authorization code in URL");
|
|
30
|
+
processing.value = false;
|
|
31
|
+
const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
|
|
32
|
+
await navigateTo(`${errorUrl}?error=invalid_request&error_description=${encodeURIComponent("Authentication failed. Please try again.")}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const response = await $fetch(
|
|
36
|
+
"/auth/token",
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: { code }
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
if (!response?.accessToken) {
|
|
43
|
+
error.value = "Authentication failed. Please try again.";
|
|
44
|
+
logger.error("No access token in response");
|
|
45
|
+
processing.value = false;
|
|
46
|
+
const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
|
|
47
|
+
await navigateTo(`${errorUrl}?error=token_exchange_failed&error_description=${encodeURIComponent("Authentication failed. Please try again.")}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setAccessToken(response.accessToken);
|
|
51
|
+
const tokenParts = response.accessToken.split(".");
|
|
52
|
+
if (tokenParts[1]) {
|
|
53
|
+
const payload = JSON.parse(atob(tokenParts[1]));
|
|
54
|
+
const { useState } = await import("#app");
|
|
55
|
+
const authState = useState("auth-state");
|
|
56
|
+
authState.value = { user: filterTimeSensitiveClaims(payload), isLoading: false, error: null };
|
|
57
|
+
}
|
|
58
|
+
window.history.replaceState(
|
|
59
|
+
{},
|
|
60
|
+
"",
|
|
61
|
+
window.location.pathname
|
|
62
|
+
);
|
|
63
|
+
processing.value = false;
|
|
64
|
+
const successUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.success || "/");
|
|
65
|
+
await navigateTo(successUrl);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error("Error processing callback:", err);
|
|
68
|
+
error.value = "Authentication failed. Please try again.";
|
|
69
|
+
processing.value = false;
|
|
70
|
+
const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
|
|
71
|
+
await navigateTo(`${errorUrl}?error=processing_error&error_description=${encodeURIComponent("Authentication failed. Please try again.")}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div class="auth-callback">
|
|
78
|
+
<div v-if="error">
|
|
79
|
+
<p>Authentication error: {{ error }}</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div v-else-if="processing">
|
|
82
|
+
<p>Processing authentication...</p>
|
|
83
|
+
</div>
|
|
84
|
+
<div v-else>
|
|
85
|
+
<p>Authentication successful. Redirecting...</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<style scoped>
|
|
91
|
+
.auth-callback{align-items:center;display:flex;justify-content:center;min-height:100vh;text-align:center}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side api plugin
|
|
3
|
+
* Intercepts API calls and attaches authorization bearer token from memory
|
|
4
|
+
* See: https://nuxt.com/docs/4.x/guide/recipes/custom-usefetch
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: import("#app").Plugin<{
|
|
7
|
+
api: import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
|
|
8
|
+
}> & import("#app").ObjectPlugin<{
|
|
9
|
+
api: import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
|
|
10
|
+
}>;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineNuxtPlugin, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuth } from "#imports";
|
|
3
|
+
import { getAccessToken, clearAccessToken } from "../utils/tokenStore.js";
|
|
4
|
+
import { isRouteMatch } from "../utils/routeMatching.js";
|
|
5
|
+
import { createLogger } from "../utils/logger.js";
|
|
6
|
+
import { validateRedirectPath } from "../utils/redirectValidation.js";
|
|
7
|
+
const logger = createLogger("API:Client");
|
|
8
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
9
|
+
let isRefreshing = false;
|
|
10
|
+
let refreshPromise = null;
|
|
11
|
+
let isInitialized = false;
|
|
12
|
+
const autoRefreshEnabled = nuxtApp.$config.public.nuxtAegis.tokenRefresh.automaticRefresh ?? true;
|
|
13
|
+
async function attemptTokenRefresh() {
|
|
14
|
+
if (isRefreshing) return refreshPromise;
|
|
15
|
+
logger.debug("Attempting token refresh...");
|
|
16
|
+
isRefreshing = true;
|
|
17
|
+
refreshPromise = nuxtApp.runWithContext(async () => {
|
|
18
|
+
const auth = useAuth();
|
|
19
|
+
try {
|
|
20
|
+
await auth.refresh();
|
|
21
|
+
return getAccessToken();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error("Token refresh failed:", error);
|
|
24
|
+
return null;
|
|
25
|
+
} finally {
|
|
26
|
+
isRefreshing = false;
|
|
27
|
+
refreshPromise = null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return refreshPromise;
|
|
31
|
+
}
|
|
32
|
+
const api = $fetch.create({
|
|
33
|
+
onRequest({ options }) {
|
|
34
|
+
const token = getAccessToken();
|
|
35
|
+
if (token) {
|
|
36
|
+
options.headers.set("Authorization", `Bearer ${token}`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async onResponseError({ options, response }) {
|
|
40
|
+
if (response.status === 401 && autoRefreshEnabled) {
|
|
41
|
+
const newToken = await attemptTokenRefresh();
|
|
42
|
+
if (newToken) {
|
|
43
|
+
options.headers.set("Authorization", `Bearer ${newToken}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
clearAccessToken();
|
|
47
|
+
const config = useRuntimeConfig();
|
|
48
|
+
const errorUrl = validateRedirectPath(config.public.nuxtAegis.redirect?.error || "/");
|
|
49
|
+
await nuxtApp.runWithContext(() => navigateTo(`${errorUrl}?error=token_refresh_failed&error_description=${encodeURIComponent("Session expired. Please log in again.")}`));
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
retry: autoRefreshEnabled ? 1 : 0,
|
|
53
|
+
retryStatusCodes: [401]
|
|
54
|
+
});
|
|
55
|
+
const result = {
|
|
56
|
+
provide: {
|
|
57
|
+
api
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (!isInitialized && autoRefreshEnabled) {
|
|
61
|
+
isInitialized = true;
|
|
62
|
+
logger.debug("Initializing auth state on startup...");
|
|
63
|
+
nuxtApp.hook("app:mounted", async () => {
|
|
64
|
+
await nuxtApp.runWithContext(async () => {
|
|
65
|
+
const callbackPath = nuxtApp.$config.public.nuxtAegis.callbackPath;
|
|
66
|
+
if (typeof window !== "undefined" && window.location.pathname === callbackPath) {
|
|
67
|
+
logger.debug("On auth callback page, skipping refresh");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof window !== "undefined") {
|
|
71
|
+
const publicRoutes = nuxtApp.$config.public.nuxtAegis?.clientMiddleware?.publicRoutes || [];
|
|
72
|
+
const currentPath = window.location.pathname;
|
|
73
|
+
if (isRouteMatch(currentPath, publicRoutes)) {
|
|
74
|
+
logger.debug("On public route, skipping refresh on startup");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const currentToken = getAccessToken();
|
|
79
|
+
if (currentToken) {
|
|
80
|
+
logger.debug("Access token already present, skipping refresh on startup");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await useAuth().refresh();
|
|
85
|
+
} catch {
|
|
86
|
+
logger.debug("No valid refresh token found on startup");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side api plugin for SSR
|
|
3
|
+
* Attaches SSR access token from event.context to $api requests
|
|
4
|
+
*
|
|
5
|
+
* Note: This creates a custom $api instance with SSR token injection.
|
|
6
|
+
* It will only work when explicitly called via useNuxtApp().$api
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("#app").Plugin<{
|
|
9
|
+
api: import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
|
|
10
|
+
}> & import("#app").ObjectPlugin<{
|
|
11
|
+
api: import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
|
|
12
|
+
}>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRequestEvent } from "#app";
|
|
2
|
+
export default defineNuxtPlugin({
|
|
3
|
+
name: "api-server",
|
|
4
|
+
enforce: "pre",
|
|
5
|
+
async setup() {
|
|
6
|
+
const api = $fetch.create({
|
|
7
|
+
onRequest({ options }) {
|
|
8
|
+
const event = useRequestEvent();
|
|
9
|
+
const ssrAccessToken = event?.context?.ssrAccessToken;
|
|
10
|
+
if (ssrAccessToken) {
|
|
11
|
+
options.headers = options.headers || {};
|
|
12
|
+
if (options.headers instanceof Headers) {
|
|
13
|
+
options.headers.set("Authorization", `Bearer ${ssrAccessToken}`);
|
|
14
|
+
} else if (Array.isArray(options.headers)) {
|
|
15
|
+
options.headers.push(["Authorization", `Bearer ${ssrAccessToken}`]);
|
|
16
|
+
} else {
|
|
17
|
+
options.headers.Authorization = `Bearer ${ssrAccessToken}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
provide: {
|
|
24
|
+
api
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useState, useRequestEvent } from "#app";
|
|
2
|
+
import { filterTimeSensitiveClaims } from "../utils/tokenUtils.js";
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
const event = useRequestEvent();
|
|
5
|
+
if (event?.context.user) {
|
|
6
|
+
const user = event.context.user;
|
|
7
|
+
useState("auth-state", () => ({
|
|
8
|
+
user: filterTimeSensitiveClaims(user),
|
|
9
|
+
isLoading: false,
|
|
10
|
+
error: null
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
scrollBehavior(to: import("vue-router").RouteLocationNormalizedGeneric, from: import("vue-router").RouteLocationNormalizedLoadedGeneric, savedPosition: {
|
|
3
|
+
behavior?: ScrollOptions["behavior"];
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
} | null): false | {
|
|
7
|
+
behavior?: ScrollOptions["behavior"];
|
|
8
|
+
left: number;
|
|
9
|
+
top: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger instance for consistent logging across client-side code
|
|
3
|
+
*/
|
|
4
|
+
export interface Logger {
|
|
5
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
7
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
8
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
9
|
+
security: (message: string, ...args: unknown[]) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a logger instance with a specific context
|
|
13
|
+
* Respects the logging configuration from runtime config
|
|
14
|
+
*
|
|
15
|
+
* @param context - Context identifier for log messages (e.g., 'Auth', 'Callback', 'API')
|
|
16
|
+
* @returns Logger instance with debug, info, warn, error, and security methods
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLogger(context: string): Logger;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
export function createLogger(context) {
|
|
4
|
+
const prefix = `[Nuxt Aegis][${context}]`;
|
|
5
|
+
return {
|
|
6
|
+
debug: (message, ...args) => {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const loggingConfig = config.public.nuxtAegis?.logging;
|
|
9
|
+
const level = loggingConfig?.level || "info";
|
|
10
|
+
if (level === "debug") {
|
|
11
|
+
consola.debug(prefix, message, ...args);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
info: (message, ...args) => {
|
|
15
|
+
const config = useRuntimeConfig();
|
|
16
|
+
const loggingConfig = config.public.nuxtAegis?.logging;
|
|
17
|
+
const level = loggingConfig?.level || "info";
|
|
18
|
+
if (level !== "silent" && level !== "error" && level !== "warn") {
|
|
19
|
+
consola.info(prefix, message, ...args);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
warn: (message, ...args) => {
|
|
23
|
+
const config = useRuntimeConfig();
|
|
24
|
+
const loggingConfig = config.public.nuxtAegis?.logging;
|
|
25
|
+
const level = loggingConfig?.level || "info";
|
|
26
|
+
if (level !== "silent" && level !== "error") {
|
|
27
|
+
consola.warn(prefix, message, ...args);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
error: (message, ...args) => {
|
|
31
|
+
const config = useRuntimeConfig();
|
|
32
|
+
const loggingConfig = config.public.nuxtAegis?.logging;
|
|
33
|
+
const level = loggingConfig?.level || "info";
|
|
34
|
+
if (level !== "silent") {
|
|
35
|
+
consola.error(prefix, message, ...args);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
security: (message, ...args) => {
|
|
39
|
+
const config = useRuntimeConfig();
|
|
40
|
+
const loggingConfig = config.public.nuxtAegis?.logging;
|
|
41
|
+
const level = loggingConfig?.level || "info";
|
|
42
|
+
const securityEnabled = loggingConfig?.security === true || level === "debug";
|
|
43
|
+
if (securityEnabled && level !== "silent") {
|
|
44
|
+
consola.warn(`[Nuxt Aegis Security][${context}]`, message, ...args);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate redirect path to prevent open redirect vulnerabilities
|
|
3
|
+
*
|
|
4
|
+
* Security Requirements:
|
|
5
|
+
* - Only allow relative paths (must start with '/')
|
|
6
|
+
* - Reject absolute URLs (http://, https://, //)
|
|
7
|
+
* - Throw error for invalid paths
|
|
8
|
+
*
|
|
9
|
+
* @param path - The redirect path to validate
|
|
10
|
+
* @returns The validated path if valid
|
|
11
|
+
* @throws Error if path is not a valid relative path
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* validateRedirectPath('/dashboard') // Returns '/dashboard'
|
|
15
|
+
* validateRedirectPath('https://evil.com') // Throws error
|
|
16
|
+
* validateRedirectPath('//evil.com') // Throws error
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateRedirectPath(path: string): string;
|