@devcoffee/nuxt-core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +407 -84
- package/dist/module.d.mts +295 -156
- package/dist/module.d.ts +300 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +192 -37
- package/dist/runtime/app/composables/useAuthContext.d.ts +26 -0
- package/dist/runtime/app/composables/useAuthContext.js +111 -0
- package/dist/runtime/app/composables/useLogger.d.ts +3 -3
- package/dist/runtime/app/composables/useSessionContext.d.ts +22 -0
- package/dist/runtime/app/composables/useSessionContext.js +5 -0
- package/dist/runtime/app/middleware/authts.d.ts +12 -0
- package/dist/runtime/app/middleware/authts.js +101 -0
- package/dist/runtime/app/pages/authorize.d.vue.ts +3 -0
- package/dist/runtime/app/pages/authorize.vue +32 -0
- package/dist/runtime/app/pages/authorize.vue.d.ts +3 -0
- package/dist/runtime/app/plugins/authts.d.ts +82 -0
- package/dist/runtime/app/plugins/authts.js +91 -0
- package/dist/runtime/app/plugins/formatters.d.ts +29 -0
- package/dist/runtime/app/plugins/formatters.js +101 -0
- package/dist/runtime/app/plugins/locale.d.ts +37 -0
- package/dist/runtime/app/plugins/locale.js +39 -0
- package/dist/runtime/app/plugins/logging.d.ts +24 -16
- package/dist/runtime/app/plugins/logging.js +0 -1
- package/dist/runtime/app/utils/hashing.d.ts +1 -0
- package/dist/runtime/app/utils/hashing.js +3 -0
- package/dist/runtime/server/adapters/http.d.ts +5 -0
- package/dist/runtime/server/adapters/http.js +15 -0
- package/dist/runtime/server/adapters/oidc.d.ts +58 -0
- package/dist/runtime/server/adapters/oidc.js +21 -0
- package/dist/runtime/server/adapters/storage.d.ts +39 -0
- package/dist/runtime/server/adapters/storage.js +14 -0
- package/dist/runtime/server/adapters/utils.d.ts +31 -0
- package/dist/runtime/server/adapters/utils.js +28 -0
- package/dist/runtime/server/composables/useServerLogger.d.ts +3 -2
- package/dist/runtime/server/composables/useServerLogger.js +4 -4
- package/dist/runtime/server/core/crypto.d.ts +70 -0
- package/dist/runtime/server/core/crypto.js +55 -0
- package/dist/runtime/server/core/helpers.d.ts +194 -0
- package/dist/runtime/server/core/helpers.js +355 -0
- package/dist/runtime/server/core/index.d.ts +1 -0
- package/dist/runtime/server/core/index.js +1 -0
- package/dist/runtime/server/core/mutex.d.ts +19 -0
- package/dist/runtime/server/core/mutex.js +39 -0
- package/dist/runtime/server/core/nuxtAuthtsHandler.d.ts +26 -0
- package/dist/runtime/server/core/nuxtAuthtsHandler.js +238 -0
- package/dist/runtime/server/core/nuxtForwardHandler.d.ts +18 -0
- package/dist/runtime/server/core/nuxtForwardHandler.js +60 -0
- package/dist/runtime/server/dev/route/session.d.ts +2 -0
- package/dist/runtime/server/dev/route/session.js +8 -0
- package/dist/runtime/server/plugins/authts.d.ts +11 -0
- package/dist/runtime/server/plugins/authts.js +55 -0
- package/dist/runtime/server/plugins/logging.js +7 -2
- package/dist/runtime/server/tsconfig.json +3 -3
- package/dist/runtime/types/global.env.d.ts +21 -7
- package/dist/runtime/types/nitro.d.ts +7 -2
- package/dist/runtime/types/nuxt.d.ts +28 -8
- package/dist/runtime/utils.d.ts +31 -0
- package/dist/runtime/utils.js +28 -0
- package/dist/types.d.mts +6 -4
- package/package.json +23 -14
- package/dist/runtime/plugin.d.ts +0 -2
- package/dist/runtime/plugin.js +0 -4
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useRequestURL } from "#app";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createError,
|
|
5
|
+
navigateTo,
|
|
6
|
+
ref,
|
|
7
|
+
useLogger,
|
|
8
|
+
useNuxtApp,
|
|
9
|
+
useRequestFetch,
|
|
10
|
+
useSessionContext
|
|
11
|
+
} from "#imports";
|
|
12
|
+
export function useAuthContext(initiator) {
|
|
13
|
+
const { callHook, runWithContext } = useNuxtApp();
|
|
14
|
+
const { getValue } = useSessionContext();
|
|
15
|
+
const fetchRequest = useRequestFetch();
|
|
16
|
+
const logger = useLogger({ level: 2, tag: "authContext" });
|
|
17
|
+
const processing = ref(false);
|
|
18
|
+
const session = computed(() => getValue(initiator, "useAuthContext.session"));
|
|
19
|
+
const isAuthenticated = computed(() => {
|
|
20
|
+
const { isAuthenticated: isAuthenticated2 = false } = session.value || {};
|
|
21
|
+
return isAuthenticated2;
|
|
22
|
+
});
|
|
23
|
+
const user = computed(() => session.value.user);
|
|
24
|
+
function sanitizeError(err) {
|
|
25
|
+
if (err?.statusCode) {
|
|
26
|
+
return createError(err);
|
|
27
|
+
} else if (err instanceof TypeError || err instanceof SyntaxError) {
|
|
28
|
+
return createError({
|
|
29
|
+
error: true,
|
|
30
|
+
statusCode: 400,
|
|
31
|
+
statusMessage: "Server Error",
|
|
32
|
+
message: err.message,
|
|
33
|
+
stack: err.stack,
|
|
34
|
+
cause: err,
|
|
35
|
+
fatal: true
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
return createError({
|
|
39
|
+
error: true,
|
|
40
|
+
statusCode: 500,
|
|
41
|
+
statusMessage: "Server Error",
|
|
42
|
+
message: String(err),
|
|
43
|
+
cause: err,
|
|
44
|
+
fatal: true
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function login(redirectTo) {
|
|
49
|
+
if (isAuthenticated.value) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const endpoint = new URL("/api/_auth/authorize-url", useRequestURL());
|
|
53
|
+
if (redirectTo) {
|
|
54
|
+
endpoint.searchParams.set("redirectUrl", redirectTo);
|
|
55
|
+
}
|
|
56
|
+
await fetchRequest(endpoint.toString(), {
|
|
57
|
+
onRequest() {
|
|
58
|
+
processing.value = true;
|
|
59
|
+
}
|
|
60
|
+
}).then(
|
|
61
|
+
async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl, { external: true, replace: true }))
|
|
62
|
+
).catch((ex) => {
|
|
63
|
+
logger.log(`[login] failed error:${ex}`, ex);
|
|
64
|
+
throw sanitizeError(ex);
|
|
65
|
+
}).finally(() => processing.value = false);
|
|
66
|
+
}
|
|
67
|
+
async function authorize(parameters) {
|
|
68
|
+
const endpoint = new URL("/api/_auth/token", useRequestURL());
|
|
69
|
+
await fetchRequest(endpoint.toString(), {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: parameters,
|
|
72
|
+
onRequest() {
|
|
73
|
+
processing.value = true;
|
|
74
|
+
}
|
|
75
|
+
}).then(async (response) => {
|
|
76
|
+
await runWithContext(async () => await callHook("user:loggedIn"));
|
|
77
|
+
return response;
|
|
78
|
+
}).then(
|
|
79
|
+
async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl))
|
|
80
|
+
).catch((ex) => {
|
|
81
|
+
logger.log(`[authorize] failed error:${ex}`, ex);
|
|
82
|
+
throw sanitizeError(ex);
|
|
83
|
+
}).finally(() => processing.value = false);
|
|
84
|
+
}
|
|
85
|
+
async function logout() {
|
|
86
|
+
const endpoint = new URL("/api/_auth/logout", useRequestURL());
|
|
87
|
+
await fetchRequest(endpoint.toString(), {
|
|
88
|
+
method: "POST",
|
|
89
|
+
onRequest() {
|
|
90
|
+
processing.value = true;
|
|
91
|
+
}
|
|
92
|
+
}).then(async (response) => {
|
|
93
|
+
await runWithContext(async () => await callHook("user:loggedOut"));
|
|
94
|
+
return response;
|
|
95
|
+
}).then(
|
|
96
|
+
async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl))
|
|
97
|
+
).catch((ex) => {
|
|
98
|
+
logger.log(`[logout] failed error:${ex}`, ex);
|
|
99
|
+
throw sanitizeError(ex);
|
|
100
|
+
}).finally(() => processing.value = false);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
user,
|
|
104
|
+
session,
|
|
105
|
+
processing,
|
|
106
|
+
isAuthenticated,
|
|
107
|
+
login,
|
|
108
|
+
authorize,
|
|
109
|
+
logout
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔐 Provides access to the global session context plugin.
|
|
3
|
+
*
|
|
4
|
+
* This composable is a lightweight wrapper around the `$sessionContext`
|
|
5
|
+
* provided by the Nuxt plugin, allowing components and composables to
|
|
6
|
+
* easily access session-related utilities such as `getValue()` and `refetch()`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const { getValue, refetch } = useSessionContext()
|
|
11
|
+
* const session = getValue()
|
|
12
|
+
*
|
|
13
|
+
* if (session.isAuthenticated) {
|
|
14
|
+
* console.log('User:', session.user)
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @returns {import('#app').NuxtApp['$sessionContext']} The session context plugin instance.
|
|
19
|
+
*
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
*/
|
|
22
|
+
export declare function useSessionContext(): import('#app').NuxtApp['$sessionContext'];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt route middleware handling authentication and access control.
|
|
3
|
+
*
|
|
4
|
+
* Handles all major auth scenarios:
|
|
5
|
+
* - Redirects unauthenticated users from protected routes to login.
|
|
6
|
+
* - Redirects authenticated users away from login/register pages.
|
|
7
|
+
* - Stores intended redirect URL in cookie before login.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: any;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abortNavigation,
|
|
3
|
+
createError,
|
|
4
|
+
defineNuxtRouteMiddleware,
|
|
5
|
+
navigateTo,
|
|
6
|
+
refreshNuxtData,
|
|
7
|
+
useCookie,
|
|
8
|
+
useNuxtApp,
|
|
9
|
+
useRequestEvent,
|
|
10
|
+
useRuntimeConfig
|
|
11
|
+
} from "#app";
|
|
12
|
+
import { omit } from "#devcoffee-core/utils";
|
|
13
|
+
import { useAuthContext, useLogger } from "#imports";
|
|
14
|
+
import { getCookie, setCookie } from "h3";
|
|
15
|
+
function normalizedAuthMeta(normalizedPath, routeMeta, opts) {
|
|
16
|
+
const authts = routeMeta.authts || {};
|
|
17
|
+
const { openIdRedirectUri } = opts;
|
|
18
|
+
const normalizedRedirectUri = (openIdRedirectUri.split("?")[0] ?? "").replace(/\/$/, "");
|
|
19
|
+
const normalizedCurrentPath = normalizedPath.replace(/\/$/, "");
|
|
20
|
+
const unauthenticatedOnly = authts.unauthenticatedOnly ?? [normalizedRedirectUri].includes(normalizedCurrentPath);
|
|
21
|
+
return {
|
|
22
|
+
// A page with unauthenticatedOnly: true is by definition not a protected route.
|
|
23
|
+
// Default required to false when unauthenticatedOnly is true to avoid contradictory meta.
|
|
24
|
+
required: authts.required ?? (unauthenticatedOnly ? false : ![normalizedRedirectUri].includes(normalizedCurrentPath)),
|
|
25
|
+
unauthenticatedOnly,
|
|
26
|
+
roles: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function setRedirectCookie(name, redirectUrl) {
|
|
30
|
+
if (import.meta.server) {
|
|
31
|
+
const event = useRequestEvent();
|
|
32
|
+
if (event) {
|
|
33
|
+
const { cookieOpts } = useRuntimeConfig(event).nuxtCore.authts.sessions;
|
|
34
|
+
setCookie(event, name, redirectUrl, omit(cookieOpts, ["expires", "maxAge"]));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (import.meta.client) {
|
|
38
|
+
const cookie = useCookie(name);
|
|
39
|
+
cookie.value = redirectUrl;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getRedirectCookie(name) {
|
|
43
|
+
if (import.meta.server) {
|
|
44
|
+
const event = useRequestEvent();
|
|
45
|
+
if (event) {
|
|
46
|
+
return getCookie(event, name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (import.meta.client) {
|
|
50
|
+
return useCookie(name).value || void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
54
|
+
const nuxtApp = useNuxtApp();
|
|
55
|
+
await nuxtApp.$sessionReady;
|
|
56
|
+
const logger = useLogger({ tag: "authts.middleware", level: 3 });
|
|
57
|
+
const {
|
|
58
|
+
loginUri,
|
|
59
|
+
defaultLoginRedirectUri,
|
|
60
|
+
redirectUri,
|
|
61
|
+
redirectCookie,
|
|
62
|
+
ignoreRegexPatterns,
|
|
63
|
+
ignoreRegexPatternsDev
|
|
64
|
+
} = useRuntimeConfig().public.nuxtCore.authts;
|
|
65
|
+
const normalizedPath = to.path.toLowerCase();
|
|
66
|
+
const authMeta = normalizedAuthMeta(normalizedPath, to.meta, { openIdRedirectUri: redirectUri.toLowerCase() });
|
|
67
|
+
if (ignoreRegexPatterns.length && ignoreRegexPatterns.some((pattern) => pattern.test(normalizedPath))) {
|
|
68
|
+
logger.debug(`Bypassing auth checks for ignored path: ${normalizedPath}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (import.meta.env.DEV && ignoreRegexPatternsDev.length && ignoreRegexPatternsDev.some((pattern) => pattern.test(normalizedPath))) {
|
|
72
|
+
logger.debug(`Bypassing auth checks for ignored path (dev): ${normalizedPath}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (authMeta.required && authMeta.unauthenticatedOnly) {
|
|
76
|
+
return abortNavigation(
|
|
77
|
+
createError({
|
|
78
|
+
statusCode: 500,
|
|
79
|
+
fatal: true,
|
|
80
|
+
statusMessage: "Invalid route meta",
|
|
81
|
+
message: 'A route cannot be both "required" and "unauthenticatedOnly".'
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (import.meta.client) {
|
|
86
|
+
await refreshNuxtData("authts:session");
|
|
87
|
+
}
|
|
88
|
+
const { isAuthenticated } = useAuthContext("core.app.authts.middleware");
|
|
89
|
+
const loginPath = loginUri.toLowerCase();
|
|
90
|
+
const loggedInRedirect = getRedirectCookie(redirectCookie) || defaultLoginRedirectUri;
|
|
91
|
+
if (isAuthenticated.value && normalizedPath === loginPath) {
|
|
92
|
+
return navigateTo(loggedInRedirect, { replace: true });
|
|
93
|
+
}
|
|
94
|
+
if (isAuthenticated.value && authMeta.unauthenticatedOnly && normalizedPath !== loginPath) {
|
|
95
|
+
return abortNavigation(createError({ statusCode: 404, message: "Page not found." }));
|
|
96
|
+
}
|
|
97
|
+
if (!isAuthenticated.value && authMeta.required && normalizedPath !== loginPath) {
|
|
98
|
+
setRedirectCookie(redirectCookie, to.fullPath);
|
|
99
|
+
return navigateTo(loginUri, { replace: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
@@ -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,32 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted } from "vue";
|
|
3
|
+
import { createError, useRoute } from "#app";
|
|
4
|
+
import { useAuthContext } from "#imports";
|
|
5
|
+
const { query } = useRoute();
|
|
6
|
+
const { authorize } = useAuthContext("core.app.pages.authorize");
|
|
7
|
+
const searchParms = computed(() => {
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
if (query.code) {
|
|
10
|
+
params.set("code", query.code);
|
|
11
|
+
}
|
|
12
|
+
if (query.state) {
|
|
13
|
+
params.set("state", query.state);
|
|
14
|
+
}
|
|
15
|
+
return params;
|
|
16
|
+
});
|
|
17
|
+
if (!searchParms.value.has("code")) {
|
|
18
|
+
throw createError({
|
|
19
|
+
status: 400,
|
|
20
|
+
message: "Invalid code params"
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
onMounted(async () => {
|
|
24
|
+
if (searchParms.value.has("code")) {
|
|
25
|
+
await authorize(searchParms.value);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div>Nuxt module auth callback!</div>
|
|
32
|
+
</template>
|
|
@@ -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,82 @@
|
|
|
1
|
+
import type { NuxtSessionContext } from '#app';
|
|
2
|
+
/**
|
|
3
|
+
* Type definition for the session context plugin.
|
|
4
|
+
*
|
|
5
|
+
* Provides helper methods to get and refresh the current session.
|
|
6
|
+
*
|
|
7
|
+
* @since 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
export type SessionContextPlugin = {
|
|
10
|
+
/**
|
|
11
|
+
* Refetches the session context from the server and updates the local state.
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
refetch: () => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the current session context value.
|
|
17
|
+
* Throws if the context has not yet been initialized.
|
|
18
|
+
* @param {...string|undefined} initiators - Optional initiators for logging purposes.
|
|
19
|
+
* @since 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
getValue: (...initiators: (string | undefined)[]) => NuxtSessionContext;
|
|
22
|
+
};
|
|
23
|
+
declare module 'vue' {
|
|
24
|
+
interface ComponentCustomProperties {
|
|
25
|
+
/**
|
|
26
|
+
* Provides access to session state and helper methods in Vue components.
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
*/
|
|
29
|
+
$sessionContext: SessionContextPlugin;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
declare module '#app' {
|
|
33
|
+
interface NuxtApp {
|
|
34
|
+
/**
|
|
35
|
+
* Provides access to session-related state and actions.
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
*/
|
|
38
|
+
$sessionContext: SessionContextPlugin;
|
|
39
|
+
/**
|
|
40
|
+
* Promise that resolves when the initial session fetch completes.
|
|
41
|
+
* Awaited by route middleware to prevent reading auth state before session is ready.
|
|
42
|
+
* Pre-resolved on SSR (plugin completes before middleware runs).
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
*/
|
|
45
|
+
$sessionReady: Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
interface RuntimeNuxtHooks {
|
|
48
|
+
/**
|
|
49
|
+
* Hook triggered to fetch the latest session data from the server.
|
|
50
|
+
* Can be manually called when authentication state changes.
|
|
51
|
+
* @since 1.0.0
|
|
52
|
+
*/
|
|
53
|
+
'session:fetch': (initiator?: string) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Hook triggered after session `session:fetch`.
|
|
56
|
+
* @since 1.0.0
|
|
57
|
+
*/
|
|
58
|
+
'session:changed': (data: NuxtSessionContext) => Awaitable<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Hook triggered after a successful user login.
|
|
61
|
+
* Used to re-sync session state or perform follow-up logic.
|
|
62
|
+
* @since 1.0.0
|
|
63
|
+
*/
|
|
64
|
+
'user:loggedIn': () => Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Hook triggered after a successful user logout.
|
|
67
|
+
* Used to re-sync session state or perform follow-up logic.
|
|
68
|
+
* @since 1.0.0
|
|
69
|
+
*/
|
|
70
|
+
'user:loggedOut': () => Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 🧩 Nuxt plugin for managing user session state.
|
|
75
|
+
*
|
|
76
|
+
* This plugin initializes and maintains the authenticated user session context.
|
|
77
|
+
* It exposes helper methods and hooks for session refetching and user login events.
|
|
78
|
+
*
|
|
79
|
+
* @since 1.0.0
|
|
80
|
+
*/
|
|
81
|
+
declare const _default: any;
|
|
82
|
+
export default _default;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
createError,
|
|
4
|
+
defineNuxtPlugin,
|
|
5
|
+
refreshNuxtData,
|
|
6
|
+
useAsyncData,
|
|
7
|
+
useCookie,
|
|
8
|
+
useNuxtApp,
|
|
9
|
+
useRequestFetch,
|
|
10
|
+
useRuntimeConfig,
|
|
11
|
+
useState
|
|
12
|
+
} from "#app";
|
|
13
|
+
export default defineNuxtPlugin(async (_nuxtApp) => {
|
|
14
|
+
const fetchRequest = useRequestFetch();
|
|
15
|
+
const logger = useNuxtApp().$logging.getLogger({ tag: "nuxt-session", level: 3 });
|
|
16
|
+
const context = useState("sessionContext", () => null);
|
|
17
|
+
watch(
|
|
18
|
+
() => context.value,
|
|
19
|
+
(value) => {
|
|
20
|
+
_nuxtApp.callHook("session:changed", value);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
async function __getServerSession(initiator) {
|
|
24
|
+
try {
|
|
25
|
+
logger.debug(`[__getServerSession] Called with initiator: '${initiator}'`);
|
|
26
|
+
return await fetchRequest("/api/_auth/session");
|
|
27
|
+
} catch (ex) {
|
|
28
|
+
throw createError({
|
|
29
|
+
status: 500,
|
|
30
|
+
fatal: true,
|
|
31
|
+
statusMessage: "Failed to fetch session",
|
|
32
|
+
message: ex instanceof Error ? ex.message : String(ex),
|
|
33
|
+
cause: ex
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let resolveReady;
|
|
38
|
+
const sessionReady = new Promise((r) => {
|
|
39
|
+
resolveReady = r;
|
|
40
|
+
});
|
|
41
|
+
_nuxtApp.provide("sessionReady", sessionReady);
|
|
42
|
+
const sessionId = useCookie(useRuntimeConfig().public.nuxtCore.authts.sessionCookie);
|
|
43
|
+
logger.debug(`Initial session fetch on plugin init with sessionId: ${sessionId.value}`);
|
|
44
|
+
if (sessionId.value) {
|
|
45
|
+
const { data } = await useAsyncData("authts:session", () => __getServerSession("plugin initialization"));
|
|
46
|
+
if (data.value) {
|
|
47
|
+
context.value = data.value;
|
|
48
|
+
}
|
|
49
|
+
watch(data, (newData) => {
|
|
50
|
+
if (newData) context.value = newData;
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
context.value = {
|
|
54
|
+
id: "",
|
|
55
|
+
isAuthenticated: false,
|
|
56
|
+
user: {
|
|
57
|
+
id: "",
|
|
58
|
+
sub: "",
|
|
59
|
+
email: "",
|
|
60
|
+
firstName: "Anonymous",
|
|
61
|
+
lastName: "User",
|
|
62
|
+
locale: "",
|
|
63
|
+
language: "",
|
|
64
|
+
timezone: ""
|
|
65
|
+
},
|
|
66
|
+
data: {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
resolveReady();
|
|
70
|
+
_nuxtApp.hooks.addHooks({
|
|
71
|
+
// session:fetch calls refreshNuxtData to stay coherent with useAsyncData cache (D-04)
|
|
72
|
+
"session:fetch": async (_initiator) => {
|
|
73
|
+
await refreshNuxtData("authts:session");
|
|
74
|
+
},
|
|
75
|
+
"user:loggedIn": async () => {
|
|
76
|
+
await _nuxtApp.callHook("session:fetch", "user:loggedIn");
|
|
77
|
+
},
|
|
78
|
+
"user:loggedOut": async () => {
|
|
79
|
+
await _nuxtApp.callHook("session:fetch", "user:loggedOut");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
function getValue(...initiators) {
|
|
83
|
+
const initiator = initiators.map((s) => s || "?").join(" \u2192 ");
|
|
84
|
+
logger.debug(`Called getValue with initiator: '${initiator}'`);
|
|
85
|
+
return context.value;
|
|
86
|
+
}
|
|
87
|
+
async function refetch() {
|
|
88
|
+
await _nuxtApp.callHook("session:fetch");
|
|
89
|
+
}
|
|
90
|
+
_nuxtApp.provide("sessionContext", { getValue, refetch });
|
|
91
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type PadOptions = {
|
|
2
|
+
prefix: string & {
|
|
3
|
+
__uppercaseBrand?: never;
|
|
4
|
+
};
|
|
5
|
+
separator: string;
|
|
6
|
+
length: number;
|
|
7
|
+
padChar: string;
|
|
8
|
+
};
|
|
9
|
+
type ToPad = (num: number, opts?: Partial<PadOptions>) => string;
|
|
10
|
+
type AsPhoneText = (numStr: string) => string;
|
|
11
|
+
type AsDateString = (date: Date) => string;
|
|
12
|
+
type AsTimeString = (date: Date, seconds?: boolean) => string;
|
|
13
|
+
type AsDateTimeString = (date: Date, seconds?: boolean) => string;
|
|
14
|
+
type AsRelativeTimeString = (date: Date) => string;
|
|
15
|
+
type PluginProviders = {
|
|
16
|
+
toPad: ToPad;
|
|
17
|
+
asPhoneText: AsPhoneText;
|
|
18
|
+
asDateString: AsDateString;
|
|
19
|
+
asTimeString: AsTimeString;
|
|
20
|
+
asDateTimeString: AsDateTimeString;
|
|
21
|
+
asRelativeTimeString: AsRelativeTimeString;
|
|
22
|
+
};
|
|
23
|
+
declare module 'vue' {
|
|
24
|
+
interface ComponentCustomProperties {
|
|
25
|
+
$formatters: PluginProviders;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
declare const _default: any;
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
|
+
import { ref, useLogger } from "#imports";
|
|
3
|
+
export default defineNuxtPlugin((_nuxtApp) => {
|
|
4
|
+
const logger = useLogger({ tag: "formatters", level: 3 });
|
|
5
|
+
const { defaultLocale, defaultLanguage, defaultTimeZone } = useRuntimeConfig().public.nuxtCore;
|
|
6
|
+
const currentLocale = ref(defaultLocale);
|
|
7
|
+
const currentLanguage = ref(defaultLanguage);
|
|
8
|
+
const currentTimeZone = ref(defaultTimeZone);
|
|
9
|
+
_nuxtApp.hooks.addHooks({
|
|
10
|
+
"dfLocale:changed": (value) => {
|
|
11
|
+
const { locale = defaultLocale, timeZone = defaultTimeZone, language = defaultLanguage } = value;
|
|
12
|
+
currentLocale.value = locale;
|
|
13
|
+
currentLanguage.value = language;
|
|
14
|
+
currentTimeZone.value = timeZone;
|
|
15
|
+
logger.debug(`dfLocale changed, updating locale='${locale}', language='${language}' and timezone='${timeZone}'.`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
function toPad(num, opts) {
|
|
19
|
+
const defaultOpts = {
|
|
20
|
+
prefix: "",
|
|
21
|
+
separator: "-",
|
|
22
|
+
length: 5,
|
|
23
|
+
padChar: "0",
|
|
24
|
+
...opts
|
|
25
|
+
};
|
|
26
|
+
const parts = [String(num).padStart(defaultOpts.length, defaultOpts.padChar)];
|
|
27
|
+
if (defaultOpts.prefix) {
|
|
28
|
+
parts.push(defaultOpts.prefix);
|
|
29
|
+
}
|
|
30
|
+
return parts.reverse().join(defaultOpts.separator);
|
|
31
|
+
}
|
|
32
|
+
function asPhoneText(numStr) {
|
|
33
|
+
const digits = numStr.replace(/\D/g, "");
|
|
34
|
+
return digits.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");
|
|
35
|
+
}
|
|
36
|
+
function asDateString(date) {
|
|
37
|
+
logger.debug(`Format date with locale ='${currentLocale.value}'`);
|
|
38
|
+
return new Intl.DateTimeFormat(currentLocale.value).format(date);
|
|
39
|
+
}
|
|
40
|
+
function asTimeString(date, seconds) {
|
|
41
|
+
const opts = {
|
|
42
|
+
hour: "2-digit",
|
|
43
|
+
minute: "2-digit",
|
|
44
|
+
hour12: false,
|
|
45
|
+
timeZone: currentTimeZone.value
|
|
46
|
+
};
|
|
47
|
+
if (seconds) {
|
|
48
|
+
opts.second = "2-digit";
|
|
49
|
+
}
|
|
50
|
+
logger.debug(`Format time with options = '%o', locale ='${currentLocale.value}'`, opts);
|
|
51
|
+
return new Intl.DateTimeFormat(currentLocale.value, opts).format(date);
|
|
52
|
+
}
|
|
53
|
+
function asDateTimeString(date, seconds) {
|
|
54
|
+
const opts = {
|
|
55
|
+
year: "numeric",
|
|
56
|
+
month: "2-digit",
|
|
57
|
+
day: "2-digit",
|
|
58
|
+
hour: "2-digit",
|
|
59
|
+
minute: "2-digit",
|
|
60
|
+
hour12: false,
|
|
61
|
+
timeZone: currentTimeZone.value
|
|
62
|
+
};
|
|
63
|
+
if (seconds) {
|
|
64
|
+
opts.second = "2-digit";
|
|
65
|
+
}
|
|
66
|
+
logger.debug(`Format datetime with options = '%o', locale ='${currentLocale.value}'`, opts);
|
|
67
|
+
return new Intl.DateTimeFormat(currentLocale.value, opts).format(date).replace(",", "");
|
|
68
|
+
}
|
|
69
|
+
function asRelativeTimeString(date) {
|
|
70
|
+
const opts = {
|
|
71
|
+
numeric: "auto",
|
|
72
|
+
style: "long"
|
|
73
|
+
};
|
|
74
|
+
logger.debug(`Format datetime with options = '%o', language ='${currentLanguage.value}'`, opts);
|
|
75
|
+
const rtf = new Intl.RelativeTimeFormat(currentLanguage.value, opts);
|
|
76
|
+
const diff = date.valueOf() - Date.now();
|
|
77
|
+
const units = {
|
|
78
|
+
year: 1e3 * 60 * 60 * 24 * 365,
|
|
79
|
+
month: 1e3 * 60 * 60 * 24 * 30,
|
|
80
|
+
day: 1e3 * 60 * 60 * 24,
|
|
81
|
+
hour: 1e3 * 60 * 60,
|
|
82
|
+
minute: 1e3 * 60,
|
|
83
|
+
second: 1e3
|
|
84
|
+
};
|
|
85
|
+
for (const [unit, ms] of Object.entries(units)) {
|
|
86
|
+
const value = Math.round(diff / ms);
|
|
87
|
+
if (Math.abs(value) >= 1) {
|
|
88
|
+
return rtf.format(value, unit);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return asDateTimeString(date, true);
|
|
92
|
+
}
|
|
93
|
+
_nuxtApp.provide("formatters", {
|
|
94
|
+
toPad,
|
|
95
|
+
asPhoneText,
|
|
96
|
+
asDateString,
|
|
97
|
+
asTimeString,
|
|
98
|
+
asDateTimeString,
|
|
99
|
+
asRelativeTimeString
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type LocaleState = {
|
|
2
|
+
locale: string;
|
|
3
|
+
language: string;
|
|
4
|
+
timeZone: string;
|
|
5
|
+
};
|
|
6
|
+
type LocaleProvider = LocaleState;
|
|
7
|
+
/**
|
|
8
|
+
* 🧩 Nuxt plugin for locale utilities.
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: any;
|
|
12
|
+
export default _default;
|
|
13
|
+
declare module 'vue' {
|
|
14
|
+
/**
|
|
15
|
+
* Provides access to locale utilities.
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
*/
|
|
18
|
+
interface ComponentCustomProperties {
|
|
19
|
+
$dfLocale: LocaleProvider;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
declare module '#app' {
|
|
23
|
+
interface NuxtApp {
|
|
24
|
+
/**
|
|
25
|
+
* Provides access to locale utilities.
|
|
26
|
+
* @since 1.0.0
|
|
27
|
+
*/
|
|
28
|
+
$dfLocale: LocaleProvider;
|
|
29
|
+
}
|
|
30
|
+
interface RuntimeNuxtHooks {
|
|
31
|
+
/**
|
|
32
|
+
* Hook triggered after locale changed.
|
|
33
|
+
* @since 1.0.0
|
|
34
|
+
*/
|
|
35
|
+
'dfLocale:changed': (value: LocaleState) => Awaitable<void>;
|
|
36
|
+
}
|
|
37
|
+
}
|