hazo_auth 10.2.0 → 10.2.2
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 +37 -0
- package/SETUP_CHECKLIST.md +8 -3
- package/cli-src/cli/demo.ts +72 -0
- package/cli-src/cli/index.ts +57 -0
- package/cli-src/lib/services/demo_account_service.ts +227 -0
- package/cli-src/lib/services/google_token_service.ts +25 -4
- package/cli-src/lib/services/index.ts +1 -0
- package/dist/cli/demo.d.ts +12 -0
- package/dist/cli/demo.d.ts.map +1 -0
- package/dist/cli/demo.js +59 -0
- package/dist/cli/index.js +61 -0
- package/dist/lib/services/demo_account_service.d.ts +43 -0
- package/dist/lib/services/demo_account_service.d.ts.map +1 -0
- package/dist/lib/services/demo_account_service.js +171 -0
- package/dist/lib/services/google_token_service.d.ts +18 -0
- package/dist/lib/services/google_token_service.d.ts.map +1 -1
- package/dist/lib/services/google_token_service.js +25 -2
- package/dist/lib/services/index.d.ts +1 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -54,6 +54,16 @@ HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
|
54
54
|
npm install hazo_secure
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
```js
|
|
58
|
+
// 4. Add hazo_secure to serverExternalPackages in next.config.mjs / next.config.js
|
|
59
|
+
// so the bundler leaves the literal import("hazo_secure/crypto") as a native
|
|
60
|
+
// external import. Without this, Google sign-in fails with
|
|
61
|
+
// GoogleTokenStorageUnconfigured even when hazo_secure is installed.
|
|
62
|
+
const nextConfig = {
|
|
63
|
+
serverExternalPackages: ["hazo_secure", /* ...existing entries */],
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
57
67
|
**Client Usage:**
|
|
58
68
|
```tsx
|
|
59
69
|
import { requestGoogleScopes } from "hazo_auth/client";
|
|
@@ -371,6 +381,29 @@ npx hazo_auth validate # Check your setup and configuration
|
|
|
371
381
|
npx hazo_auth --help # Show all commands
|
|
372
382
|
```
|
|
373
383
|
|
|
384
|
+
#### Dev-only demo account seeder
|
|
385
|
+
|
|
386
|
+
Seed a **login-ready** test user (email pre-verified, assigned to the default
|
|
387
|
+
system scope with a role, no verification email sent) for local development.
|
|
388
|
+
This is **physically incapable of running in production**: it refuses unless the
|
|
389
|
+
resolved env (`HAZO_ENV ?? NODE_ENV`) is non-production **and**
|
|
390
|
+
`HAZO_AUTH_ALLOW_DEMO_SEED=true` is set.
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# Create (member by default; --admin grants the global-admin role)
|
|
394
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin --email=demo@example.com
|
|
395
|
+
# or via npm script
|
|
396
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:create -- --admin
|
|
397
|
+
|
|
398
|
+
# Delete (by email, prefix, or all __demo_* users)
|
|
399
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --email=demo@example.com
|
|
400
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:delete -- --all
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Programmatic equivalents are exported from `hazo_auth/server-lib`:
|
|
404
|
+
`create_demo_user`, `delete_demo_users`, `assert_demo_seed_allowed`,
|
|
405
|
+
`DemoSeedNotAllowed`.
|
|
406
|
+
|
|
374
407
|
### Using Zero-Config Page Components (v2.0+)
|
|
375
408
|
|
|
376
409
|
**NEW in v2.0:** All pages are now React Server Components that initialize everything on the server. No configuration, no loading state, no hassle!
|
|
@@ -1340,6 +1373,10 @@ Google OAuth adds one new dependency:
|
|
|
1340
1373
|
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
1341
1374
|
```
|
|
1342
1375
|
3. Install the optional peer: `npm install hazo_secure`
|
|
1376
|
+
4. Add `hazo_secure` to `serverExternalPackages` in `next.config.mjs` / `next.config.js`. The
|
|
1377
|
+
service loads encryption via a literal `import("hazo_secure/crypto")`; if the bundler inlines
|
|
1378
|
+
`hazo_secure` instead of treating it as an external import, Google sign-in fails with
|
|
1379
|
+
`GoogleTokenStorageUnconfigured` even when the peer is installed and keys are set.
|
|
1343
1380
|
|
|
1344
1381
|
**Usage:**
|
|
1345
1382
|
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -13,6 +13,7 @@ const nextConfig = {
|
|
|
13
13
|
serverExternalPackages: [
|
|
14
14
|
"hazo_notify", "argon2",
|
|
15
15
|
"hazo_core", "hazo_config", // required for Next 16 + Turbopack
|
|
16
|
+
"hazo_secure", // required if using Google API token storage (getGoogleToken)
|
|
16
17
|
],
|
|
17
18
|
};
|
|
18
19
|
```
|
|
@@ -1138,13 +1139,17 @@ ls app/api/hazo_auth/set_password/route.ts
|
|
|
1138
1139
|
Only needed if you use `requestGoogleScopes` / `getGoogleToken` for Google API access beyond sign-in:
|
|
1139
1140
|
|
|
1140
1141
|
1. Install optional peer: `npm install hazo_secure`
|
|
1141
|
-
2.
|
|
1142
|
-
|
|
1142
|
+
2. Add `hazo_secure` to `serverExternalPackages` in `next.config.mjs` / `next.config.js`. The
|
|
1143
|
+
service loads encryption via a literal `import("hazo_secure/crypto")`; if the bundler inlines
|
|
1144
|
+
`hazo_secure` rather than leaving it as a native external import, Google sign-in fails with
|
|
1145
|
+
`GoogleTokenStorageUnconfigured` even when the peer is installed and keys are configured.
|
|
1146
|
+
3. Run migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
|
|
1147
|
+
4. Set env vars:
|
|
1143
1148
|
```env
|
|
1144
1149
|
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
1145
1150
|
HAZO_AUTH_OAUTH_KEY_V1=<base64-32-bytes from: openssl rand -base64 32>
|
|
1146
1151
|
```
|
|
1147
|
-
|
|
1152
|
+
5. Wire the new routes in your Next.js app (same pattern as other hazo_auth routes):
|
|
1148
1153
|
```ts
|
|
1149
1154
|
// app/api/hazo_auth/google/token/route.ts
|
|
1150
1155
|
export { GET as googleTokenGET, DELETE as googleTokenDELETE } from "hazo_auth/server/routes";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// file_description: CLI command handlers for demo account seeding (dev-only)
|
|
2
|
+
|
|
3
|
+
// section: imports
|
|
4
|
+
import { get_hazo_connect_instance } from "../lib/hazo_connect_instance.server.js";
|
|
5
|
+
import {
|
|
6
|
+
create_demo_user,
|
|
7
|
+
delete_demo_users,
|
|
8
|
+
DemoSeedNotAllowed,
|
|
9
|
+
type CreateDemoUserOptions,
|
|
10
|
+
type DeleteDemoUsersOptions,
|
|
11
|
+
} from "../lib/services/demo_account_service.js";
|
|
12
|
+
|
|
13
|
+
// section: create_handler
|
|
14
|
+
/**
|
|
15
|
+
* Creates a login-ready demo user and prints a summary.
|
|
16
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
17
|
+
*/
|
|
18
|
+
export async function handle_demo_create(opts: CreateDemoUserOptions): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const adapter = get_hazo_connect_instance();
|
|
21
|
+
const result = await create_demo_user(adapter, opts);
|
|
22
|
+
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log("=".repeat(60));
|
|
25
|
+
console.log("DEMO USER CREATED");
|
|
26
|
+
console.log("=".repeat(60));
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(` Email: ${result.email}`);
|
|
29
|
+
console.log(` Password: ${result.password}`);
|
|
30
|
+
console.log(` User ID: ${result.user_id}`);
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log(" ✓ Email pre-verified — account is login-ready.");
|
|
33
|
+
console.log(" ✓ Assigned to the default system scope.");
|
|
34
|
+
if (opts.admin) {
|
|
35
|
+
console.log(" ✓ Admin account — global-admin role assigned.");
|
|
36
|
+
}
|
|
37
|
+
console.log("=".repeat(60));
|
|
38
|
+
console.log("");
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof DemoSeedNotAllowed) {
|
|
41
|
+
console.error(
|
|
42
|
+
"Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.",
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// section: delete_handler
|
|
52
|
+
/**
|
|
53
|
+
* Deletes demo users matching the given email or prefix(es) and prints a summary.
|
|
54
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
55
|
+
*/
|
|
56
|
+
export async function handle_demo_delete(opts: DeleteDemoUsersOptions): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const adapter = get_hazo_connect_instance();
|
|
59
|
+
const result = await delete_demo_users(adapter, opts);
|
|
60
|
+
|
|
61
|
+
console.log(`✓ Deleted ${result.deleted} demo user(s).`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof DemoSeedNotAllowed) {
|
|
64
|
+
console.error(
|
|
65
|
+
"Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.",
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/cli-src/cli/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { handle_init } from "./init.js";
|
|
|
9
9
|
import { handle_init_users, show_init_users_help } from "./init_users.js";
|
|
10
10
|
import { handle_init_db } from "./init_db.js";
|
|
11
11
|
import { handle_init_permissions, show_init_permissions_help } from "./init_permissions.js";
|
|
12
|
+
import { handle_demo_create, handle_demo_delete } from "./demo.js";
|
|
12
13
|
|
|
13
14
|
// section: constants
|
|
14
15
|
const VERSION = "1.6.0";
|
|
@@ -23,6 +24,8 @@ Commands:
|
|
|
23
24
|
init-db Create SQLite database with hazo_auth schema (standalone)
|
|
24
25
|
init-permissions Create default permissions from config (no user required)
|
|
25
26
|
init-users Initialize permissions, roles, and super user from config
|
|
27
|
+
demo-create [DEV ONLY] Create a login-ready demo user (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
|
|
28
|
+
demo-delete [DEV ONLY] Delete demo users by email/prefix (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
|
|
26
29
|
validate Check your hazo_auth setup and configuration
|
|
27
30
|
generate-routes Generate API route files and pages in your project
|
|
28
31
|
schema Print the canonical SQLite schema SQL
|
|
@@ -41,6 +44,8 @@ Examples:
|
|
|
41
44
|
npx hazo_auth generate-routes --pages
|
|
42
45
|
npx hazo_auth generate-routes --all --dir=src/app
|
|
43
46
|
npx hazo_auth schema
|
|
47
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin
|
|
48
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --all
|
|
44
49
|
|
|
45
50
|
Documentation:
|
|
46
51
|
https://github.com/your-repo/hazo_auth/blob/main/SETUP_CHECKLIST.md
|
|
@@ -216,6 +221,58 @@ Requires: better-sqlite3 (npm install better-sqlite3)
|
|
|
216
221
|
break;
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
case "demo-create": {
|
|
225
|
+
if (help) {
|
|
226
|
+
console.log(`
|
|
227
|
+
hazo_auth demo-create [DEV ONLY]
|
|
228
|
+
|
|
229
|
+
Create a login-ready demo user (email pre-verified, assigned to the default system scope).
|
|
230
|
+
Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
|
|
231
|
+
|
|
232
|
+
Options:
|
|
233
|
+
--email=<addr> Email for the demo user (default: __demo_<timestamp>@hazo.test)
|
|
234
|
+
--password=<pw> Password (default: Demo_Pass123!)
|
|
235
|
+
--name=<name> Display name (default: Demo User)
|
|
236
|
+
--admin Grant the global-admin role instead of a plain member role
|
|
237
|
+
`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const demoCreateOpts: { email?: string; password?: string; name?: string; admin?: boolean } = {};
|
|
241
|
+
for (const arg of args) {
|
|
242
|
+
if (arg.startsWith("--email=")) demoCreateOpts.email = arg.replace("--email=", "");
|
|
243
|
+
else if (arg.startsWith("--password=")) demoCreateOpts.password = arg.replace("--password=", "");
|
|
244
|
+
else if (arg.startsWith("--name=")) demoCreateOpts.name = arg.replace("--name=", "");
|
|
245
|
+
else if (arg === "--admin") demoCreateOpts.admin = true;
|
|
246
|
+
}
|
|
247
|
+
await handle_demo_create(demoCreateOpts);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "demo-delete": {
|
|
252
|
+
if (help) {
|
|
253
|
+
console.log(`
|
|
254
|
+
hazo_auth demo-delete [DEV ONLY]
|
|
255
|
+
|
|
256
|
+
Delete demo users (and their scope rows) by email or prefix. Defaults to the __demo_ prefix.
|
|
257
|
+
Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
|
|
258
|
+
|
|
259
|
+
Options:
|
|
260
|
+
--email=<addr> Delete only the user with this exact email
|
|
261
|
+
--prefix=<p> Delete users whose email starts with this prefix (default: __demo_)
|
|
262
|
+
--all Delete all __demo_-prefixed users (the default behaviour)
|
|
263
|
+
`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const demoDeleteOpts: { email?: string; prefixes?: string[] } = {};
|
|
267
|
+
for (const arg of args) {
|
|
268
|
+
if (arg.startsWith("--email=")) demoDeleteOpts.email = arg.replace("--email=", "");
|
|
269
|
+
else if (arg.startsWith("--prefix=")) demoDeleteOpts.prefixes = [arg.replace("--prefix=", "")];
|
|
270
|
+
}
|
|
271
|
+
// --all is a no-op / explicit default (prefixes stays undefined → service defaults to ['__demo_'])
|
|
272
|
+
await handle_demo_delete(demoDeleteOpts);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
219
276
|
case "validate":
|
|
220
277
|
if (help) {
|
|
221
278
|
console.log(`
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// file_description: dev-only service for seeding demo accounts in hazo_auth; physically blocked in production
|
|
2
|
+
|
|
3
|
+
// section: imports
|
|
4
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
5
|
+
import { createCrudService } from "hazo_connect/server";
|
|
6
|
+
import argon2 from "argon2";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import { DEFAULT_SYSTEM_SCOPE_ID } from "./scope_service.js";
|
|
9
|
+
import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
|
|
10
|
+
|
|
11
|
+
// section: errors
|
|
12
|
+
|
|
13
|
+
export class DemoSeedNotAllowed extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "DemoSeedNotAllowed";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// section: guards
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Throws DemoSeedNotAllowed if the current environment is production
|
|
24
|
+
* or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
|
|
25
|
+
* This must be called at the top of every exported function in this module.
|
|
26
|
+
*/
|
|
27
|
+
export function assert_demo_seed_allowed(): void {
|
|
28
|
+
const env = process.env.HAZO_ENV ?? process.env.NODE_ENV ?? "development";
|
|
29
|
+
if (env === "production") {
|
|
30
|
+
throw new DemoSeedNotAllowed(
|
|
31
|
+
"Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (process.env.HAZO_AUTH_ALLOW_DEMO_SEED !== "true") {
|
|
35
|
+
throw new DemoSeedNotAllowed(
|
|
36
|
+
"Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// section: types
|
|
42
|
+
|
|
43
|
+
export type CreateDemoUserOptions = {
|
|
44
|
+
email?: string; // default: `__demo_${Date.now()}@hazo.test`
|
|
45
|
+
password?: string; // default: 'Demo_Pass123!'
|
|
46
|
+
name?: string; // default: 'Demo User'
|
|
47
|
+
admin?: boolean; // default: false
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CreateDemoUserResult = {
|
|
51
|
+
user_id: string;
|
|
52
|
+
email: string;
|
|
53
|
+
password: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type DeleteDemoUsersOptions = {
|
|
57
|
+
email?: string;
|
|
58
|
+
prefixes?: string[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// section: helpers
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a demo user in the database, assigning them to the default system scope.
|
|
65
|
+
* Idempotent on email — if the user already exists their id is reused and the
|
|
66
|
+
* scope/role assignment is still ensured.
|
|
67
|
+
*
|
|
68
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
69
|
+
*/
|
|
70
|
+
export async function create_demo_user(
|
|
71
|
+
adapter: HazoConnectAdapter,
|
|
72
|
+
opts: CreateDemoUserOptions = {},
|
|
73
|
+
): Promise<CreateDemoUserResult> {
|
|
74
|
+
assert_demo_seed_allowed();
|
|
75
|
+
|
|
76
|
+
const email = opts.email ?? `__demo_${Date.now()}@hazo.test`;
|
|
77
|
+
const password = opts.password ?? "Demo_Pass123!";
|
|
78
|
+
const name = opts.name ?? "Demo User";
|
|
79
|
+
const admin = opts.admin ?? false;
|
|
80
|
+
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
|
|
83
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
84
|
+
const roles_service = createCrudService(adapter, "hazo_roles");
|
|
85
|
+
const permissions_service = createCrudService(adapter, "hazo_permissions");
|
|
86
|
+
const role_permissions_service = createCrudService(
|
|
87
|
+
adapter,
|
|
88
|
+
"hazo_role_permissions",
|
|
89
|
+
{ primaryKeys: ["role_id", "permission_id"], autoId: false },
|
|
90
|
+
);
|
|
91
|
+
const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
|
|
92
|
+
primaryKeys: ["user_id", "scope_id"],
|
|
93
|
+
autoId: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Idempotent: reuse existing user if email already registered
|
|
97
|
+
let user_id: string;
|
|
98
|
+
const existing_users = await users_service.findBy({ email_address: email });
|
|
99
|
+
if (Array.isArray(existing_users) && existing_users.length > 0) {
|
|
100
|
+
user_id = existing_users[0].id as string;
|
|
101
|
+
} else {
|
|
102
|
+
const password_hash = await argon2.hash(password);
|
|
103
|
+
user_id = randomUUID();
|
|
104
|
+
|
|
105
|
+
await users_service.insert({
|
|
106
|
+
id: user_id,
|
|
107
|
+
email_address: email,
|
|
108
|
+
password_hash,
|
|
109
|
+
email_verified: true,
|
|
110
|
+
status: "ACTIVE",
|
|
111
|
+
login_attempts: 0,
|
|
112
|
+
auth_providers: "email",
|
|
113
|
+
name,
|
|
114
|
+
created_at: now,
|
|
115
|
+
changed_at: now,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Ensure role exists
|
|
120
|
+
const role_name = admin ? "default_super_user_role" : "demo_member_role";
|
|
121
|
+
const existing_roles = await roles_service.findBy({ role_name });
|
|
122
|
+
let role_id: string;
|
|
123
|
+
if (Array.isArray(existing_roles) && existing_roles.length > 0) {
|
|
124
|
+
role_id = existing_roles[0].id as string;
|
|
125
|
+
} else {
|
|
126
|
+
const new_role = await roles_service.insert({ role_name, created_at: now, changed_at: now });
|
|
127
|
+
role_id = Array.isArray(new_role)
|
|
128
|
+
? (new_role[0] as { id: string }).id
|
|
129
|
+
: (new_role as { id: string }).id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Admin users: ensure GLOBAL_ADMIN_PERMISSION is in the catalog and linked to the role
|
|
133
|
+
if (admin) {
|
|
134
|
+
// Ensure permission exists
|
|
135
|
+
const existing_perms = await permissions_service.findBy({
|
|
136
|
+
permission_name: GLOBAL_ADMIN_PERMISSION,
|
|
137
|
+
});
|
|
138
|
+
let permission_id: string;
|
|
139
|
+
if (Array.isArray(existing_perms) && existing_perms.length > 0) {
|
|
140
|
+
permission_id = existing_perms[0].id as string;
|
|
141
|
+
} else {
|
|
142
|
+
const new_perm = await permissions_service.insert({
|
|
143
|
+
permission_name: GLOBAL_ADMIN_PERMISSION,
|
|
144
|
+
description: "Global admin — access to all scopes and operations",
|
|
145
|
+
created_at: now,
|
|
146
|
+
changed_at: now,
|
|
147
|
+
});
|
|
148
|
+
permission_id = Array.isArray(new_perm)
|
|
149
|
+
? (new_perm[0] as { id: string }).id
|
|
150
|
+
: (new_perm as { id: string }).id;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Ensure role→permission link
|
|
154
|
+
const existing_rp = await role_permissions_service.findBy({ role_id, permission_id });
|
|
155
|
+
if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
|
|
156
|
+
await role_permissions_service.insert({ role_id, permission_id });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Ensure user→scope assignment
|
|
161
|
+
const existing_user_scopes = await user_scopes_service.findBy({
|
|
162
|
+
user_id,
|
|
163
|
+
scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
164
|
+
});
|
|
165
|
+
if (!Array.isArray(existing_user_scopes) || existing_user_scopes.length === 0) {
|
|
166
|
+
await user_scopes_service.insert({
|
|
167
|
+
user_id,
|
|
168
|
+
scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
169
|
+
root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
170
|
+
role_id,
|
|
171
|
+
created_at: now,
|
|
172
|
+
changed_at: now,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { user_id, email, password };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Deletes demo users (and their scope rows) whose email matches the given
|
|
181
|
+
* address or any of the given prefixes. Defaults to the "__demo_" prefix.
|
|
182
|
+
*
|
|
183
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
184
|
+
*/
|
|
185
|
+
export async function delete_demo_users(
|
|
186
|
+
adapter: HazoConnectAdapter,
|
|
187
|
+
opts: DeleteDemoUsersOptions = {},
|
|
188
|
+
): Promise<{ deleted: number }> {
|
|
189
|
+
assert_demo_seed_allowed();
|
|
190
|
+
|
|
191
|
+
const prefixes = opts.prefixes ?? ["__demo_"];
|
|
192
|
+
|
|
193
|
+
const users = createCrudService<{ id: string; email_address: string }>(
|
|
194
|
+
adapter,
|
|
195
|
+
"hazo_users",
|
|
196
|
+
);
|
|
197
|
+
const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
|
|
198
|
+
primaryKeys: ["user_id", "scope_id"],
|
|
199
|
+
autoId: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const all = await users.list((qb) => qb.limit(500));
|
|
203
|
+
const targets = all.filter((u) => {
|
|
204
|
+
if (opts.email && u.email_address === opts.email) return true;
|
|
205
|
+
return prefixes.some(
|
|
206
|
+
(p) => typeof u.email_address === "string" && u.email_address.startsWith(p),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let deleted = 0;
|
|
211
|
+
for (const u of targets) {
|
|
212
|
+
const uid = u.id;
|
|
213
|
+
try {
|
|
214
|
+
// Best-effort: delete this user's scope rows. hazo_user_scopes has a
|
|
215
|
+
// composite PK (no `id` column); deleteById on a multi-key table falls
|
|
216
|
+
// back to the first primary key (user_id), so this removes all of the
|
|
217
|
+
// user's scope rows in one call.
|
|
218
|
+
await user_scopes_service.deleteById(uid).catch(() => {});
|
|
219
|
+
await users.deleteById(uid);
|
|
220
|
+
deleted += 1;
|
|
221
|
+
} catch {
|
|
222
|
+
// best-effort — skip users that can't be deleted
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { deleted };
|
|
227
|
+
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { createCrudService } from "hazo_connect/server";
|
|
4
4
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
5
5
|
import { create_app_logger } from "../app_logger.js";
|
|
6
|
-
import { optional_import } from "hazo_core";
|
|
7
6
|
import { randomUUID } from "crypto";
|
|
8
7
|
|
|
9
8
|
// section: errors
|
|
@@ -70,10 +69,32 @@ function makeKeyProvider(
|
|
|
70
69
|
});
|
|
71
70
|
}
|
|
72
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Indirection around the hazo_secure/crypto dynamic import.
|
|
74
|
+
*
|
|
75
|
+
* The import specifier MUST be a string literal. hazo_secure is an optional
|
|
76
|
+
* peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
|
|
77
|
+
* that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
|
|
78
|
+
* statically resolve when consumers bundle their server. The import then fails
|
|
79
|
+
* unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
|
|
80
|
+
* hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
|
|
81
|
+
* a native external import and resolves at runtime (consumers must list
|
|
82
|
+
* hazo_secure in serverExternalPackages so it isn't bundled).
|
|
83
|
+
*
|
|
84
|
+
* Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
|
|
85
|
+
* the literal specifier. Not part of the public API (underscore-prefixed).
|
|
86
|
+
*/
|
|
87
|
+
export const _cryptoLoader = {
|
|
88
|
+
load: (): Promise<typeof import("hazo_secure/crypto")> => import("hazo_secure/crypto"),
|
|
89
|
+
};
|
|
90
|
+
|
|
73
91
|
async function load_crypto_module() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
let cryptoModule: typeof import("hazo_secure/crypto") | null;
|
|
93
|
+
try {
|
|
94
|
+
cryptoModule = await _cryptoLoader.load();
|
|
95
|
+
} catch {
|
|
96
|
+
cryptoModule = null;
|
|
97
|
+
}
|
|
77
98
|
if (!cryptoModule) throw new GoogleTokenStorageUnconfigured();
|
|
78
99
|
return cryptoModule;
|
|
79
100
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CreateDemoUserOptions, type DeleteDemoUsersOptions } from "../lib/services/demo_account_service.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a login-ready demo user and prints a summary.
|
|
4
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handle_demo_create(opts: CreateDemoUserOptions): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Deletes demo users matching the given email or prefix(es) and prints a summary.
|
|
9
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function handle_demo_delete(opts: DeleteDemoUsersOptions): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=demo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/cli/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AAGjD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BnF;AAGD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
|
package/dist/cli/demo.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// file_description: CLI command handlers for demo account seeding (dev-only)
|
|
2
|
+
// section: imports
|
|
3
|
+
import { get_hazo_connect_instance } from "../lib/hazo_connect_instance.server.js";
|
|
4
|
+
import { create_demo_user, delete_demo_users, DemoSeedNotAllowed, } from "../lib/services/demo_account_service.js";
|
|
5
|
+
// section: create_handler
|
|
6
|
+
/**
|
|
7
|
+
* Creates a login-ready demo user and prints a summary.
|
|
8
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
9
|
+
*/
|
|
10
|
+
export async function handle_demo_create(opts) {
|
|
11
|
+
try {
|
|
12
|
+
const adapter = get_hazo_connect_instance();
|
|
13
|
+
const result = await create_demo_user(adapter, opts);
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("=".repeat(60));
|
|
16
|
+
console.log("DEMO USER CREATED");
|
|
17
|
+
console.log("=".repeat(60));
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log(` Email: ${result.email}`);
|
|
20
|
+
console.log(` Password: ${result.password}`);
|
|
21
|
+
console.log(` User ID: ${result.user_id}`);
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log(" ✓ Email pre-verified — account is login-ready.");
|
|
24
|
+
console.log(" ✓ Assigned to the default system scope.");
|
|
25
|
+
if (opts.admin) {
|
|
26
|
+
console.log(" ✓ Admin account — global-admin role assigned.");
|
|
27
|
+
}
|
|
28
|
+
console.log("=".repeat(60));
|
|
29
|
+
console.log("");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof DemoSeedNotAllowed) {
|
|
33
|
+
console.error("Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// section: delete_handler
|
|
41
|
+
/**
|
|
42
|
+
* Deletes demo users matching the given email or prefix(es) and prints a summary.
|
|
43
|
+
* Exits with code 1 if demo seeding is not allowed or an error occurs.
|
|
44
|
+
*/
|
|
45
|
+
export async function handle_demo_delete(opts) {
|
|
46
|
+
try {
|
|
47
|
+
const adapter = get_hazo_connect_instance();
|
|
48
|
+
const result = await delete_demo_users(adapter, opts);
|
|
49
|
+
console.log(`✓ Deleted ${result.deleted} demo user(s).`);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof DemoSeedNotAllowed) {
|
|
53
|
+
console.error("Demo seeding is disabled. Set HAZO_AUTH_ALLOW_DEMO_SEED=true in a non-production env.");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { handle_init } from "./init.js";
|
|
|
8
8
|
import { handle_init_users, show_init_users_help } from "./init_users.js";
|
|
9
9
|
import { handle_init_db } from "./init_db.js";
|
|
10
10
|
import { handle_init_permissions, show_init_permissions_help } from "./init_permissions.js";
|
|
11
|
+
import { handle_demo_create, handle_demo_delete } from "./demo.js";
|
|
11
12
|
// section: constants
|
|
12
13
|
const VERSION = "1.6.0";
|
|
13
14
|
const HELP_TEXT = `
|
|
@@ -20,6 +21,8 @@ Commands:
|
|
|
20
21
|
init-db Create SQLite database with hazo_auth schema (standalone)
|
|
21
22
|
init-permissions Create default permissions from config (no user required)
|
|
22
23
|
init-users Initialize permissions, roles, and super user from config
|
|
24
|
+
demo-create [DEV ONLY] Create a login-ready demo user (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
|
|
25
|
+
demo-delete [DEV ONLY] Delete demo users by email/prefix (needs HAZO_AUTH_ALLOW_DEMO_SEED=true)
|
|
23
26
|
validate Check your hazo_auth setup and configuration
|
|
24
27
|
generate-routes Generate API route files and pages in your project
|
|
25
28
|
schema Print the canonical SQLite schema SQL
|
|
@@ -38,6 +41,8 @@ Examples:
|
|
|
38
41
|
npx hazo_auth generate-routes --pages
|
|
39
42
|
npx hazo_auth generate-routes --all --dir=src/app
|
|
40
43
|
npx hazo_auth schema
|
|
44
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin
|
|
45
|
+
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete --all
|
|
41
46
|
|
|
42
47
|
Documentation:
|
|
43
48
|
https://github.com/your-repo/hazo_auth/blob/main/SETUP_CHECKLIST.md
|
|
@@ -200,6 +205,62 @@ Requires: better-sqlite3 (npm install better-sqlite3)
|
|
|
200
205
|
await handle_init_users({ email });
|
|
201
206
|
break;
|
|
202
207
|
}
|
|
208
|
+
case "demo-create": {
|
|
209
|
+
if (help) {
|
|
210
|
+
console.log(`
|
|
211
|
+
hazo_auth demo-create [DEV ONLY]
|
|
212
|
+
|
|
213
|
+
Create a login-ready demo user (email pre-verified, assigned to the default system scope).
|
|
214
|
+
Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
|
|
215
|
+
|
|
216
|
+
Options:
|
|
217
|
+
--email=<addr> Email for the demo user (default: __demo_<timestamp>@hazo.test)
|
|
218
|
+
--password=<pw> Password (default: Demo_Pass123!)
|
|
219
|
+
--name=<name> Display name (default: Demo User)
|
|
220
|
+
--admin Grant the global-admin role instead of a plain member role
|
|
221
|
+
`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const demoCreateOpts = {};
|
|
225
|
+
for (const arg of args) {
|
|
226
|
+
if (arg.startsWith("--email="))
|
|
227
|
+
demoCreateOpts.email = arg.replace("--email=", "");
|
|
228
|
+
else if (arg.startsWith("--password="))
|
|
229
|
+
demoCreateOpts.password = arg.replace("--password=", "");
|
|
230
|
+
else if (arg.startsWith("--name="))
|
|
231
|
+
demoCreateOpts.name = arg.replace("--name=", "");
|
|
232
|
+
else if (arg === "--admin")
|
|
233
|
+
demoCreateOpts.admin = true;
|
|
234
|
+
}
|
|
235
|
+
await handle_demo_create(demoCreateOpts);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "demo-delete": {
|
|
239
|
+
if (help) {
|
|
240
|
+
console.log(`
|
|
241
|
+
hazo_auth demo-delete [DEV ONLY]
|
|
242
|
+
|
|
243
|
+
Delete demo users (and their scope rows) by email or prefix. Defaults to the __demo_ prefix.
|
|
244
|
+
Refuses to run unless the env is non-production AND HAZO_AUTH_ALLOW_DEMO_SEED=true.
|
|
245
|
+
|
|
246
|
+
Options:
|
|
247
|
+
--email=<addr> Delete only the user with this exact email
|
|
248
|
+
--prefix=<p> Delete users whose email starts with this prefix (default: __demo_)
|
|
249
|
+
--all Delete all __demo_-prefixed users (the default behaviour)
|
|
250
|
+
`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const demoDeleteOpts = {};
|
|
254
|
+
for (const arg of args) {
|
|
255
|
+
if (arg.startsWith("--email="))
|
|
256
|
+
demoDeleteOpts.email = arg.replace("--email=", "");
|
|
257
|
+
else if (arg.startsWith("--prefix="))
|
|
258
|
+
demoDeleteOpts.prefixes = [arg.replace("--prefix=", "")];
|
|
259
|
+
}
|
|
260
|
+
// --all is a no-op / explicit default (prefixes stays undefined → service defaults to ['__demo_'])
|
|
261
|
+
await handle_demo_delete(demoDeleteOpts);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
203
264
|
case "validate":
|
|
204
265
|
if (help) {
|
|
205
266
|
console.log(`
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
2
|
+
export declare class DemoSeedNotAllowed extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Throws DemoSeedNotAllowed if the current environment is production
|
|
7
|
+
* or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
|
|
8
|
+
* This must be called at the top of every exported function in this module.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assert_demo_seed_allowed(): void;
|
|
11
|
+
export type CreateDemoUserOptions = {
|
|
12
|
+
email?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
admin?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type CreateDemoUserResult = {
|
|
18
|
+
user_id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
password: string;
|
|
21
|
+
};
|
|
22
|
+
export type DeleteDemoUsersOptions = {
|
|
23
|
+
email?: string;
|
|
24
|
+
prefixes?: string[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates a demo user in the database, assigning them to the default system scope.
|
|
28
|
+
* Idempotent on email — if the user already exists their id is reused and the
|
|
29
|
+
* scope/role assignment is still ensured.
|
|
30
|
+
*
|
|
31
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
32
|
+
*/
|
|
33
|
+
export declare function create_demo_user(adapter: HazoConnectAdapter, opts?: CreateDemoUserOptions): Promise<CreateDemoUserResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Deletes demo users (and their scope rows) whose email matches the given
|
|
36
|
+
* address or any of the given prefixes. Defaults to the "__demo_" prefix.
|
|
37
|
+
*
|
|
38
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
39
|
+
*/
|
|
40
|
+
export declare function delete_demo_users(adapter: HazoConnectAdapter, opts?: DeleteDemoUsersOptions): Promise<{
|
|
41
|
+
deleted: number;
|
|
42
|
+
}>;
|
|
43
|
+
//# sourceMappingURL=demo_account_service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo_account_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/demo_account_service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAY/C;AAID,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAIF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC,CAwG/B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuC9B"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// file_description: dev-only service for seeding demo accounts in hazo_auth; physically blocked in production
|
|
2
|
+
import { createCrudService } from "hazo_connect/server";
|
|
3
|
+
import argon2 from "argon2";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { DEFAULT_SYSTEM_SCOPE_ID } from "./scope_service.js";
|
|
6
|
+
import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
|
|
7
|
+
// section: errors
|
|
8
|
+
export class DemoSeedNotAllowed extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "DemoSeedNotAllowed";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// section: guards
|
|
15
|
+
/**
|
|
16
|
+
* Throws DemoSeedNotAllowed if the current environment is production
|
|
17
|
+
* or if HAZO_AUTH_ALLOW_DEMO_SEED is not set to "true".
|
|
18
|
+
* This must be called at the top of every exported function in this module.
|
|
19
|
+
*/
|
|
20
|
+
export function assert_demo_seed_allowed() {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
const env = (_b = (_a = process.env.HAZO_ENV) !== null && _a !== void 0 ? _a : process.env.NODE_ENV) !== null && _b !== void 0 ? _b : "development";
|
|
23
|
+
if (env === "production") {
|
|
24
|
+
throw new DemoSeedNotAllowed("Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.");
|
|
25
|
+
}
|
|
26
|
+
if (process.env.HAZO_AUTH_ALLOW_DEMO_SEED !== "true") {
|
|
27
|
+
throw new DemoSeedNotAllowed("Demo seeding is disabled. Requires a non-production env AND HAZO_AUTH_ALLOW_DEMO_SEED=true.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// section: helpers
|
|
31
|
+
/**
|
|
32
|
+
* Creates a demo user in the database, assigning them to the default system scope.
|
|
33
|
+
* Idempotent on email — if the user already exists their id is reused and the
|
|
34
|
+
* scope/role assignment is still ensured.
|
|
35
|
+
*
|
|
36
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
37
|
+
*/
|
|
38
|
+
export async function create_demo_user(adapter, opts = {}) {
|
|
39
|
+
var _a, _b, _c, _d;
|
|
40
|
+
assert_demo_seed_allowed();
|
|
41
|
+
const email = (_a = opts.email) !== null && _a !== void 0 ? _a : `__demo_${Date.now()}@hazo.test`;
|
|
42
|
+
const password = (_b = opts.password) !== null && _b !== void 0 ? _b : "Demo_Pass123!";
|
|
43
|
+
const name = (_c = opts.name) !== null && _c !== void 0 ? _c : "Demo User";
|
|
44
|
+
const admin = (_d = opts.admin) !== null && _d !== void 0 ? _d : false;
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
47
|
+
const roles_service = createCrudService(adapter, "hazo_roles");
|
|
48
|
+
const permissions_service = createCrudService(adapter, "hazo_permissions");
|
|
49
|
+
const role_permissions_service = createCrudService(adapter, "hazo_role_permissions", { primaryKeys: ["role_id", "permission_id"], autoId: false });
|
|
50
|
+
const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
|
|
51
|
+
primaryKeys: ["user_id", "scope_id"],
|
|
52
|
+
autoId: false,
|
|
53
|
+
});
|
|
54
|
+
// Idempotent: reuse existing user if email already registered
|
|
55
|
+
let user_id;
|
|
56
|
+
const existing_users = await users_service.findBy({ email_address: email });
|
|
57
|
+
if (Array.isArray(existing_users) && existing_users.length > 0) {
|
|
58
|
+
user_id = existing_users[0].id;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const password_hash = await argon2.hash(password);
|
|
62
|
+
user_id = randomUUID();
|
|
63
|
+
await users_service.insert({
|
|
64
|
+
id: user_id,
|
|
65
|
+
email_address: email,
|
|
66
|
+
password_hash,
|
|
67
|
+
email_verified: true,
|
|
68
|
+
status: "ACTIVE",
|
|
69
|
+
login_attempts: 0,
|
|
70
|
+
auth_providers: "email",
|
|
71
|
+
name,
|
|
72
|
+
created_at: now,
|
|
73
|
+
changed_at: now,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Ensure role exists
|
|
77
|
+
const role_name = admin ? "default_super_user_role" : "demo_member_role";
|
|
78
|
+
const existing_roles = await roles_service.findBy({ role_name });
|
|
79
|
+
let role_id;
|
|
80
|
+
if (Array.isArray(existing_roles) && existing_roles.length > 0) {
|
|
81
|
+
role_id = existing_roles[0].id;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const new_role = await roles_service.insert({ role_name, created_at: now, changed_at: now });
|
|
85
|
+
role_id = Array.isArray(new_role)
|
|
86
|
+
? new_role[0].id
|
|
87
|
+
: new_role.id;
|
|
88
|
+
}
|
|
89
|
+
// Admin users: ensure GLOBAL_ADMIN_PERMISSION is in the catalog and linked to the role
|
|
90
|
+
if (admin) {
|
|
91
|
+
// Ensure permission exists
|
|
92
|
+
const existing_perms = await permissions_service.findBy({
|
|
93
|
+
permission_name: GLOBAL_ADMIN_PERMISSION,
|
|
94
|
+
});
|
|
95
|
+
let permission_id;
|
|
96
|
+
if (Array.isArray(existing_perms) && existing_perms.length > 0) {
|
|
97
|
+
permission_id = existing_perms[0].id;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const new_perm = await permissions_service.insert({
|
|
101
|
+
permission_name: GLOBAL_ADMIN_PERMISSION,
|
|
102
|
+
description: "Global admin — access to all scopes and operations",
|
|
103
|
+
created_at: now,
|
|
104
|
+
changed_at: now,
|
|
105
|
+
});
|
|
106
|
+
permission_id = Array.isArray(new_perm)
|
|
107
|
+
? new_perm[0].id
|
|
108
|
+
: new_perm.id;
|
|
109
|
+
}
|
|
110
|
+
// Ensure role→permission link
|
|
111
|
+
const existing_rp = await role_permissions_service.findBy({ role_id, permission_id });
|
|
112
|
+
if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
|
|
113
|
+
await role_permissions_service.insert({ role_id, permission_id });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Ensure user→scope assignment
|
|
117
|
+
const existing_user_scopes = await user_scopes_service.findBy({
|
|
118
|
+
user_id,
|
|
119
|
+
scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
120
|
+
});
|
|
121
|
+
if (!Array.isArray(existing_user_scopes) || existing_user_scopes.length === 0) {
|
|
122
|
+
await user_scopes_service.insert({
|
|
123
|
+
user_id,
|
|
124
|
+
scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
125
|
+
root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
|
|
126
|
+
role_id,
|
|
127
|
+
created_at: now,
|
|
128
|
+
changed_at: now,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { user_id, email, password };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Deletes demo users (and their scope rows) whose email matches the given
|
|
135
|
+
* address or any of the given prefixes. Defaults to the "__demo_" prefix.
|
|
136
|
+
*
|
|
137
|
+
* REQUIRES: non-production env + HAZO_AUTH_ALLOW_DEMO_SEED=true
|
|
138
|
+
*/
|
|
139
|
+
export async function delete_demo_users(adapter, opts = {}) {
|
|
140
|
+
var _a;
|
|
141
|
+
assert_demo_seed_allowed();
|
|
142
|
+
const prefixes = (_a = opts.prefixes) !== null && _a !== void 0 ? _a : ["__demo_"];
|
|
143
|
+
const users = createCrudService(adapter, "hazo_users");
|
|
144
|
+
const user_scopes_service = createCrudService(adapter, "hazo_user_scopes", {
|
|
145
|
+
primaryKeys: ["user_id", "scope_id"],
|
|
146
|
+
autoId: false,
|
|
147
|
+
});
|
|
148
|
+
const all = await users.list((qb) => qb.limit(500));
|
|
149
|
+
const targets = all.filter((u) => {
|
|
150
|
+
if (opts.email && u.email_address === opts.email)
|
|
151
|
+
return true;
|
|
152
|
+
return prefixes.some((p) => typeof u.email_address === "string" && u.email_address.startsWith(p));
|
|
153
|
+
});
|
|
154
|
+
let deleted = 0;
|
|
155
|
+
for (const u of targets) {
|
|
156
|
+
const uid = u.id;
|
|
157
|
+
try {
|
|
158
|
+
// Best-effort: delete this user's scope rows. hazo_user_scopes has a
|
|
159
|
+
// composite PK (no `id` column); deleteById on a multi-key table falls
|
|
160
|
+
// back to the first primary key (user_id), so this removes all of the
|
|
161
|
+
// user's scope rows in one call.
|
|
162
|
+
await user_scopes_service.deleteById(uid).catch(() => { });
|
|
163
|
+
await users.deleteById(uid);
|
|
164
|
+
deleted += 1;
|
|
165
|
+
}
|
|
166
|
+
catch (_b) {
|
|
167
|
+
// best-effort — skip users that can't be deleted
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { deleted };
|
|
171
|
+
}
|
|
@@ -21,6 +21,24 @@ export type GoogleTokenStatus = {
|
|
|
21
21
|
scopes: string;
|
|
22
22
|
expires_at: string | null;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Indirection around the hazo_secure/crypto dynamic import.
|
|
26
|
+
*
|
|
27
|
+
* The import specifier MUST be a string literal. hazo_secure is an optional
|
|
28
|
+
* peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
|
|
29
|
+
* that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
|
|
30
|
+
* statically resolve when consumers bundle their server. The import then fails
|
|
31
|
+
* unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
|
|
32
|
+
* hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
|
|
33
|
+
* a native external import and resolves at runtime (consumers must list
|
|
34
|
+
* hazo_secure in serverExternalPackages so it isn't bundled).
|
|
35
|
+
*
|
|
36
|
+
* Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
|
|
37
|
+
* the literal specifier. Not part of the public API (underscore-prefixed).
|
|
38
|
+
*/
|
|
39
|
+
export declare const _cryptoLoader: {
|
|
40
|
+
load: () => Promise<typeof import("hazo_secure/crypto")>;
|
|
41
|
+
};
|
|
24
42
|
/**
|
|
25
43
|
* Stores (inserts or updates) Google OAuth tokens for a user, encrypting sensitive fields.
|
|
26
44
|
* Throws GoogleTokenStorageUnconfigured if hazo_secure/crypto is unavailable or keys are missing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/google_token_service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"google_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/google_token_service.ts"],"names":[],"mappings":"AASA,qBAAa,8BAA+B,SAAQ,KAAK;;CAOxD;AAID,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,cAAc,CAAA;CAAE,CAAC;AAErG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAmCF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa;gBACd,OAAO,CAAC,cAAc,oBAAoB,CAAC,CAAC;CACvD,CAAC;AAeF;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,IAAI,CAAC,CA0Ef;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA6I5B;AAID;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6D1C;AAID;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAexF"}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { createCrudService } from "hazo_connect/server";
|
|
4
4
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
5
5
|
import { create_app_logger } from "../app_logger.js";
|
|
6
|
-
import { optional_import } from "hazo_core";
|
|
7
6
|
import { randomUUID } from "crypto";
|
|
8
7
|
// section: errors
|
|
9
8
|
export class GoogleTokenStorageUnconfigured extends Error {
|
|
@@ -26,8 +25,32 @@ function makeKeyProvider(LookupKeyProvider) {
|
|
|
26
25
|
return undefined;
|
|
27
26
|
});
|
|
28
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Indirection around the hazo_secure/crypto dynamic import.
|
|
30
|
+
*
|
|
31
|
+
* The import specifier MUST be a string literal. hazo_secure is an optional
|
|
32
|
+
* peer dep, so this used to go through hazo_core's `optional_import(pkg)` — but
|
|
33
|
+
* that performs a dynamic `import(variable)`, which Turbopack/webpack cannot
|
|
34
|
+
* statically resolve when consumers bundle their server. The import then fails
|
|
35
|
+
* unconditionally and sign-in dies on GoogleTokenStorageUnconfigured even when
|
|
36
|
+
* hazo_secure is installed. A literal `import("hazo_secure/crypto")` is left as
|
|
37
|
+
* a native external import and resolves at runtime (consumers must list
|
|
38
|
+
* hazo_secure in serverExternalPackages so it isn't bundled).
|
|
39
|
+
*
|
|
40
|
+
* Wrapped in an object so tests can stub `_cryptoLoader.load` without touching
|
|
41
|
+
* the literal specifier. Not part of the public API (underscore-prefixed).
|
|
42
|
+
*/
|
|
43
|
+
export const _cryptoLoader = {
|
|
44
|
+
load: () => import("hazo_secure/crypto"),
|
|
45
|
+
};
|
|
29
46
|
async function load_crypto_module() {
|
|
30
|
-
|
|
47
|
+
let cryptoModule;
|
|
48
|
+
try {
|
|
49
|
+
cryptoModule = await _cryptoLoader.load();
|
|
50
|
+
}
|
|
51
|
+
catch (_a) {
|
|
52
|
+
cryptoModule = null;
|
|
53
|
+
}
|
|
31
54
|
if (!cryptoModule)
|
|
32
55
|
throw new GoogleTokenStorageUnconfigured();
|
|
33
56
|
return cryptoModule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_auth",
|
|
3
|
-
"version": "10.2.
|
|
3
|
+
"version": "10.2.2",
|
|
4
4
|
"description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -206,6 +206,8 @@
|
|
|
206
206
|
"dev:server": "tsx src/server/index.ts",
|
|
207
207
|
"migrate": "tsx scripts/apply_migration.ts",
|
|
208
208
|
"init-users": "tsx scripts/init_users.ts init_users",
|
|
209
|
+
"demo:create": "tsx --conditions react-server scripts/demo.ts create",
|
|
210
|
+
"demo:delete": "tsx --conditions react-server scripts/demo.ts delete",
|
|
209
211
|
"test": "cross-env NODE_ENV=test jest --runInBand",
|
|
210
212
|
"test:watch": "cross-env NODE_ENV=test jest --watch"
|
|
211
213
|
},
|
|
@@ -253,9 +255,9 @@
|
|
|
253
255
|
"@radix-ui/react-tabs": "^1.1.0",
|
|
254
256
|
"@radix-ui/react-tooltip": "^1.2.0",
|
|
255
257
|
"hazo_api": "^2.4.0",
|
|
256
|
-
"hazo_config": "^2.1.
|
|
257
|
-
"hazo_connect": "^3.
|
|
258
|
-
"hazo_core": "^1.
|
|
258
|
+
"hazo_config": "^2.1.11",
|
|
259
|
+
"hazo_connect": "^3.7.0",
|
|
260
|
+
"hazo_core": "^1.2.0",
|
|
259
261
|
"hazo_logs": "^2.0.3",
|
|
260
262
|
"hazo_notify": "^6.1.3",
|
|
261
263
|
"hazo_secure": "^1.1.0",
|
|
@@ -392,13 +394,13 @@
|
|
|
392
394
|
"eslint": "^9.39.1",
|
|
393
395
|
"eslint-config-next": "^16.0.4",
|
|
394
396
|
"eslint-plugin-storybook": "^10.0.6",
|
|
395
|
-
"hazo_api": "^2.
|
|
396
|
-
"hazo_config": "^2.1.
|
|
397
|
-
"hazo_connect": "^3.
|
|
398
|
-
"hazo_core": "^1.
|
|
397
|
+
"hazo_api": "^2.5.0",
|
|
398
|
+
"hazo_config": "^2.1.11",
|
|
399
|
+
"hazo_connect": "^3.7.0",
|
|
400
|
+
"hazo_core": "^1.2.0",
|
|
399
401
|
"hazo_logs": "^2.0.3",
|
|
400
402
|
"hazo_notify": "^6.1.3",
|
|
401
|
-
"hazo_ui": "^4.
|
|
403
|
+
"hazo_ui": "^4.2.0",
|
|
402
404
|
"input-otp": "^1.4.0",
|
|
403
405
|
"jest": "^30.2.0",
|
|
404
406
|
"jest-environment-jsdom": "^30.0.0",
|