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.
- package/README.md +110 -0
- package/dist/module.d.mts +67 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +153 -0
- package/dist/runtime/app/composables/useHuia.d.ts +111 -0
- package/dist/runtime/app/composables/useHuia.js +100 -0
- package/dist/runtime/app/composables/useHuiaAdmin.d.ts +24 -0
- package/dist/runtime/app/composables/useHuiaAdmin.js +58 -0
- package/dist/runtime/app/composables/useUserSession.d.ts +48 -0
- package/dist/runtime/app/composables/useUserSession.js +44 -0
- package/dist/runtime/app/middleware/auth.d.ts +3 -0
- package/dist/runtime/app/middleware/auth.js +12 -0
- package/dist/runtime/app/plugins/session.server.d.ts +8 -0
- package/dist/runtime/app/plugins/session.server.js +8 -0
- package/dist/runtime/app/utils/route-match.d.ts +8 -0
- package/dist/runtime/app/utils/route-match.js +9 -0
- package/dist/runtime/server/middleware/session.context.d.ts +8 -0
- package/dist/runtime/server/middleware/session.context.js +6 -0
- package/dist/runtime/server/routes/auth/admin.d.ts +2 -0
- package/dist/runtime/server/routes/auth/admin.js +55 -0
- package/dist/runtime/server/routes/auth/confirm-email.get.d.ts +4 -0
- package/dist/runtime/server/routes/auth/confirm-email.get.js +19 -0
- package/dist/runtime/server/routes/auth/external/[provider].get.d.ts +11 -0
- package/dist/runtime/server/routes/auth/external/[provider].get.js +16 -0
- package/dist/runtime/server/routes/auth/external/complete-profile.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/external/complete-profile.post.js +19 -0
- package/dist/runtime/server/routes/auth/external/exchange.post.d.ts +16 -0
- package/dist/runtime/server/routes/auth/external/exchange.post.js +28 -0
- package/dist/runtime/server/routes/auth/forgot-password.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/forgot-password.post.js +16 -0
- package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/login.post.js +19 -0
- package/dist/runtime/server/routes/auth/logout.post.d.ts +9 -0
- package/dist/runtime/server/routes/auth/logout.post.js +6 -0
- package/dist/runtime/server/routes/auth/phone/complete-profile.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/phone/complete-profile.post.js +19 -0
- package/dist/runtime/server/routes/auth/phone/start.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/phone/start.post.js +15 -0
- package/dist/runtime/server/routes/auth/phone/verify.post.d.ts +10 -0
- package/dist/runtime/server/routes/auth/phone/verify.post.js +22 -0
- package/dist/runtime/server/routes/auth/refresh.post.d.ts +7 -0
- package/dist/runtime/server/routes/auth/refresh.post.js +6 -0
- package/dist/runtime/server/routes/auth/register.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/register.post.js +16 -0
- package/dist/runtime/server/routes/auth/resend-confirmation.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/resend-confirmation.post.js +16 -0
- package/dist/runtime/server/routes/auth/reset-password.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/reset-password.post.js +16 -0
- package/dist/runtime/server/routes/auth/session.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/session.get.js +6 -0
- package/dist/runtime/server/utils/backend.d.ts +54 -0
- package/dist/runtime/server/utils/backend.js +81 -0
- package/dist/runtime/server/utils/config.d.ts +3 -0
- package/dist/runtime/server/utils/config.js +39 -0
- package/dist/runtime/server/utils/cookie.d.ts +9 -0
- package/dist/runtime/server/utils/cookie.js +90 -0
- package/dist/runtime/server/utils/internal-types.d.ts +93 -0
- package/dist/runtime/server/utils/internal-types.js +0 -0
- package/dist/runtime/server/utils/refresh.d.ts +6 -0
- package/dist/runtime/server/utils/refresh.js +100 -0
- package/dist/runtime/server/utils/session.d.ts +10 -0
- package/dist/runtime/server/utils/session.js +98 -0
- package/dist/runtime/server/utils/storage.d.ts +11 -0
- package/dist/runtime/server/utils/storage.js +31 -0
- package/dist/runtime/server/utils/tokens.d.ts +5 -0
- package/dist/runtime/server/utils/tokens.js +5 -0
- package/dist/runtime/types.d.ts +62 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +3 -0
- 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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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
|
+
}
|