nuxt-huia-headless 1.0.0-alpha.5

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.
Files changed (70) hide show
  1. package/README.md +110 -0
  2. package/dist/module.d.mts +67 -0
  3. package/dist/module.json +12 -0
  4. package/dist/module.mjs +146 -0
  5. package/dist/runtime/app/composables/useHuia.d.ts +107 -0
  6. package/dist/runtime/app/composables/useHuia.js +100 -0
  7. package/dist/runtime/app/composables/useHuiaAdmin.d.ts +24 -0
  8. package/dist/runtime/app/composables/useHuiaAdmin.js +58 -0
  9. package/dist/runtime/app/composables/useUserSession.d.ts +48 -0
  10. package/dist/runtime/app/composables/useUserSession.js +44 -0
  11. package/dist/runtime/app/middleware/auth.d.ts +3 -0
  12. package/dist/runtime/app/middleware/auth.js +12 -0
  13. package/dist/runtime/app/plugins/session.server.d.ts +8 -0
  14. package/dist/runtime/app/plugins/session.server.js +8 -0
  15. package/dist/runtime/app/utils/route-match.d.ts +8 -0
  16. package/dist/runtime/app/utils/route-match.js +9 -0
  17. package/dist/runtime/server/middleware/session.context.d.ts +8 -0
  18. package/dist/runtime/server/middleware/session.context.js +6 -0
  19. package/dist/runtime/server/routes/auth/admin.d.ts +2 -0
  20. package/dist/runtime/server/routes/auth/admin.js +55 -0
  21. package/dist/runtime/server/routes/auth/confirm-email.get.d.ts +4 -0
  22. package/dist/runtime/server/routes/auth/confirm-email.get.js +19 -0
  23. package/dist/runtime/server/routes/auth/external/[provider].get.d.ts +11 -0
  24. package/dist/runtime/server/routes/auth/external/[provider].get.js +16 -0
  25. package/dist/runtime/server/routes/auth/external/complete-profile.post.d.ts +4 -0
  26. package/dist/runtime/server/routes/auth/external/complete-profile.post.js +19 -0
  27. package/dist/runtime/server/routes/auth/external/exchange.post.d.ts +16 -0
  28. package/dist/runtime/server/routes/auth/external/exchange.post.js +28 -0
  29. package/dist/runtime/server/routes/auth/forgot-password.post.d.ts +4 -0
  30. package/dist/runtime/server/routes/auth/forgot-password.post.js +16 -0
  31. package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
  32. package/dist/runtime/server/routes/auth/login.post.js +19 -0
  33. package/dist/runtime/server/routes/auth/logout.post.d.ts +9 -0
  34. package/dist/runtime/server/routes/auth/logout.post.js +6 -0
  35. package/dist/runtime/server/routes/auth/phone/complete-profile.post.d.ts +4 -0
  36. package/dist/runtime/server/routes/auth/phone/complete-profile.post.js +19 -0
  37. package/dist/runtime/server/routes/auth/phone/start.post.d.ts +2 -0
  38. package/dist/runtime/server/routes/auth/phone/start.post.js +15 -0
  39. package/dist/runtime/server/routes/auth/phone/verify.post.d.ts +10 -0
  40. package/dist/runtime/server/routes/auth/phone/verify.post.js +22 -0
  41. package/dist/runtime/server/routes/auth/refresh.post.d.ts +7 -0
  42. package/dist/runtime/server/routes/auth/refresh.post.js +6 -0
  43. package/dist/runtime/server/routes/auth/register.post.d.ts +4 -0
  44. package/dist/runtime/server/routes/auth/register.post.js +16 -0
  45. package/dist/runtime/server/routes/auth/resend-confirmation.post.d.ts +4 -0
  46. package/dist/runtime/server/routes/auth/resend-confirmation.post.js +16 -0
  47. package/dist/runtime/server/routes/auth/reset-password.post.d.ts +4 -0
  48. package/dist/runtime/server/routes/auth/reset-password.post.js +16 -0
  49. package/dist/runtime/server/routes/auth/session.get.d.ts +2 -0
  50. package/dist/runtime/server/routes/auth/session.get.js +6 -0
  51. package/dist/runtime/server/utils/backend.d.ts +50 -0
  52. package/dist/runtime/server/utils/backend.js +81 -0
  53. package/dist/runtime/server/utils/config.d.ts +3 -0
  54. package/dist/runtime/server/utils/config.js +39 -0
  55. package/dist/runtime/server/utils/cookie.d.ts +9 -0
  56. package/dist/runtime/server/utils/cookie.js +90 -0
  57. package/dist/runtime/server/utils/internal-types.d.ts +93 -0
  58. package/dist/runtime/server/utils/internal-types.js +0 -0
  59. package/dist/runtime/server/utils/refresh.d.ts +7 -0
  60. package/dist/runtime/server/utils/refresh.js +104 -0
  61. package/dist/runtime/server/utils/session.d.ts +10 -0
  62. package/dist/runtime/server/utils/session.js +98 -0
  63. package/dist/runtime/server/utils/storage.d.ts +11 -0
  64. package/dist/runtime/server/utils/storage.js +31 -0
  65. package/dist/runtime/server/utils/tokens.d.ts +5 -0
  66. package/dist/runtime/server/utils/tokens.js +5 -0
  67. package/dist/runtime/types.d.ts +62 -0
  68. package/dist/runtime/types.js +0 -0
  69. package/dist/types.d.mts +3 -0
  70. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # nuxt-huia-headless
