hazo_auth 7.0.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/SETUP_CHECKLIST.md +31 -0
- package/cli-src/lib/AGENTS.md +26 -0
- package/cli-src/lib/app_logger.ts +3 -7
- package/cli-src/lib/auth/auth_types.ts +3 -0
- package/cli-src/lib/auth/auth_utils.server.ts +2 -1
- package/cli-src/lib/auth/ensure_anon_id.server.ts +2 -1
- package/cli-src/lib/auth/hazo_get_auth.server.ts +30 -4
- package/cli-src/lib/config/hazo_auth_core_config.ts +44 -0
- package/cli-src/lib/cookies_config.server.ts +13 -10
- package/cli-src/lib/hazo_connect_setup.server.ts +19 -11
- package/cli-src/lib/legal/legal_docs_config.server.ts +61 -0
- package/cli-src/lib/legal/legal_docs_reader.server.ts +36 -0
- package/cli-src/lib/legal/legal_docs_service.ts +197 -0
- package/cli-src/lib/legal/legal_docs_types.ts +31 -0
- package/cli-src/lib/services/email_service.ts +22 -11
- package/cli-src/lib/services/firm_service.ts +2 -1
- package/cli-src/lib/services/otp_service.ts +3 -2
- package/cli-src/lib/services/profile_picture_service.ts +2 -1
- package/cli-src/lib/services/registration_service.ts +16 -1
- package/cli-src/lib/services/relationship_service.ts +5 -4
- package/cli-src/lib/services/session_token_service.ts +3 -2
- package/cli-src/lib/utils/api_route_helpers.ts +4 -59
- package/cli-src/lib/utils/get_origin_url.ts +5 -61
- package/cli-src/lib/utils.ts +4 -10
- package/config/hazo_auth_config.example.ini +6 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -0
- package/dist/components/layouts/index.d.ts +1 -0
- package/dist/components/layouts/index.d.ts.map +1 -1
- package/dist/components/layouts/index.js +2 -0
- package/dist/components/layouts/legal/index.d.ts +5 -0
- package/dist/components/layouts/legal/index.d.ts.map +1 -0
- package/dist/components/layouts/legal/index.js +4 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts +7 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.js +84 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts +9 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.js +11 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts +9 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.js +11 -0
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts +8 -0
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_drawer.js +55 -0
- package/dist/components/layouts/register/hooks/use_register_form.d.ts +5 -1
- package/dist/components/layouts/register/hooks/use_register_form.d.ts.map +1 -1
- package/dist/components/layouts/register/hooks/use_register_form.js +25 -10
- package/dist/components/layouts/register/index.d.ts.map +1 -1
- package/dist/components/layouts/register/index.js +21 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +45 -7
- package/dist/components/ui/input-otp.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/app_logger.d.ts +2 -3
- package/dist/lib/app_logger.d.ts.map +1 -1
- package/dist/lib/app_logger.js +3 -5
- package/dist/lib/auth/auth_types.d.ts +2 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.js +0 -2
- package/dist/lib/auth/auth_utils.server.d.ts.map +1 -1
- package/dist/lib/auth/auth_utils.server.js +2 -1
- package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -1
- package/dist/lib/auth/ensure_anon_id.server.js +2 -1
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +30 -4
- package/dist/lib/config/hazo_auth_core_config.d.ts +44 -0
- package/dist/lib/config/hazo_auth_core_config.d.ts.map +1 -0
- package/dist/lib/config/hazo_auth_core_config.js +40 -0
- package/dist/lib/cookies_config.server.d.ts.map +1 -1
- package/dist/lib/cookies_config.server.js +12 -7
- package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
- package/dist/lib/hazo_connect_setup.server.js +18 -5
- package/dist/lib/legal/legal_docs_config.server.d.ts +22 -0
- package/dist/lib/legal/legal_docs_config.server.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_config.server.js +52 -0
- package/dist/lib/legal/legal_docs_reader.server.d.ts +15 -0
- package/dist/lib/legal/legal_docs_reader.server.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_reader.server.js +24 -0
- package/dist/lib/legal/legal_docs_service.d.ts +49 -0
- package/dist/lib/legal/legal_docs_service.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_service.js +141 -0
- package/dist/lib/legal/legal_docs_types.d.ts +25 -0
- package/dist/lib/legal/legal_docs_types.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_types.js +2 -0
- package/dist/lib/services/email_service.d.ts +1 -1
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +21 -9
- package/dist/lib/services/firm_service.d.ts.map +1 -1
- package/dist/lib/services/firm_service.js +2 -1
- package/dist/lib/services/otp_service.d.ts.map +1 -1
- package/dist/lib/services/otp_service.js +3 -2
- package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
- package/dist/lib/services/profile_picture_service.js +2 -1
- package/dist/lib/services/registration_service.d.ts +5 -0
- package/dist/lib/services/registration_service.d.ts.map +1 -1
- package/dist/lib/services/registration_service.js +6 -0
- package/dist/lib/services/relationship_service.d.ts.map +1 -1
- package/dist/lib/services/relationship_service.js +5 -4
- package/dist/lib/services/session_token_service.d.ts.map +1 -1
- package/dist/lib/services/session_token_service.js +3 -2
- package/dist/lib/utils/api_route_helpers.d.ts +1 -12
- package/dist/lib/utils/api_route_helpers.d.ts.map +1 -1
- package/dist/lib/utils/api_route_helpers.js +4 -57
- package/dist/lib/utils/get_origin_url.d.ts +1 -22
- package/dist/lib/utils/get_origin_url.d.ts.map +1 -1
- package/dist/lib/utils/get_origin_url.js +5 -57
- package/dist/lib/utils.d.ts +2 -3
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +4 -9
- package/dist/page_components/index.d.ts +0 -5
- package/dist/page_components/index.d.ts.map +1 -1
- package/dist/page_components/index.js +0 -5
- package/dist/server/config/config_loader.js +2 -2
- package/dist/server/index.js +1 -1
- package/dist/server/routes/index.d.ts +3 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +4 -0
- package/dist/server/routes/legal_docs_accept.d.ts +3 -0
- package/dist/server/routes/legal_docs_accept.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_accept.js +43 -0
- package/dist/server/routes/legal_docs_get.d.ts +3 -0
- package/dist/server/routes/legal_docs_get.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_get.js +49 -0
- package/dist/server/routes/legal_docs_publish.d.ts +3 -0
- package/dist/server/routes/legal_docs_publish.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_publish.js +35 -0
- package/dist/server/routes/register.d.ts.map +1 -1
- package/dist/server/routes/register.js +26 -0
- package/dist/server/routes/remove_profile_picture.d.ts.map +1 -1
- package/dist/server/routes/remove_profile_picture.js +6 -1
- package/dist/server/routes/upload_profile_picture.d.ts.map +1 -1
- package/dist/server/routes/upload_profile_picture.js +6 -1
- package/dist/server/routes/user_management_users.d.ts +2 -2
- package/dist/server/routes/user_management_users.d.ts.map +1 -1
- package/dist/server/routes/user_management_users.js +46 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +7 -0
- package/dist/strings.d.ts +2 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +3 -0
- package/package.json +33 -35
package/README.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
|
|
4
4
|
|
|
5
|
+
### What's New in v8.0.1 🔧
|
|
6
|
+
|
|
7
|
+
**Auto-test and middleware bug fixes**
|
|
8
|
+
|
|
9
|
+
- Fixed 307 redirects blocking OTP, strings, consent, and legal_docs API routes when hit without auth cookies — all four are now in `middleware.ts` `public_routes`.
|
|
10
|
+
- Auto-test runner register calls now include `legal_accepted` hashes when legal docs are configured, fixing 24 test failures that cascaded from the initial registration step.
|
|
11
|
+
|
|
12
|
+
### What's New in v8.0.0 ⚠️ BREAKING CHANGE
|
|
13
|
+
|
|
14
|
+
**Legal Document Acceptance** — opt-in, INI-configured. Add `[hazo_auth__legal_docs]` to `hazo_auth_config.ini` to require users to accept terms before registering. Each doc is a markdown file; hazo_auth hashes it, tracks acceptance history, and blocks register until all current hashes are accepted.
|
|
15
|
+
|
|
16
|
+
**Breaking:** Deprecated page-component wrappers removed (`LoginPage`, `RegisterPage`, `ForgotPasswordPage`, `ResetPasswordPage`, `VerifyEmailPage` from `hazo_auth/page_components/*`). Use `*Layout` components directly in a server component instead.
|
|
17
|
+
|
|
18
|
+
Run these migrations (in order) to upgrade an existing database:
|
|
19
|
+
- `017_legal_acceptance_column.sql`
|
|
20
|
+
- `018_hazo_legal_acceptances.sql`
|
|
21
|
+
- `019_hazo_legal_doc_versions.sql`
|
|
22
|
+
|
|
23
|
+
**New exports** — `LegalAcceptanceGate`, `LegalDocDrawer`, `LegalDocCheckboxList`, `LegalDocCombinedView` from `hazo_auth/client`; `legalDocsGET`, `legalDocsAcceptPOST`, `legalDocsPublishPOST` from `hazo_auth/server/routes`; `LegalDoc`, `LegalAcceptanceRecord`, `LegalAcceptanceMap` types from `hazo_auth`.
|
|
24
|
+
|
|
25
|
+
**New dependency** — `react-markdown` (markdown rendering for legal doc drawers).
|
|
26
|
+
|
|
27
|
+
```ini
|
|
28
|
+
# config/hazo_auth_config.ini
|
|
29
|
+
[hazo_auth__legal_docs]
|
|
30
|
+
display_mode = separate # separate | combined
|
|
31
|
+
doc_1_key = tos
|
|
32
|
+
doc_1_title = Terms of Service
|
|
33
|
+
doc_1_path = legal/tos.md
|
|
34
|
+
doc_2_key = privacy
|
|
35
|
+
doc_2_title = Privacy Policy
|
|
36
|
+
doc_2_path = legal/privacy.md
|
|
37
|
+
```
|
|
38
|
+
|
|
5
39
|
### What's New in v5.3.1 🔧
|
|
6
40
|
|
|
7
41
|
**`get_client_ip(request)` exported from `hazo_auth/server-lib`** — extracts the client IP from `x-forwarded-for` (first element), falling back to `x-real-ip`, then `"unknown"`. Previously private to `hazo_get_auth.server.ts`. Useful for consumers that need consistent IP extraction across handlers (e.g., `hazo_feedback` audit logging).
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
This checklist provides step-by-step instructions for setting up the `hazo_auth` package in your Next.js project. AI assistants can follow this guide to ensure complete and correct setup.
|
|
4
4
|
|
|
5
|
+
## v8.0.0 Migration (from v7.x)
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
Remove the deprecated page-component imports if you used them:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- import LoginPage from 'hazo_auth/page_components/login';
|
|
13
|
+
- import RegisterPage from 'hazo_auth/page_components/register';
|
|
14
|
+
+ // Use the Layout components directly in a server component instead
|
|
15
|
+
+ import LoginLayout from 'hazo_auth/components/layouts/login';
|
|
16
|
+
+ import RegisterLayout from 'hazo_auth/components/layouts/register';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### New migrations (run in order)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run migrate migrations/017_legal_acceptance_column.sql
|
|
23
|
+
npm run migrate migrations/018_hazo_legal_acceptances.sql
|
|
24
|
+
npm run migrate migrations/019_hazo_legal_doc_versions.sql
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Legal docs setup (optional)
|
|
28
|
+
|
|
29
|
+
1. Add `[hazo_auth__legal_docs]` to `config/hazo_auth_config.ini` (see commented sample at the bottom of that file)
|
|
30
|
+
2. Create markdown files at the configured paths (e.g. `legal/tos.md`, `legal/privacy.md`)
|
|
31
|
+
3. Wrap your app layout with `<LegalAcceptanceGate>` from `hazo_auth/client`
|
|
32
|
+
4. Go to User Management → Legal Docs tab, click "Publish current version" per doc to activate gating for existing users
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
5
36
|
## v5.3.0 Migration (from v5.2.x)
|
|
6
37
|
|
|
7
38
|
If you are already on hazo_auth `5.2.x`, the only mandatory work is wiring up hazo_notify's template manager at boot. Apps that don't use the templated email path (only the plain `send_email`) keep working unchanged after bumping the peer dep.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# src/lib/ — AGENTS.md
|
|
2
|
+
|
|
3
|
+
Server-side business logic, configuration loaders, and utilities.
|
|
4
|
+
|
|
5
|
+
## Key subdirectories
|
|
6
|
+
|
|
7
|
+
- `auth/` — Core auth logic: `hazo_get_auth.server.ts` (the main auth utility), session token validation, OAuth helpers, auth cache, scope cache, rate limiter.
|
|
8
|
+
- `config/` — Config loaders: `config_loader.server.ts` (hazo_config-based, handles all 18+ INI sections), `hazo_auth_core_config.ts` (hazo_core loadConfig overlay for critical sections with Zod validation).
|
|
9
|
+
- `services/` — Business logic: `login_service.ts`, `registration_service.ts`, `password_reset_service.ts`, `session_token_service.ts`, `scope_service.ts`, `invitation_service.ts`, `email_service.ts`, etc.
|
|
10
|
+
- `auto_test/` — hazo_auth's internal test runner (server-side). This is the original test infrastructure that inspired `hazo_ui/test-harness`. Executes scenario definitions via API calls; the runner is invoked through `/api/hazo_auth/auto_test`.
|
|
11
|
+
- `utils/` — Utilities: `api_route_helpers.ts` (re-exports `get_filename`/`get_line_number` from hazo_logs), `sanitize_error_for_user.ts`.
|
|
12
|
+
- `legal/` — Legal docs management (acceptance tracking, versioned docs).
|
|
13
|
+
- `schema/` — Zod schemas for request validation.
|
|
14
|
+
|
|
15
|
+
## Error handling (Wave 2)
|
|
16
|
+
|
|
17
|
+
Server-side `throw new Error(...)` replaced with `HazoError` subclasses from `hazo_core`:
|
|
18
|
+
- `HazoAuthError(HAZO_AUTH_FORBIDDEN)` — authentication required or permission denied
|
|
19
|
+
- `HazoAuthError(HAZO_AUTH_INVALID_TOKEN)` — invalid session token
|
|
20
|
+
- `HazoNotFoundError(HAZO_AUTH_USER_NOT_FOUND)` — user not found
|
|
21
|
+
- `HazoRateLimitError(HAZO_AUTH_RATE_LIMITED)` — rate limit exceeded
|
|
22
|
+
- `HazoConfigError(HAZO_AUTH_CONFIG)` — missing/invalid config or env vars
|
|
23
|
+
|
|
24
|
+
## Logging
|
|
25
|
+
|
|
26
|
+
All logging via `create_app_logger()` from `app_logger.ts`, which returns `createLogger('hazo_auth')` from `hazo_core`. Log calls use structured format: `logger.info('event.name', { fields })`.
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
// file_description: server-only wrapper for the main app logging service using
|
|
2
|
-
// section: server-only-guard
|
|
3
|
-
import "server-only";
|
|
4
|
-
|
|
1
|
+
// file_description: server-only wrapper for the main app logging service using hazo_core
|
|
5
2
|
// section: imports
|
|
6
|
-
import { createLogger } from "
|
|
3
|
+
import { createLogger } from "hazo_core";
|
|
7
4
|
|
|
8
5
|
// section: logger_instance
|
|
9
6
|
// Create a singleton logger for the hazo_auth package
|
|
@@ -11,7 +8,6 @@ const logger = createLogger("hazo_auth");
|
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* Returns the hazo_auth logger instance
|
|
14
|
-
* Uses
|
|
11
|
+
* Uses hazo_core for consistent logging across hazo packages
|
|
15
12
|
*/
|
|
16
13
|
export const create_app_logger = () => logger;
|
|
17
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// file_description: Type definitions and error classes for hazo_get_auth utility
|
|
2
2
|
// section: types
|
|
3
|
+
import type { LegalAcceptanceMap } from '../legal/legal_docs_types';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* User data structure returned by hazo_get_auth
|
|
@@ -13,6 +14,8 @@ export type HazoAuthUser = {
|
|
|
13
14
|
managed_by_user_id?: string | null;
|
|
14
15
|
// App-specific user data (JSON object stored as TEXT in database)
|
|
15
16
|
app_user_data: Record<string, unknown> | null;
|
|
17
|
+
// Legal document acceptance map (JSON object stored in database)
|
|
18
|
+
legal_acceptance: LegalAcceptanceMap | null;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -4,6 +4,7 @@ import "server-only";
|
|
|
4
4
|
|
|
5
5
|
// section: imports
|
|
6
6
|
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import { HazoAuthError } from "hazo_core";
|
|
7
8
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
8
9
|
import { createCrudService } from "hazo_connect/server";
|
|
9
10
|
import { map_db_source_to_ui } from "../services/profile_picture_source_mapper.js";
|
|
@@ -118,7 +119,7 @@ export async function require_auth(request: NextRequest): Promise<AuthUser> {
|
|
|
118
119
|
const result = await get_authenticated_user(request);
|
|
119
120
|
|
|
120
121
|
if (!result.authenticated) {
|
|
121
|
-
throw new
|
|
122
|
+
throw new HazoAuthError({ code: 'HAZO_AUTH_FORBIDDEN', pkg: 'hazo_auth', message: 'Authentication required' });
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
return result;
|
|
@@ -24,6 +24,7 @@ import "server-only";
|
|
|
24
24
|
// section: imports
|
|
25
25
|
import { NextRequest } from "next/server";
|
|
26
26
|
import { cookies } from "next/headers";
|
|
27
|
+
import { generateRequestId } from "hazo_core";
|
|
27
28
|
import {
|
|
28
29
|
BASE_COOKIE_NAMES,
|
|
29
30
|
get_cookie_name,
|
|
@@ -71,7 +72,7 @@ export async function ensure_anon_id(request: NextRequest): Promise<string> {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// Issue a new id and queue the Set-Cookie via the next/headers cookie store.
|
|
74
|
-
const new_id =
|
|
75
|
+
const new_id = generateRequestId().slice(4);
|
|
75
76
|
const cookie_options = get_cookie_options({
|
|
76
77
|
httpOnly: true,
|
|
77
78
|
secure: process.env.NODE_ENV === "production",
|
|
@@ -4,6 +4,7 @@ import "server-only";
|
|
|
4
4
|
|
|
5
5
|
// section: imports
|
|
6
6
|
import { NextRequest } from "next/server";
|
|
7
|
+
import { HazoNotFoundError, HazoAuthError, HazoRateLimitError, getCorrelationId } from "hazo_core";
|
|
7
8
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
8
9
|
import { createCrudService } from "hazo_connect/server";
|
|
9
10
|
import { create_app_logger } from "../app_logger.js";
|
|
@@ -59,6 +60,24 @@ function parse_app_user_data(
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Parse raw legal_acceptance field from DB to LegalAcceptanceMap
|
|
65
|
+
* @param raw - Raw value from database (string or object)
|
|
66
|
+
* @returns Parsed LegalAcceptanceMap or null
|
|
67
|
+
*/
|
|
68
|
+
function parse_legal_acceptance(
|
|
69
|
+
raw: unknown,
|
|
70
|
+
): import('../legal/legal_docs_types').LegalAcceptanceMap | null {
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
74
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
75
|
+
return parsed as import('../legal/legal_docs_types').LegalAcceptanceMap;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Gets client IP address from request
|
|
64
83
|
* @param request - NextRequest object
|
|
@@ -165,14 +184,14 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
|
|
|
165
184
|
// Fetch user
|
|
166
185
|
const users = await users_service.findBy({ id: user_id });
|
|
167
186
|
if (!Array.isArray(users) || users.length === 0) {
|
|
168
|
-
throw new
|
|
187
|
+
throw new HazoNotFoundError({ code: 'HAZO_AUTH_USER_NOT_FOUND', pkg: 'hazo_auth', message: 'User not found' });
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
const user_db = users[0];
|
|
172
191
|
|
|
173
192
|
// Check if user is active (status must be 'ACTIVE')
|
|
174
193
|
if (user_db.status !== "ACTIVE") {
|
|
175
|
-
throw new
|
|
194
|
+
throw new HazoAuthError({ code: 'HAZO_AUTH_FORBIDDEN', pkg: 'hazo_auth', message: 'User account is inactive' });
|
|
176
195
|
}
|
|
177
196
|
|
|
178
197
|
// Build user object
|
|
@@ -185,6 +204,7 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
|
|
|
185
204
|
(user_db.profile_picture_url as string | null) || null,
|
|
186
205
|
managed_by_user_id: (user_db.managed_by_user_id as string | undefined) || null,
|
|
187
206
|
app_user_data: parse_app_user_data(user_db.app_user_data),
|
|
207
|
+
legal_acceptance: parse_legal_acceptance(user_db.legal_acceptance),
|
|
188
208
|
};
|
|
189
209
|
|
|
190
210
|
// v5.x: Fetch user's roles from hazo_user_scopes (scope-based role assignments)
|
|
@@ -422,6 +442,7 @@ export async function hazo_get_auth(
|
|
|
422
442
|
line_number: get_line_number(),
|
|
423
443
|
error: token_error_message,
|
|
424
444
|
note: "Falling back to simple cookie check",
|
|
445
|
+
correlation_id: getCorrelationId(),
|
|
425
446
|
});
|
|
426
447
|
}
|
|
427
448
|
}
|
|
@@ -445,8 +466,9 @@ export async function hazo_get_auth(
|
|
|
445
466
|
filename: get_filename(),
|
|
446
467
|
line_number: get_line_number(),
|
|
447
468
|
ip: client_ip,
|
|
469
|
+
correlation_id: getCorrelationId(),
|
|
448
470
|
});
|
|
449
|
-
throw new
|
|
471
|
+
throw new HazoRateLimitError({ code: 'HAZO_AUTH_RATE_LIMITED', pkg: 'hazo_auth', message: 'Rate limit exceeded. Please try again later.' });
|
|
450
472
|
}
|
|
451
473
|
|
|
452
474
|
return {
|
|
@@ -464,8 +486,9 @@ export async function hazo_get_auth(
|
|
|
464
486
|
filename: get_filename(),
|
|
465
487
|
line_number: get_line_number(),
|
|
466
488
|
user_id,
|
|
489
|
+
correlation_id: getCorrelationId(),
|
|
467
490
|
});
|
|
468
|
-
throw new
|
|
491
|
+
throw new HazoRateLimitError({ code: 'HAZO_AUTH_RATE_LIMITED', pkg: 'hazo_auth', message: 'Rate limit exceeded. Please try again later.' });
|
|
469
492
|
}
|
|
470
493
|
|
|
471
494
|
// Check cache
|
|
@@ -497,6 +520,7 @@ export async function hazo_get_auth(
|
|
|
497
520
|
line_number: get_line_number(),
|
|
498
521
|
user_id,
|
|
499
522
|
error: error_message,
|
|
523
|
+
correlation_id: getCorrelationId(),
|
|
500
524
|
});
|
|
501
525
|
|
|
502
526
|
return {
|
|
@@ -531,6 +555,7 @@ export async function hazo_get_auth(
|
|
|
531
555
|
missing_permissions,
|
|
532
556
|
user_permissions: permissions,
|
|
533
557
|
ip: client_ip,
|
|
558
|
+
correlation_id: getCorrelationId(),
|
|
534
559
|
});
|
|
535
560
|
}
|
|
536
561
|
|
|
@@ -580,6 +605,7 @@ export async function hazo_get_auth(
|
|
|
580
605
|
scope_id: options.scope_id,
|
|
581
606
|
user_scopes: scope_result.user_scopes,
|
|
582
607
|
ip: client_ip,
|
|
608
|
+
correlation_id: getCorrelationId(),
|
|
583
609
|
});
|
|
584
610
|
}
|
|
585
611
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// file_description: Zod-validated config loader for hazo_auth core settings.
|
|
2
|
+
// Covers server-critical sections ([hazo_auth__tokens], [hazo_auth__cookies], [hazo_auth__rate_limit], [log.overrides]).
|
|
3
|
+
// UI sections (login_layout, register_layout, etc.) are still handled by config_loader.server.ts.
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { loadConfig } from 'hazo_core';
|
|
6
|
+
|
|
7
|
+
const HazoAuthCoreConfigSchema = z.object({
|
|
8
|
+
hazo_auth__tokens: z
|
|
9
|
+
.object({
|
|
10
|
+
access_token_ttl_seconds: z.string().optional().transform(v => v ? parseInt(v, 10) : 900),
|
|
11
|
+
refresh_token_ttl_seconds: z.string().optional().transform(v => v ? parseInt(v, 10) : 2592000),
|
|
12
|
+
})
|
|
13
|
+
.optional()
|
|
14
|
+
.transform(v => v ?? { access_token_ttl_seconds: 900, refresh_token_ttl_seconds: 2592000 }),
|
|
15
|
+
hazo_auth__cookies: z
|
|
16
|
+
.object({
|
|
17
|
+
cookie_prefix: z.string().optional().default(''),
|
|
18
|
+
cookie_domain: z.string().optional().default(''),
|
|
19
|
+
})
|
|
20
|
+
.optional()
|
|
21
|
+
.transform(v => v ?? { cookie_prefix: '', cookie_domain: '' }),
|
|
22
|
+
hazo_auth__rate_limit: z
|
|
23
|
+
.object({
|
|
24
|
+
max_attempts: z.string().optional().transform(v => v ? parseInt(v, 10) : 5),
|
|
25
|
+
window_minutes: z.string().optional().transform(v => v ? parseInt(v, 10) : 5),
|
|
26
|
+
})
|
|
27
|
+
.optional()
|
|
28
|
+
.transform(v => v ?? { max_attempts: 5, window_minutes: 5 }),
|
|
29
|
+
log: z
|
|
30
|
+
.object({
|
|
31
|
+
overrides: z.record(z.string(), z.string()).optional().default({}),
|
|
32
|
+
})
|
|
33
|
+
.optional()
|
|
34
|
+
.transform(v => v ?? { overrides: {} }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type HazoAuthCoreConfig = z.infer<typeof HazoAuthCoreConfigSchema>;
|
|
38
|
+
|
|
39
|
+
export function getHazoAuthCoreConfig(): HazoAuthCoreConfig {
|
|
40
|
+
return loadConfig<HazoAuthCoreConfig>({
|
|
41
|
+
pkg: 'hazo_auth',
|
|
42
|
+
schema: HazoAuthCoreConfigSchema as never,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// section: server-only-guard
|
|
3
3
|
import "server-only";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { HazoConfigError } from "hazo_core";
|
|
6
6
|
import { read_config_section } from "./config/config_loader.server.js";
|
|
7
7
|
|
|
8
8
|
// section: types
|
|
@@ -44,15 +44,18 @@ export function get_cookies_config(): CookiesConfig {
|
|
|
44
44
|
const cookie_prefix = section?.cookie_prefix || "";
|
|
45
45
|
|
|
46
46
|
if (!cookie_prefix) {
|
|
47
|
-
throw new
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
throw new HazoConfigError({
|
|
48
|
+
code: 'HAZO_AUTH_CONFIG',
|
|
49
|
+
pkg: 'hazo_auth',
|
|
50
|
+
message:
|
|
51
|
+
"[hazo_auth] cookie_prefix is required but not configured.\n" +
|
|
52
|
+
"Set cookie_prefix in [hazo_auth__cookies] section of config/hazo_auth_config.ini:\n\n" +
|
|
53
|
+
" [hazo_auth__cookies]\n" +
|
|
54
|
+
" cookie_prefix = myapp_\n\n" +
|
|
55
|
+
"Also set the matching environment variable for Edge runtime (middleware):\n" +
|
|
56
|
+
" HAZO_AUTH_COOKIE_PREFIX=myapp_\n\n" +
|
|
57
|
+
"This prevents cookie conflicts between apps using hazo_auth.",
|
|
58
|
+
});
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
return {
|
|
@@ -7,6 +7,7 @@ import "server-only";
|
|
|
7
7
|
// section: imports
|
|
8
8
|
import { createHazoConnect } from "hazo_connect/server";
|
|
9
9
|
import { HazoConfig } from "hazo_config/server";
|
|
10
|
+
import { HazoConfigError } from "hazo_core";
|
|
10
11
|
import path from "path";
|
|
11
12
|
import fs from "fs";
|
|
12
13
|
import { create_app_logger } from "./app_logger.js";
|
|
@@ -79,10 +80,13 @@ function get_hazo_connect_config(): {
|
|
|
79
80
|
);
|
|
80
81
|
sqlite_path = path.normalize(fallback_sqlite_path);
|
|
81
82
|
} else {
|
|
82
|
-
throw new
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
throw new HazoConfigError({
|
|
84
|
+
code: 'HAZO_AUTH_CONFIG',
|
|
85
|
+
pkg: 'hazo_auth',
|
|
86
|
+
message:
|
|
87
|
+
"[hazo_auth] sqlite_path not configured. Set sqlite_path in [hazo_connect] section of config/hazo_auth_config.ini, " +
|
|
88
|
+
"or set HAZO_CONNECT_SQLITE_PATH environment variable.",
|
|
89
|
+
});
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
// Validate config keys for typos
|
|
@@ -132,9 +136,11 @@ function get_hazo_connect_config(): {
|
|
|
132
136
|
process.env.POSTGREST_API_KEY;
|
|
133
137
|
|
|
134
138
|
if (!postgrest_url) {
|
|
135
|
-
throw new
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
throw new HazoConfigError({
|
|
140
|
+
code: 'HAZO_AUTH_CONFIG',
|
|
141
|
+
pkg: 'hazo_auth',
|
|
142
|
+
message: 'PostgREST URL is required. Set postgrest_url in [hazo_connect] section of hazo_auth_config.ini or HAZO_CONNECT_POSTGREST_URL environment variable.',
|
|
143
|
+
});
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
return {
|
|
@@ -145,9 +151,11 @@ function get_hazo_connect_config(): {
|
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
throw new
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
throw new HazoConfigError({
|
|
155
|
+
code: 'HAZO_AUTH_CONFIG',
|
|
156
|
+
pkg: 'hazo_auth',
|
|
157
|
+
message: `Unsupported HAZO_CONNECT_TYPE: ${type}. Supported types: sqlite, postgrest`,
|
|
158
|
+
});
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
/**
|
|
@@ -177,7 +185,7 @@ export function create_sqlite_hazo_connect_server() {
|
|
|
177
185
|
});
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
throw new
|
|
188
|
+
throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: `Unsupported database type: ${config.type}` });
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// file_description: server-only helper to read legal docs configuration from hazo_auth_config.ini
|
|
2
|
+
// section: server-only-guard
|
|
3
|
+
import 'server-only';
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import { read_config_section } from '../config/config_loader.server.js';
|
|
7
|
+
import type { LegalDocConfig, LegalDocsConfig } from './legal_docs_types';
|
|
8
|
+
|
|
9
|
+
// section: constants
|
|
10
|
+
const SECTION_NAME = 'hazo_auth__legal_docs';
|
|
11
|
+
|
|
12
|
+
// section: cache
|
|
13
|
+
// Cached after first load — INI changes require server restart anyway
|
|
14
|
+
let _cached: LegalDocsConfig | null = null;
|
|
15
|
+
|
|
16
|
+
// section: exports
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads legal docs configuration from hazo_auth_config.ini.
|
|
20
|
+
* Returns an empty docs array if the section is absent (legal docs disabled).
|
|
21
|
+
*
|
|
22
|
+
* Expected INI shape:
|
|
23
|
+
* [hazo_auth__legal_docs]
|
|
24
|
+
* display_mode = separate ; or: combined
|
|
25
|
+
* doc_1_key = terms
|
|
26
|
+
* doc_1_title = Terms of Service
|
|
27
|
+
* doc_1_path = legal/terms.md
|
|
28
|
+
* doc_2_key = privacy
|
|
29
|
+
* doc_2_title = Privacy Policy
|
|
30
|
+
* doc_2_path = legal/privacy.md
|
|
31
|
+
*/
|
|
32
|
+
export function get_legal_docs_config(): LegalDocsConfig {
|
|
33
|
+
if (_cached) return _cached;
|
|
34
|
+
|
|
35
|
+
const section = read_config_section(SECTION_NAME) ?? {};
|
|
36
|
+
|
|
37
|
+
const docs: LegalDocConfig[] = [];
|
|
38
|
+
let i = 1;
|
|
39
|
+
while (section[`doc_${i}_key`]) {
|
|
40
|
+
docs.push({
|
|
41
|
+
key: section[`doc_${i}_key`],
|
|
42
|
+
title: section[`doc_${i}_title`] ?? section[`doc_${i}_key`],
|
|
43
|
+
path: section[`doc_${i}_path`],
|
|
44
|
+
});
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_cached = {
|
|
49
|
+
docs,
|
|
50
|
+
display_mode: section['display_mode'] === 'combined' ? 'combined' : 'separate',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return _cached;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Call this in tests to clear the cache between runs.
|
|
58
|
+
*/
|
|
59
|
+
export function _reset_legal_docs_config_cache(): void {
|
|
60
|
+
_cached = null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// file_description: server-only utility that reads a legal document from disk and returns its content + SHA-256 hash
|
|
2
|
+
// section: server-only-guard
|
|
3
|
+
import 'server-only';
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
// section: types
|
|
11
|
+
|
|
12
|
+
export interface ReadDocResult {
|
|
13
|
+
content: string;
|
|
14
|
+
hash: string; // "sha256:<hex>"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// section: exports
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads a legal document from the filesystem and returns its text content
|
|
21
|
+
* together with a deterministic SHA-256 hash of that content.
|
|
22
|
+
*
|
|
23
|
+
* @param doc_path - Absolute path, or a path relative to process.cwd().
|
|
24
|
+
* @returns { content, hash } where hash is formatted as "sha256:<hex>".
|
|
25
|
+
* @throws If the file cannot be read.
|
|
26
|
+
*/
|
|
27
|
+
export function read_legal_doc(doc_path: string): ReadDocResult {
|
|
28
|
+
const abs_path = path.isAbsolute(doc_path)
|
|
29
|
+
? doc_path
|
|
30
|
+
: path.join(process.cwd(), doc_path);
|
|
31
|
+
|
|
32
|
+
const content = fs.readFileSync(abs_path, 'utf-8');
|
|
33
|
+
const hex = createHash('sha256').update(content).digest('hex');
|
|
34
|
+
|
|
35
|
+
return { content, hash: `sha256:${hex}` };
|
|
36
|
+
}
|