nuxt-huia-headless 1.0.0-alpha.10

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 +153 -0
  5. package/dist/runtime/app/composables/useHuia.d.ts +111 -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 +54 -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 +6 -0
  60. package/dist/runtime/server/utils/refresh.js +100 -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 3+** 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 `>=3`, 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": ">=3.0.0"
6
+ },
7
+ "version": "1.0.0-alpha.10",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,153 @@
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: ">=3.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
+ nuxt.options.imports = nuxt.options.imports || {};
114
+ nuxt.options.imports.exclude = [...nuxt.options.imports.exclude || [], /[\\/]huia-auth-core[\\/]/];
115
+ nuxt.hook("nitro:config", (nitroConfig) => {
116
+ nitroConfig.imports = nitroConfig.imports || {};
117
+ nitroConfig.imports.exclude = nitroConfig.imports.exclude || [];
118
+ nitroConfig.imports.exclude.push(/[\\/]huia-auth-core[\\/]/);
119
+ });
120
+ addServerImportsDir(resolve("runtime/server/utils"));
121
+ addImportsDir(resolve("runtime/app/composables"));
122
+ if (opts.hydration === "useState") {
123
+ addPlugin({ src: resolve("runtime/app/plugins/session.server"), mode: "server" });
124
+ }
125
+ addRouteMiddleware({
126
+ name: "auth",
127
+ path: resolve("runtime/app/middleware/auth"),
128
+ global: opts.middleware.global
129
+ });
130
+ addServerHandler({ route: opts.routes.register, method: "post", handler: resolve("runtime/server/routes/auth/register.post") });
131
+ addServerHandler({ route: opts.routes.login, method: "post", handler: resolve("runtime/server/routes/auth/login.post") });
132
+ addServerHandler({ route: opts.routes.logout, method: "post", handler: resolve("runtime/server/routes/auth/logout.post") });
133
+ addServerHandler({ route: opts.routes.refresh, method: "post", handler: resolve("runtime/server/routes/auth/refresh.post") });
134
+ addServerHandler({ route: opts.routes.session, method: "get", handler: resolve("runtime/server/routes/auth/session.get") });
135
+ addServerHandler({ route: opts.routes.confirmEmail, method: "get", handler: resolve("runtime/server/routes/auth/confirm-email.get") });
136
+ addServerHandler({ route: opts.routes.resendConfirmation, method: "post", handler: resolve("runtime/server/routes/auth/resend-confirmation.post") });
137
+ addServerHandler({ route: opts.routes.forgotPassword, method: "post", handler: resolve("runtime/server/routes/auth/forgot-password.post") });
138
+ addServerHandler({ route: opts.routes.resetPassword, method: "post", handler: resolve("runtime/server/routes/auth/reset-password.post") });
139
+ addServerHandler({ route: opts.routes.phoneStart, method: "post", handler: resolve("runtime/server/routes/auth/phone/start.post") });
140
+ addServerHandler({ route: opts.routes.phoneVerify, method: "post", handler: resolve("runtime/server/routes/auth/phone/verify.post") });
141
+ addServerHandler({ route: opts.routes.phoneCompleteProfile, method: "post", handler: resolve("runtime/server/routes/auth/phone/complete-profile.post") });
142
+ addServerHandler({ route: `${opts.routes.externalLogin}/:provider`, method: "get", handler: resolve("runtime/server/routes/auth/external/[provider].get") });
143
+ addServerHandler({ route: opts.routes.externalExchange, method: "post", handler: resolve("runtime/server/routes/auth/external/exchange.post") });
144
+ addServerHandler({ route: opts.routes.externalCompleteProfile, method: "post", handler: resolve("runtime/server/routes/auth/external/complete-profile.post") });
145
+ addServerHandler({ route: `${opts.routes.admin}/**`, handler: resolve("runtime/server/routes/auth/admin") });
146
+ addServerHandler({ middleware: true, handler: resolve("runtime/server/middleware/session.context") });
147
+ nuxt.hook("prepare:types", ({ references }) => {
148
+ references.push({ path: resolve("runtime/types.d.ts") });
149
+ });
150
+ }
151
+ });
152
+
153
+ export { module$1 as default };
@@ -0,0 +1,111 @@
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
+ phoneNumber?: string;
60
+ country?: string;
61
+ }): Promise<{
62
+ ok: boolean;
63
+ error?: unknown;
64
+ }>;
65
+ /** Signs in with email + password (and, when required, a `twoFactorCode`/`twoFactorRecoveryCode`). */
66
+ login(opts: {
67
+ email: string;
68
+ password: string;
69
+ twoFactorCode?: string;
70
+ twoFactorRecoveryCode?: string;
71
+ }): Promise<HuiaLoginResult>;
72
+ /** Local logout — clears the session cookie and the stored tokens. Never redirects. */
73
+ logout: () => Promise<void>;
74
+ /** Sends a one-time SMS code and returns an opaque `flowId` — pass it to `verifyPhoneLogin`. */
75
+ startPhoneLogin(opts: {
76
+ phoneNumber: string;
77
+ country?: string;
78
+ captchaResponse?: string;
79
+ }): Promise<{
80
+ ok: boolean;
81
+ flowId?: string;
82
+ error?: unknown;
83
+ }>;
84
+ /** Verifies a phone one-time code. Signs in directly, or reports a first-time sign-up needs a name. */
85
+ verifyPhoneLogin(opts: {
86
+ flowId: string;
87
+ code: string;
88
+ }): Promise<HuiaFlowResult>;
89
+ /** Completes a first-time phone sign-up with a name, then signs in. */
90
+ completePhoneProfile(opts: {
91
+ flowId: string;
92
+ firstName: string;
93
+ lastName: string;
94
+ }): Promise<HuiaLoginResult>;
95
+ /**
96
+ * The URL to navigate the browser to (a plain link, not a fetch call) to start signing in with
97
+ * `provider` (must match a name configured on the Huia.Headless tenant). `returnTo` is a
98
+ * same-origin path within this app, resolved once the provider round trip completes.
99
+ */
100
+ externalLoginHref(provider: string, returnTo?: string): string;
101
+ /** Exchanges the one-time `code` from the callback URL. Signs in directly, or needs a name for a new sign-up. */
102
+ exchangeExternalCode(code: string): Promise<HuiaFlowResult>;
103
+ /** Completes a first-time external sign-up with a name, then signs in. */
104
+ completeExternalProfile(opts: {
105
+ code: string;
106
+ firstName: string;
107
+ lastName: string;
108
+ phoneNumber?: string;
109
+ country?: string;
110
+ }): Promise<HuiaLoginResult>;
111
+ };
@@ -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
+ }