2
+
3
+ **Nuxt 4** authentication module for [`Huia.Headless`](https://github.com/Ayman-Elfaki/Huia) — a
4
+ bearer-token identity API (register, login, refresh, passkeys, passwordless SMS, external providers)
5
+ with no OAuth redirect dance for password/phone sign-in. Same dual-layer session principle as
6
+ [`nuxt-huia-oidc`](../nuxt-huia-oidc): the browser only ever holds a sealed, chunked session cookie;
7
+ access/refresh tokens live server-side in Nitro Storage and are refreshed transparently.
8
+
9
+ - Requires Nuxt `>=4`, Nitro `>=2.10`, Node `>=20.11`
10
+ - No `openid-client` dependency — this talks to Huia.Headless with plain `fetch`, since there is no
11
+ authorization-code redirect or discovery document to negotiate.
12
+ - Docs: <https://github.com/Ayman-Elfaki/Huia/tree/main/docs/nuxt-headless>
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install nuxt-huia-headless
18
+ ```
19
+
20
+ ```ts
21
+ // nuxt.config.ts
22
+ export default defineNuxtConfig({
23
+ modules: ['nuxt-huia-headless'],
24
+ huiaHeadless: {
25
+ baseUrl: 'https://api.example.com',
26
+ // session.password via NUXT_HUIA_HEADLESS_SESSION_PASSWORD (>= 32 chars)
27
+ },
28
+ })
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```vue
34
+ <!-- app/pages/login.vue — the app owns this page; Huia.Headless has no hosted login UI -->
35
+ <script setup lang="ts">
36
+ const { login } = useHuia()
37
+ const result = await login({ email, password })
38
+ </script>
39
+ ```
40
+
41
+ ```vue
42
+ <script setup lang="ts">
43
+ const { user, loggedIn, logout } = useHuia()
44
+ </script>
45
+ ```
46
+
47
+ Passwordless SMS is a two- or three-step flow — `verifyPhoneLogin` returns the bearer session
48
+ directly for a returning, complete-profile account, or `{ requiresProfile: true, flowId }` for a
49
+ first-time number that still needs a name:
50
+
51
+ ```vue
52
+ <script setup lang="ts">
53
+ const { startPhoneLogin, verifyPhoneLogin, completePhoneProfile } = useHuia()
54
+
55
+ const { flowId } = await startPhoneLogin({ phoneNumber: '+12025550123' })
56
+ const result = await verifyPhoneLogin({ flowId, code })
57
+ if (result.requiresProfile) {
58
+ await completePhoneProfile({ flowId: result.flowId, firstName, lastName })
59
+ }
60
+ </script>
61
+ ```
62
+
63
+ External login (Google/GitHub/Microsoft/generic OIDC) is a real browser redirect through the
64
+ provider, so it starts as a plain link, not a `fetch` call — `externalLoginHref` builds it:
65
+
66
+ ```vue
67
+ <!-- app/pages/login.vue -->
68
+ <a :href="useHuia().externalLoginHref('google', '/dashboard')">Sign in with Google</a>
69
+ ```
70
+
71
+ ```vue
72
+ <!-- app/pages/auth/callback.vue — the page huiaHeadless.externalCallbackPage points at (default /auth/callback) -->
73
+ <script setup lang="ts">
74
+ const { code, returnTo } = useRoute().query
75
+ const { exchangeExternalCode, completeExternalProfile } = useHuia()
76
+
77
+ const result = await exchangeExternalCode(code as string)
78
+ if (!result.requiresProfile) await navigateTo((returnTo as string) ?? '/')
79
+ // else render a small first/last name form, then call completeExternalProfile({ code: result.flowId, firstName, lastName })
80
+ </script>
81
+ ```
82
+
83
+ ```ts
84
+ // server/api/me.get.ts
85
+ export default defineEventHandler(async (event) => {
86
+ const { user } = await requireUserSession(event) // 401 if anonymous
87
+ return { id: user.sub, email: user.email, roles: user.roles }
88
+ })
89
+ ```
90
+
91
+ Protect a page with the built-in `auth` middleware (`definePageMeta({ middleware: 'auth' })`, or
92
+ `huiaHeadless.middleware.global = true` for every page); an unauthenticated visit redirects to
93
+ `huiaHeadless.loginPage` (default `/login`) rather than an external URL — there is nothing external
94
+ to redirect to. `huiaHeadless.middleware.exclude` (glob patterns, `**`/`*` supported) opts specific
95
+ pages out; the module's own auth routes and `loginPage` are always excluded regardless.
96
+
97
+ `logout()` is purely local — it clears the session cookie and the stored token record. Huia.Headless's
98
+ bearer tokens are the framework's stock (non-revocable) format, so unlike `nuxt-huia-oidc` there is no
99
+ upstream session to end and no redirect.
100
+
101
+ ## Develop
102
+
103
+ ```bash
104
+ npm install
105
+ npm run dev:prepare
106
+ npm test # Vitest unit + integration (mocked Huia.Headless backend)
107
+ npm run dev # playground on http://localhost:3000
108
+ ```
109
+
110
+ `.npmrc` pins `legacy-peer-deps=true` (an npm 10 arborist crash on the nuxt peer graph).
@@ -0,0 +1,67 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ baseUrl?: string;
5
+ session?: {
6
+ name?: string;
7
+ password?: string;
8
+ maxAge?: number;
9
+ cookie?: {
10
+ sameSite?: 'lax' | 'strict' | 'none';
11
+ secure?: boolean;
12
+ };
13
+ userClaims?: string[];
14
+ };
15
+ storage?: {
16
+ base?: string;
17
+ };
18
+ refresh?: {
19
+ enabled?: boolean;
20
+ earlyRefreshSeconds?: number;
21
+ lock?: {
22
+ ttlMs?: number;
23
+ waitMs?: number;
24
+ pollMs?: number;
25
+ };
26
+ };
27
+ cookie?: {
28
+ chunkSize?: number;
29
+ maxChunks?: number;
30
+ };
31
+ middleware?: {
32
+ global?: boolean;
33
+ exclude?: string[];
34
+ };
35
+ hydration?: 'useState' | 'asyncData';
36
+ routes?: {
37
+ register?: string;
38
+ login?: string;
39
+ logout?: string;
40
+ refresh?: string;
41
+ session?: string;
42
+ confirmEmail?: string;
43
+ resendConfirmation?: string;
44
+ forgotPassword?: string;
45
+ resetPassword?: string;
46
+ phoneStart?: string;
47
+ phoneVerify?: string;
48
+ phoneCompleteProfile?: string;
49
+ /** Prefix the external-login challenge redirects from; the provider name is appended (`{prefix}/{provider}`). */
50
+ externalLogin?: string;
51
+ externalExchange?: string;
52
+ externalCompleteProfile?: string;
53
+ admin?: string;
54
+ };
55
+ /** The app's own login page (Huia.Headless has no hosted login UI to redirect to). */
56
+ loginPage?: string;
57
+ /**
58
+ * The app's own page that reads `?code=` off the query string after an external-provider callback
59
+ * and calls `externalExchange` (Huia.Headless has no hosted "completing sign-in…" page either).
60
+ */
61
+ externalCallbackPage?: string;
62
+ allowInsecureTls?: boolean;
63
+ }
64
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
65
+
66
+ export { _default as default };
67
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-huia-headless",
3
+ "configKey": "huiaHeadless",
4
+ "compatibility": {
5
+ "nuxt": ">=4.0.0"
6
+ },
7
+ "version": "1.0.0-alpha.5",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,146 @@
1
+ import { defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addPlugin, addRouteMiddleware, addServerHandler } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+
4
+ const defaults = {
5
+ baseUrl: "",
6
+ session: {
7
+ name: "__Host-huia_headless_sess",
8
+ password: "",
9
+ maxAge: 60 * 60 * 24 * 7,
10
+ cookie: { sameSite: "lax" },
11
+ userClaims: ["sub", "email", "firstName", "lastName", "roles"]
12
+ },
13
+ storage: { base: "huia-headless-auth" },
14
+ refresh: {
15
+ enabled: true,
16
+ earlyRefreshSeconds: 60,
17
+ lock: { ttlMs: 1e4, waitMs: 8e3, pollMs: 150 }
18
+ },
19
+ cookie: { chunkSize: 3800, maxChunks: 8 },
20
+ middleware: { global: false, exclude: [] },
21
+ hydration: "useState",
22
+ routes: {
23
+ register: "/auth/register",
24
+ login: "/auth/login",
25
+ logout: "/auth/logout",
26
+ refresh: "/auth/refresh",
27
+ session: "/auth/session",
28
+ confirmEmail: "/auth/confirm-email",
29
+ resendConfirmation: "/auth/resend-confirmation",
30
+ forgotPassword: "/auth/forgot-password",
31
+ resetPassword: "/auth/reset-password",
32
+ phoneStart: "/auth/phone/start",
33
+ phoneVerify: "/auth/phone/verify",
34
+ phoneCompleteProfile: "/auth/phone/complete-profile",
35
+ externalLogin: "/auth/external",
36
+ externalExchange: "/auth/external-exchange",
37
+ externalCompleteProfile: "/auth/external-complete-profile",
38
+ admin: "/auth/admin"
39
+ },
40
+ loginPage: "/login",
41
+ externalCallbackPage: "/auth/callback",
42
+ allowInsecureTls: false
43
+ };
44
+ const module$1 = defineNuxtModule({
45
+ meta: {
46
+ name: "nuxt-huia-headless",
47
+ configKey: "huiaHeadless",
48
+ compatibility: { nuxt: ">=4.0.0" }
49
+ },
50
+ defaults,
51
+ setup(options, nuxt) {
52
+ const { resolve } = createResolver(import.meta.url);
53
+ const opts = defu(options, defaults);
54
+ if (!opts.session.password && !process.env.NUXT_HUIA_HEADLESS_SESSION_PASSWORD) {
55
+ console.warn("[huia-headless-auth] no session password set \u2014 set NUXT_HUIA_HEADLESS_SESSION_PASSWORD (>= 32 chars)");
56
+ }
57
+ nuxt.options.runtimeConfig.huiaHeadless = defu(
58
+ nuxt.options.runtimeConfig.huiaHeadless,
59
+ {
60
+ baseUrl: opts.baseUrl ?? "",
61
+ session: { ...opts.session, password: opts.session.password ?? "" },
62
+ storage: opts.storage,
63
+ refresh: opts.refresh,
64
+ cookie: opts.cookie,
65
+ allowInsecureTls: opts.allowInsecureTls,
66
+ externalCallbackPath: opts.externalCallbackPage
67
+ }
68
+ );
69
+ nuxt.options.runtimeConfig.public.huiaHeadless = defu(
70
+ nuxt.options.runtimeConfig.public.huiaHeadless,
71
+ {
72
+ registerPath: opts.routes.register,
73
+ loginPath: opts.routes.login,
74
+ logoutPath: opts.routes.logout,
75
+ sessionPath: opts.routes.session,
76
+ loginPage: opts.loginPage,
77
+ phoneStartPath: opts.routes.phoneStart,
78
+ phoneVerifyPath: opts.routes.phoneVerify,
79
+ phoneCompleteProfilePath: opts.routes.phoneCompleteProfile,
80
+ externalLoginPath: opts.routes.externalLogin,
81
+ externalExchangePath: opts.routes.externalExchange,
82
+ externalCompleteProfilePath: opts.routes.externalCompleteProfile,
83
+ externalCallbackPage: opts.externalCallbackPage,
84
+ adminPath: opts.routes.admin,
85
+ // The module's own auth routes are never protected by its own middleware, regardless of
86
+ // `middleware.exclude`, plus the app's own login/callback pages — same rationale as
87
+ // nuxt-huia-oidc: applying "must be logged in" to the routes/pages that exist for the
88
+ // logged-out flow is never correct.
89
+ middlewareExclude: [
90
+ ...opts.middleware.exclude,
91
+ opts.loginPage,
92
+ opts.externalCallbackPage,
93
+ opts.routes.register,
94
+ opts.routes.login,
95
+ opts.routes.logout,
96
+ opts.routes.refresh,
97
+ opts.routes.session,
98
+ opts.routes.confirmEmail,
99
+ opts.routes.resendConfirmation,
100
+ opts.routes.forgotPassword,
101
+ opts.routes.resetPassword,
102
+ opts.routes.phoneStart,
103
+ opts.routes.phoneVerify,
104
+ opts.routes.phoneCompleteProfile,
105
+ `${opts.routes.externalLogin}/**`,
106
+ opts.routes.externalExchange,
107
+ opts.routes.externalCompleteProfile,
108
+ `${opts.routes.admin}/**`
109
+ ]
110
+ }
111
+ );
112
+ nuxt.options.alias["#huia-headless-auth"] = resolve("runtime/types");
113
+ addServerImportsDir(resolve("runtime/server/utils"));
114
+ addImportsDir(resolve("runtime/app/composables"));
115
+ if (opts.hydration === "useState") {
116
+ addPlugin({ src: resolve("runtime/app/plugins/session.server"), mode: "server" });
117
+ }
118
+ addRouteMiddleware({
119
+ name: "auth",
120
+ path: resolve("runtime/app/middleware/auth"),
121
+ global: opts.middleware.global
122
+ });
123
+ addServerHandler({ route: opts.routes.register, method: "post", handler: resolve("runtime/server/routes/auth/register.post") });
124
+ addServerHandler({ route: opts.routes.login, method: "post", handler: resolve("runtime/server/routes/auth/login.post") });
125
+ addServerHandler({ route: opts.routes.logout, method: "post", handler: resolve("runtime/server/routes/auth/logout.post") });
126
+ addServerHandler({ route: opts.routes.refresh, method: "post", handler: resolve("runtime/server/routes/auth/refresh.post") });
127
+ addServerHandler({ route: opts.routes.session, method: "get", handler: resolve("runtime/server/routes/auth/session.get") });
128
+ addServerHandler({ route: opts.routes.confirmEmail, method: "get", handler: resolve("runtime/server/routes/auth/confirm-email.get") });
129
+ addServerHandler({ route: opts.routes.resendConfirmation, method: "post", handler: resolve("runtime/server/routes/auth/resend-confirmation.post") });
130
+ addServerHandler({ route: opts.routes.forgotPassword, method: "post", handler: resolve("runtime/server/routes/auth/forgot-password.post") });
131
+ addServerHandler({ route: opts.routes.resetPassword, method: "post", handler: resolve("runtime/server/routes/auth/reset-password.post") });
132
+ addServerHandler({ route: opts.routes.phoneStart, method: "post", handler: resolve("runtime/server/routes/auth/phone/start.post") });
133
+ addServerHandler({ route: opts.routes.phoneVerify, method: "post", handler: resolve("runtime/server/routes/auth/phone/verify.post") });
134
+ addServerHandler({ route: opts.routes.phoneCompleteProfile, method: "post", handler: resolve("runtime/server/routes/auth/phone/complete-profile.post") });
135
+ addServerHandler({ route: `${opts.routes.externalLogin}/:provider`, method: "get", handler: resolve("runtime/server/routes/auth/external/[provider].get") });
136
+ addServerHandler({ route: opts.routes.externalExchange, method: "post", handler: resolve("runtime/server/routes/auth/external/exchange.post") });
137
+ addServerHandler({ route: opts.routes.externalCompleteProfile, method: "post", handler: resolve("runtime/server/routes/auth/external/complete-profile.post") });
138
+ addServerHandler({ route: `${opts.routes.admin}/**`, handler: resolve("runtime/server/routes/auth/admin") });
139
+ addServerHandler({ middleware: true, handler: resolve("runtime/server/middleware/session.context") });
140
+ nuxt.hook("prepare:types", ({ references }) => {
141
+ references.push({ path: resolve("runtime/types.d.ts") });
142
+ });
143
+ }
144
+ });
145
+
146
+ export { module$1 as default };
@@ -0,0 +1,107 @@
1
+ import type { UserClaims } from '../../types.js';
2
+ export interface HuiaLoginResult {
3
+ ok: boolean;
4
+ user?: UserClaims;
5
+ /** The raw problem-details body from Huia.Headless when `ok` is false. */
6
+ error?: unknown;
7
+ }
8
+ /** Either a completed sign-in, or a first-time sign-up still needing a first/last name. */
9
+ export type HuiaFlowResult = {
10
+ ok: true;
11
+ requiresProfile: false;
12
+ user: UserClaims;
13
+ } | {
14
+ ok: true;
15
+ requiresProfile: true;
16
+ flowId: string;
17
+ email?: string | null;
18
+ firstName?: string | null;
19
+ lastName?: string | null;
20
+ } | {
21
+ ok: false;
22
+ requiresProfile: false;
23
+ error: unknown;
24
+ };
25
+ export declare function useHuia(): {
26
+ user: import("vue").ComputedRef<UserClaims | null>;
27
+ loggedIn: import("vue").ComputedRef<boolean>;
28
+ session: Readonly<import("vue").Ref<{
29
+ readonly user?: {
30
+ readonly [x: string]: Readonly<unknown>;
31
+ readonly sub: string;
32
+ readonly email?: string | undefined;
33
+ readonly firstName?: string | undefined;
34
+ readonly lastName?: string | undefined;
35
+ readonly roles?: readonly string[] | undefined;
36
+ } | undefined;
37
+ readonly loggedIn?: boolean | undefined;
38
+ readonly expiresAt?: number | undefined;
39
+ }, {
40
+ readonly user?: {
41
+ readonly [x: string]: Readonly<unknown>;
42
+ readonly sub: string;
43
+ readonly email?: string | undefined;
44
+ readonly firstName?: string | undefined;
45
+ readonly lastName?: string | undefined;
46
+ readonly roles?: readonly string[] | undefined;
47
+ } | undefined;
48
+ readonly loggedIn?: boolean | undefined;
49
+ readonly expiresAt?: number | undefined;
50
+ }>>;
51
+ hasRole: (role: string) => boolean;
52
+ hasAnyRole: (...roles: string[]) => boolean;
53
+ /** Creates an account. Does not sign in — call `login()` afterward (once any required email confirmation is done). */
54
+ register(opts: {
55
+ email: string;
56
+ password: string;
57
+ firstName: string;
58
+ lastName: string;
59
+ }): Promise<{
60
+ ok: boolean;
61
+ error?: unknown;
62
+ }>;
63
+ /** Signs in with email + password (and, when required, a `twoFactorCode`/`twoFactorRecoveryCode`). */
64
+ login(opts: {
65
+ email: string;
66
+ password: string;
67
+ twoFactorCode?: string;
68
+ twoFactorRecoveryCode?: string;
69
+ }): Promise<HuiaLoginResult>;
70
+ /** Local logout — clears the session cookie and the stored tokens. Never redirects. */
71
+ logout: () => Promise<void>;
72
+ /** Sends a one-time SMS code and returns an opaque `flowId` — pass it to `verifyPhoneLogin`. */
73
+ startPhoneLogin(opts: {
74
+ phoneNumber: string;
75
+ country?: string;
76
+ captchaResponse?: string;
77
+ }): Promise<{
78
+ ok: boolean;
79
+ flowId?: string;
80
+ error?: unknown;
81
+ }>;
82
+ /** Verifies a phone one-time code. Signs in directly, or reports a first-time sign-up needs a name. */
83
+ verifyPhoneLogin(opts: {
84
+ flowId: string;
85
+ code: string;
86
+ }): Promise<HuiaFlowResult>;
87
+ /** Completes a first-time phone sign-up with a name, then signs in. */
88
+ completePhoneProfile(opts: {
89
+ flowId: string;
90
+ firstName: string;
91
+ lastName: string;
92
+ }): Promise<HuiaLoginResult>;
93
+ /**
94
+ * The URL to navigate the browser to (a plain link, not a fetch call) to start signing in with
95
+ * `provider` (must match a name configured on the Huia.Headless tenant). `returnTo` is a
96
+ * same-origin path within this app, resolved once the provider round trip completes.
97
+ */
98
+ externalLoginHref(provider: string, returnTo?: string): string;
99
+ /** Exchanges the one-time `code` from the callback URL. Signs in directly, or needs a name for a new sign-up. */
100
+ exchangeExternalCode(code: string): Promise<HuiaFlowResult>;
101
+ /** Completes a first-time external sign-up with a name, then signs in. */
102
+ completeExternalProfile(opts: {
103
+ code: string;
104
+ firstName: string;
105
+ lastName: string;
106
+ }): Promise<HuiaLoginResult>;
107
+ };
@@ -0,0 +1,100 @@
1
+ import { useRequestFetch, useRuntimeConfig } from "#imports";
2
+ import { useUserSession } from "./useUserSession.js";
3
+ export function useHuia() {
4
+ const { user, loggedIn, session, hasRole, hasAnyRole, fetch: fetchSession, clear } = useUserSession();
5
+ const paths = useRuntimeConfig().public.huiaHeadless;
6
+ return {
7
+ user,
8
+ loggedIn,
9
+ session,
10
+ hasRole,
11
+ hasAnyRole,
12
+ /** Creates an account. Does not sign in — call `login()` afterward (once any required email confirmation is done). */
13
+ async register(opts) {
14
+ try {
15
+ await useRequestFetch()(paths.registerPath, { method: "POST", body: opts });
16
+ return { ok: true };
17
+ } catch (err) {
18
+ return { ok: false, error: errorData(err) };
19
+ }
20
+ },
21
+ /** Signs in with email + password (and, when required, a `twoFactorCode`/`twoFactorRecoveryCode`). */
22
+ async login(opts) {
23
+ try {
24
+ const result = await useRequestFetch()(paths.loginPath, { method: "POST", body: opts });
25
+ await fetchSession();
26
+ return { ok: true, user: result.user };
27
+ } catch (err) {
28
+ return { ok: false, error: errorData(err) };
29
+ }
30
+ },
31
+ /** Local logout — clears the session cookie and the stored tokens. Never redirects. */
32
+ logout: clear,
33
+ /** Sends a one-time SMS code and returns an opaque `flowId` — pass it to `verifyPhoneLogin`. */
34
+ async startPhoneLogin(opts) {
35
+ try {
36
+ const result = await useRequestFetch()(paths.phoneStartPath, { method: "POST", body: opts });
37
+ return { ok: true, flowId: result.flowId };
38
+ } catch (err) {
39
+ return { ok: false, error: errorData(err) };
40
+ }
41
+ },
42
+ /** Verifies a phone one-time code. Signs in directly, or reports a first-time sign-up needs a name. */
43
+ async verifyPhoneLogin(opts) {
44
+ try {
45
+ const result = await useRequestFetch()(paths.phoneVerifyPath, { method: "POST", body: opts });
46
+ if ("requiresProfile" in result) {
47
+ return { ok: true, requiresProfile: true, flowId: result.flowId };
48
+ }
49
+ await fetchSession();
50
+ return { ok: true, requiresProfile: false, user: result.user };
51
+ } catch (err) {
52
+ return { ok: false, requiresProfile: false, error: errorData(err) };
53
+ }
54
+ },
55
+ /** Completes a first-time phone sign-up with a name, then signs in. */
56
+ async completePhoneProfile(opts) {
57
+ try {
58
+ const result = await useRequestFetch()(paths.phoneCompleteProfilePath, { method: "POST", body: opts });
59
+ await fetchSession();
60
+ return { ok: true, user: result.user };
61
+ } catch (err) {
62
+ return { ok: false, error: errorData(err) };
63
+ }
64
+ },
65
+ /**
66
+ * The URL to navigate the browser to (a plain link, not a fetch call) to start signing in with
67
+ * `provider` (must match a name configured on the Huia.Headless tenant). `returnTo` is a
68
+ * same-origin path within this app, resolved once the provider round trip completes.
69
+ */
70
+ externalLoginHref(provider, returnTo = "/") {
71
+ return `${paths.externalLoginPath}/${encodeURIComponent(provider)}?returnUrl=${encodeURIComponent(returnTo)}`;
72
+ },
73
+ /** Exchanges the one-time `code` from the callback URL. Signs in directly, or needs a name for a new sign-up. */
74
+ async exchangeExternalCode(code) {
75
+ try {
76
+ const result = await useRequestFetch()(paths.externalExchangePath, { method: "POST", body: { code } });
77
+ if ("requiresProfile" in result) {
78
+ return { ok: true, requiresProfile: true, flowId: result.code, email: result.email, firstName: result.firstName, lastName: result.lastName };
79
+ }
80
+ await fetchSession();
81
+ return { ok: true, requiresProfile: false, user: result.user };
82
+ } catch (err) {
83
+ return { ok: false, requiresProfile: false, error: errorData(err) };
84
+ }
85
+ },
86
+ /** Completes a first-time external sign-up with a name, then signs in. */
87
+ async completeExternalProfile(opts) {
88
+ try {
89
+ const result = await useRequestFetch()(paths.externalCompleteProfilePath, { method: "POST", body: opts });
90
+ await fetchSession();
91
+ return { ok: true, user: result.user };
92
+ } catch (err) {
93
+ return { ok: false, error: errorData(err) };
94
+ }
95
+ }
96
+ };
97
+ }
98
+ function errorData(err) {
99
+ return err?.data ?? err;
100
+ }
@@ -0,0 +1,24 @@
1
+ import type { HeadlessAdminUser, HeadlessAdminRole, HeadlessAdminUsersPage, CreateHeadlessAdminUserRequest, UpdateHeadlessAdminUserRequest } from 'huia-auth-core';
2
+ export declare function useHuiaAdmin(): {
3
+ listUsers(params?: {
4
+ page?: number;
5
+ pageSize?: number;
6
+ search?: string;
7
+ }): Promise<HeadlessAdminUsersPage>;
8
+ getUser(id: string): Promise<HeadlessAdminUser>;
9
+ createUser(body: CreateHeadlessAdminUserRequest): Promise<HeadlessAdminUser>;
10
+ updateUser(id: string, body: UpdateHeadlessAdminUserRequest): Promise<HeadlessAdminUser>;
11
+ deleteUser(id: string): Promise<void>;
12
+ getUserRoles(id: string): Promise<string[]>;
13
+ addUserRole(id: string, role: string): Promise<void>;
14
+ removeUserRole(id: string, role: string): Promise<void>;
15
+ lockUser(id: string): Promise<void>;
16
+ unlockUser(id: string): Promise<void>;
17
+ listRoles(): Promise<{
18
+ data: HeadlessAdminRole[];
19
+ }>;
20
+ getRole(id: string): Promise<HeadlessAdminRole>;
21
+ createRole(name: string): Promise<HeadlessAdminRole>;
22
+ updateRole(id: string, name: string): Promise<HeadlessAdminRole>;
23
+ deleteRole(id: string): Promise<void>;
24
+ };
@@ -0,0 +1,58 @@
1
+ import { useRequestFetch, useRuntimeConfig } from "#imports";
2
+ export function useHuiaAdmin() {
3
+ const paths = useRuntimeConfig().public.huiaHeadless;
4
+ const adminPath = paths.adminPath ?? "/auth/admin";
5
+ const fetch = useRequestFetch();
6
+ return {
7
+ async listUsers(params) {
8
+ const q = new URLSearchParams();
9
+ if (params?.page) q.set("page", String(params.page));
10
+ if (params?.pageSize) q.set("pageSize", String(params.pageSize));
11
+ if (params?.search) q.set("search", params.search);
12
+ const qs = q.toString() ? `?${q}` : "";
13
+ return await fetch(`${adminPath}/users${qs}`, { method: "GET" });
14
+ },
15
+ async getUser(id) {
16
+ return await fetch(`${adminPath}/users/${encodeURIComponent(id)}`, { method: "GET" });
17
+ },
18
+ async createUser(body) {
19
+ return await fetch(`${adminPath}/users`, { method: "POST", body });
20
+ },
21
+ async updateUser(id, body) {
22
+ return await fetch(`${adminPath}/users/${encodeURIComponent(id)}`, { method: "PUT", body });
23
+ },
24
+ async deleteUser(id) {
25
+ await fetch(`${adminPath}/users/${encodeURIComponent(id)}`, { method: "DELETE" });
26
+ },
27
+ async getUserRoles(id) {
28
+ return await fetch(`${adminPath}/users/${encodeURIComponent(id)}/roles`, { method: "GET" });
29
+ },
30
+ async addUserRole(id, role) {
31
+ await fetch(`${adminPath}/users/${encodeURIComponent(id)}/roles`, { method: "POST", body: { role } });
32
+ },
33
+ async removeUserRole(id, role) {
34
+ await fetch(`${adminPath}/users/${encodeURIComponent(id)}/roles/${encodeURIComponent(role)}`, { method: "DELETE" });
35
+ },
36
+ async lockUser(id) {
37
+ await fetch(`${adminPath}/users/${encodeURIComponent(id)}/lock`, { method: "POST" });
38
+ },
39
+ async unlockUser(id) {
40
+ await fetch(`${adminPath}/users/${encodeURIComponent(id)}/unlock`, { method: "POST" });
41
+ },
42
+ async listRoles() {
43
+ return await fetch(`${adminPath}/roles`, { method: "GET" });
44
+ },
45
+ async getRole(id) {
46
+ return await fetch(`${adminPath}/roles/${encodeURIComponent(id)}`, { method: "GET" });
47
+ },
48
+ async createRole(name) {
49
+ return await fetch(`${adminPath}/roles`, { method: "POST", body: { name } });
50
+ },
51
+ async updateRole(id, name) {
52
+ return await fetch(`${adminPath}/roles/${encodeURIComponent(id)}`, { method: "PUT", body: { name } });
53
+ },
54
+ async deleteRole(id) {
55
+ await fetch(`${adminPath}/roles/${encodeURIComponent(id)}`, { method: "DELETE" });
56
+ }
57
+ };
58
+ }
@@ -0,0 +1,48 @@
1
+ import type { UserClaims } from '../../types.js';
2
+ export declare function useUserSession(): {
3
+ session: Readonly<import("vue").Ref<{
4
+ readonly user?: {
5
+ readonly [x: string]: Readonly<unknown>;
6
+ readonly sub: string;
7
+ readonly email?: string | undefined;
8
+ readonly firstName?: string | undefined;
9
+ readonly lastName?: string | undefined;
10
+ readonly roles?: readonly string[] | undefined;
11
+ } | undefined;
12
+ readonly loggedIn?: boolean | undefined;
13
+ readonly expiresAt?: number | undefined;
14
+ }, {
15
+ readonly user?: {
16
+ readonly [x: string]: Readonly<unknown>;
17
+ readonly sub: string;
18
+ readonly email?: string | undefined;
19
+ readonly firstName?: string | undefined;
20
+ readonly lastName?: string | undefined;
21
+ readonly roles?: readonly string[] | undefined;
22
+ } | undefined;
23
+ readonly loggedIn?: boolean | undefined;
24
+ readonly expiresAt?: number | undefined;
25
+ }>>;
26
+ user: import("vue").ComputedRef<UserClaims | null>;
27
+ loggedIn: import("vue").ComputedRef<boolean>;
28
+ expiresAt: import("vue").ComputedRef<number | undefined>;
29
+ /**
30
+ * Re-read the server session (after login, on tab focus, …). Client-only: called as the second
31
+ * nested `await` inside a login/exchange flow (e.g. `useHuia().login()` → `fetch()` →
32
+ * `useRequestFetch()`), which during SSR is one level deeper than Vue's `withAsyncContext` — set up
33
+ * only for a page's own top-level `await` — keeps the Nuxt app context alive for; `useRequestFetch()`
34
+ * then throws `NUXT_E1001` instead of refreshing anything. Skipping server-side is safe: the session
35
+ * cookie a login/exchange call sets is already on the response by the time `fetch()` runs, so a
36
+ * follow-up navigation (the usual next step) picks up the fresh session on its own next request
37
+ * regardless of whether this reactive `session` ref got updated first.
38
+ */
39
+ fetch(): Promise<void>;
40
+ /** Full logout via the server route (clears storage + cookies). Never redirects. */
41
+ clear(): Promise<void>;
42
+ /** Optimistic local reset only (UI); does not touch the server. */
43
+ clearLocal(): void;
44
+ /** Whether the signed-in user has `role` (requires Huia.Headless's `identity/me` to report it). */
45
+ hasRole(role: string): boolean;
46
+ /** Whether the signed-in user has at least one of `roles`. */
47
+ hasAnyRole(...roles: string[]): boolean;
48
+ };