hazo_auth 5.1.37 → 5.1.40
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 +271 -258
- package/SETUP_CHECKLIST.md +314 -148
- package/cli-src/lib/schema/sqlite_schema.ts +12 -6
- package/dist/lib/schema/sqlite_schema.d.ts +1 -1
- package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
- package/dist/lib/schema/sqlite_schema.js +12 -6
- package/package.json +27 -26
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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 v5.1.39 🔧
|
|
6
|
+
|
|
7
|
+
**Postgres-Compatibility Schema Alignment** — fixes two long-standing drifts between the canonical SQLite schema and what the runtime actually writes. SQLite consumers see no behaviour change; Postgres + PostgREST consumers can now sign-up users without bespoke schema patches.
|
|
8
|
+
|
|
9
|
+
- **`hazo_refresh_tokens.token_hash`** — `token_service.ts` writes the argon2-hashed token, but the canonical schema only declared `token`. Added `token_hash TEXT` column and an index on it; relaxed the legacy plaintext `token` column to nullable. Runtime no longer writes plaintext, so this is purely additive for new installs.
|
|
10
|
+
- **Boolean-typed columns** — `hazo_users.email_verified` and the four flags on `hazo_user_relationships` (`can_view_progress`, `can_edit_profile`, `can_delete`, `is_self`) are now declared `BOOLEAN` instead of `INTEGER`. SQLite tolerates `BOOLEAN` as a NUMERIC affinity (existing 0/1 data evaluates correctly). PostgreSQL via PostgREST was previously rejecting the runtime's `email_verified: false` payload with `400 — invalid input syntax for type integer: "false"`; now it accepts the boolean cleanly.
|
|
11
|
+
- **Migration `014_align_schema_with_runtime.sql`** — applies the two changes above to existing deployments. SQLite version is active (additive `ADD COLUMN`); PostgreSQL ALTERs are commented blocks for consumers to opt in (with `NOTIFY pgrst, 'reload schema';` reminders).
|
|
12
|
+
|
|
13
|
+
Reported by Kinstripe (Postgres + PostgREST consumer) during sign-up smoke tests on 2026-04-28.
|
|
14
|
+
|
|
5
15
|
### What's New in v5.1.28
|
|
6
16
|
|
|
7
17
|
**Schema Validation, Permission Constants & DX Improvements**
|
|
@@ -547,347 +557,355 @@ cookie_domain = .example.com
|
|
|
547
557
|
|
|
548
558
|
Before using `hazo_auth`, you need to create the required database tables. The package supports both **PostgreSQL** (for production) and **SQLite** (for local development/testing).
|
|
549
559
|
|
|
550
|
-
|
|
560
|
+
The v5.x schema consists of **9 tables**:
|
|
551
561
|
|
|
552
|
-
|
|
562
|
+
| Table | Purpose |
|
|
563
|
+
|-------|---------|
|
|
564
|
+
| `hazo_users` | User accounts and profile data |
|
|
565
|
+
| `hazo_refresh_tokens` | Refresh, password-reset, and email-verification tokens |
|
|
566
|
+
| `hazo_roles` | Role definitions (e.g. `super_user`, `firm_admin`) |
|
|
567
|
+
| `hazo_permissions` | Permission definitions |
|
|
568
|
+
| `hazo_role_permissions` | Role → permission assignments (composite PK) |
|
|
569
|
+
| `hazo_scopes` | Unified hierarchical multi-tenancy with firm branding |
|
|
570
|
+
| `hazo_user_scopes` | User → scope membership with scope-specific role (replaces `hazo_user_roles`) |
|
|
571
|
+
| `hazo_invitations` | Invitations to onboard new users into existing scopes |
|
|
572
|
+
| `hazo_user_relationships` | Managed sub-profile parent/child links (shared-device support) |
|
|
553
573
|
|
|
554
|
-
|
|
574
|
+
> **Removed in v5.0:** the legacy `hazo_org` and `hazo_scopes_l1..l7` tables are gone. The unified `hazo_scopes` table replaces them with an arbitrary-depth `parent_id` hierarchy. The `hazo_user_roles` table is also gone — roles are now assigned per-scope on `hazo_user_scopes.role_id`.
|
|
555
575
|
|
|
556
|
-
|
|
557
|
-
-- Enum type for profile picture source
|
|
558
|
-
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
576
|
+
### Quickest path: use the CLI
|
|
559
577
|
|
|
560
|
-
|
|
561
|
-
|
|
578
|
+
For SQLite development databases, the canonical schema (in `src/lib/schema/sqlite_schema.ts`) ships with the package and can be applied via the CLI:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
npx hazo_auth init-db # Create/recreate the SQLite database with full schema
|
|
582
|
+
npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)
|
|
562
583
|
```
|
|
563
584
|
|
|
564
|
-
|
|
585
|
+
For PostgreSQL or PostgREST deployments, run the script below.
|
|
586
|
+
|
|
587
|
+
### PostgreSQL Setup
|
|
565
588
|
|
|
566
589
|
```sql
|
|
567
|
-
--
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
name TEXT NOT NULL,
|
|
571
|
-
parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
572
|
-
root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
573
|
-
user_limit INTEGER NOT NULL DEFAULT 0,
|
|
574
|
-
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
575
|
-
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
576
|
-
created_by UUID, -- Will reference hazo_users after it's created
|
|
577
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
578
|
-
changed_by UUID
|
|
579
|
-
);
|
|
590
|
+
-- ============================================================
|
|
591
|
+
-- hazo_auth canonical PostgreSQL schema (v5.x)
|
|
592
|
+
-- ============================================================
|
|
580
593
|
|
|
581
|
-
|
|
582
|
-
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
|
|
583
|
-
CREATE INDEX idx_hazo_org_active ON hazo_org(active);
|
|
584
|
-
```
|
|
594
|
+
SET search_path TO public;
|
|
585
595
|
|
|
586
|
-
|
|
596
|
+
-- 1. Enum types
|
|
597
|
+
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
598
|
+
CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
|
|
599
|
+
CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
|
|
600
|
+
CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
|
|
587
601
|
|
|
588
|
-
|
|
589
|
-
-- Main users table
|
|
602
|
+
-- 2. Users
|
|
590
603
|
CREATE TABLE hazo_users (
|
|
591
604
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
592
605
|
email_address TEXT NOT NULL UNIQUE,
|
|
593
|
-
password_hash TEXT,
|
|
606
|
+
password_hash TEXT, -- NULL for OAuth-only users
|
|
594
607
|
name TEXT,
|
|
595
608
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
596
|
-
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
597
609
|
login_attempts INTEGER NOT NULL DEFAULT 0,
|
|
598
610
|
last_logon TIMESTAMP WITH TIME ZONE,
|
|
599
611
|
profile_picture_url TEXT,
|
|
600
612
|
profile_source hazo_enum_profile_source_enum,
|
|
601
613
|
mfa_secret TEXT,
|
|
602
|
-
url_on_logon TEXT,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
614
|
+
url_on_logon TEXT, -- per-user post-login redirect
|
|
615
|
+
google_id TEXT UNIQUE, -- Google OAuth ID
|
|
616
|
+
auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
|
|
617
|
+
user_type TEXT, -- optional categorisation
|
|
618
|
+
app_user_data JSONB, -- consumer-app JSON blob
|
|
619
|
+
status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
|
|
620
|
+
managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
621
|
+
pin_hash TEXT, -- simple PIN auth on shared devices
|
|
608
622
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
609
623
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
610
624
|
);
|
|
611
|
-
|
|
612
|
-
-- Indexes
|
|
613
625
|
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
|
|
626
|
+
CREATE INDEX idx_hazo_users_status ON hazo_users(status);
|
|
614
627
|
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
|
|
615
628
|
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
|
|
616
|
-
CREATE INDEX
|
|
617
|
-
CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
|
|
618
|
-
|
|
619
|
-
-- Add FK constraints to hazo_org after hazo_users exists
|
|
620
|
-
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
|
|
621
|
-
FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
622
|
-
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
|
|
623
|
-
FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
**Note:** The `url_on_logon` field is used to store a custom redirect URL for users after successful login. This allows per-user customization of post-login navigation.
|
|
629
|
+
CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
627
630
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
```sql
|
|
631
|
-
-- Refresh tokens table (used for password reset, email verification, etc.)
|
|
631
|
+
-- 3. Refresh tokens (also used for password reset / email verification)
|
|
632
632
|
CREATE TABLE hazo_refresh_tokens (
|
|
633
633
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
634
634
|
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
635
635
|
token_hash TEXT NOT NULL,
|
|
636
|
-
token_type TEXT NOT NULL,
|
|
636
|
+
token_type TEXT NOT NULL DEFAULT 'refresh',
|
|
637
637
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
638
638
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
639
639
|
);
|
|
640
|
-
|
|
641
|
-
-- Index for token lookups
|
|
642
640
|
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
|
|
643
641
|
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
|
|
644
|
-
```
|
|
645
642
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
```sql
|
|
649
|
-
-- Permissions table for RBAC
|
|
650
|
-
CREATE TABLE hazo_permissions (
|
|
643
|
+
-- 4. Roles
|
|
644
|
+
CREATE TABLE hazo_roles (
|
|
651
645
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
652
|
-
|
|
653
|
-
description TEXT,
|
|
646
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
654
647
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
655
648
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
656
649
|
);
|
|
657
|
-
```
|
|
658
650
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
```sql
|
|
662
|
-
-- Roles table for RBAC
|
|
663
|
-
CREATE TABLE hazo_roles (
|
|
651
|
+
-- 5. Permissions
|
|
652
|
+
CREATE TABLE hazo_permissions (
|
|
664
653
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
665
|
-
|
|
654
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
655
|
+
description TEXT,
|
|
666
656
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
667
657
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
668
658
|
);
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
#### 7. Create the Role-Permissions Junction Table
|
|
672
659
|
|
|
673
|
-
|
|
674
|
-
-- Junction table linking roles to permissions
|
|
660
|
+
-- 6. Role-permission assignments (composite PK, no id column)
|
|
675
661
|
CREATE TABLE hazo_role_permissions (
|
|
676
662
|
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
677
663
|
permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
678
664
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
679
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
680
665
|
PRIMARY KEY (role_id, permission_id)
|
|
681
666
|
);
|
|
682
667
|
|
|
683
|
-
--
|
|
684
|
-
CREATE
|
|
685
|
-
CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
#### 8. Create the User-Roles Junction Table
|
|
689
|
-
|
|
690
|
-
```sql
|
|
691
|
-
-- Junction table linking users to roles
|
|
692
|
-
CREATE TABLE hazo_user_roles (
|
|
693
|
-
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
694
|
-
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
695
|
-
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
696
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
697
|
-
PRIMARY KEY (user_id, role_id)
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
-- Indexes for lookups
|
|
701
|
-
CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
|
|
702
|
-
CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
### Complete PostgreSQL Setup Script
|
|
706
|
-
|
|
707
|
-
For convenience, here's the complete SQL script to create all tables at once:
|
|
708
|
-
|
|
709
|
-
```sql
|
|
710
|
-
-- ============================================
|
|
711
|
-
-- hazo_auth Database Setup Script (PostgreSQL)
|
|
712
|
-
-- ============================================
|
|
713
|
-
|
|
714
|
-
-- 1. Create enum types
|
|
715
|
-
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
716
|
-
-- Note: hazo_enum_scope_types was removed in v5.0 (uses unified hazo_scopes table)
|
|
717
|
-
|
|
718
|
-
-- 2. Create organization table (multi-tenancy)
|
|
719
|
-
CREATE TABLE hazo_org (
|
|
668
|
+
-- 7. Unified scope hierarchy (firms, divisions, departments, ... with branding)
|
|
669
|
+
CREATE TABLE hazo_scopes (
|
|
720
670
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
671
|
+
parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
721
672
|
name TEXT NOT NULL,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
729
|
-
changed_by UUID
|
|
730
|
-
);
|
|
731
|
-
CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
|
|
732
|
-
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
|
|
733
|
-
CREATE INDEX idx_hazo_org_active ON hazo_org(active);
|
|
734
|
-
|
|
735
|
-
-- 3. Create users table
|
|
736
|
-
CREATE TABLE hazo_users (
|
|
737
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
738
|
-
email_address TEXT NOT NULL UNIQUE,
|
|
739
|
-
password_hash TEXT,
|
|
740
|
-
name TEXT,
|
|
741
|
-
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
742
|
-
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
743
|
-
login_attempts INTEGER NOT NULL DEFAULT 0,
|
|
744
|
-
last_logon TIMESTAMP WITH TIME ZONE,
|
|
745
|
-
profile_picture_url TEXT,
|
|
746
|
-
profile_source hazo_enum_profile_source_enum,
|
|
747
|
-
mfa_secret TEXT,
|
|
748
|
-
url_on_logon TEXT,
|
|
749
|
-
user_type TEXT,
|
|
750
|
-
google_id TEXT UNIQUE,
|
|
751
|
-
auth_providers TEXT DEFAULT 'email',
|
|
752
|
-
org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
753
|
-
root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
673
|
+
level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division'
|
|
674
|
+
logo_url TEXT,
|
|
675
|
+
primary_color TEXT,
|
|
676
|
+
secondary_color TEXT,
|
|
677
|
+
tagline TEXT,
|
|
678
|
+
slug TEXT, -- URL-friendly identifier
|
|
754
679
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
755
680
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
756
681
|
);
|
|
757
|
-
CREATE INDEX
|
|
758
|
-
CREATE INDEX
|
|
759
|
-
CREATE
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
--
|
|
770
|
-
CREATE TABLE
|
|
771
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
682
|
+
CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
683
|
+
CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
|
|
684
|
+
CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
685
|
+
|
|
686
|
+
-- 7a. Reserved system scopes
|
|
687
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level)
|
|
688
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
|
|
689
|
+
ON CONFLICT (id) DO NOTHING;
|
|
690
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level)
|
|
691
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
|
|
692
|
+
ON CONFLICT (id) DO NOTHING;
|
|
693
|
+
|
|
694
|
+
-- 8. User-scope membership (replaces v4.x hazo_user_roles)
|
|
695
|
+
CREATE TABLE hazo_user_scopes (
|
|
772
696
|
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
);
|
|
778
|
-
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
|
|
779
|
-
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
|
|
780
|
-
|
|
781
|
-
-- 4. Create permissions table
|
|
782
|
-
CREATE TABLE hazo_permissions (
|
|
783
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
784
|
-
permission_name TEXT NOT NULL UNIQUE,
|
|
785
|
-
description TEXT,
|
|
697
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
698
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
699
|
+
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
700
|
+
status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
|
|
786
701
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
787
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
702
|
+
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
703
|
+
PRIMARY KEY (user_id, scope_id)
|
|
788
704
|
);
|
|
705
|
+
CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
706
|
+
CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
707
|
+
CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
789
708
|
|
|
790
|
-
--
|
|
791
|
-
CREATE TABLE
|
|
709
|
+
-- 9. Invitations
|
|
710
|
+
CREATE TABLE hazo_invitations (
|
|
792
711
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
793
|
-
|
|
712
|
+
email_address TEXT NOT NULL,
|
|
713
|
+
token TEXT NOT NULL UNIQUE,
|
|
714
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
715
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
716
|
+
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
717
|
+
invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
718
|
+
status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
|
|
719
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
720
|
+
accepted_at TIMESTAMP WITH TIME ZONE,
|
|
794
721
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
795
722
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
796
723
|
);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
CREATE
|
|
800
|
-
|
|
801
|
-
|
|
724
|
+
CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
725
|
+
CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
|
|
726
|
+
CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
727
|
+
CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
|
|
728
|
+
CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
729
|
+
|
|
730
|
+
-- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
|
|
731
|
+
CREATE TABLE hazo_user_relationships (
|
|
732
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
733
|
+
parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
734
|
+
child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
735
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
736
|
+
can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
|
|
737
|
+
can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
|
|
738
|
+
can_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
|
739
|
+
is_self BOOLEAN NOT NULL DEFAULT FALSE,
|
|
802
740
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
803
|
-
|
|
804
|
-
PRIMARY KEY (role_id, permission_id)
|
|
741
|
+
UNIQUE (parent_user_id, child_user_id)
|
|
805
742
|
);
|
|
806
|
-
CREATE INDEX
|
|
807
|
-
CREATE INDEX
|
|
743
|
+
CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
744
|
+
CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
808
745
|
|
|
809
|
-
--
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
814
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
815
|
-
PRIMARY KEY (user_id, role_id)
|
|
816
|
-
);
|
|
817
|
-
CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
|
|
818
|
-
CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
|
|
746
|
+
-- 11. Built-in firm_admin role (used when a user creates their first firm)
|
|
747
|
+
INSERT INTO hazo_roles (id, role_name)
|
|
748
|
+
VALUES (gen_random_uuid(), 'firm_admin')
|
|
749
|
+
ON CONFLICT (role_name) DO NOTHING;
|
|
819
750
|
```
|
|
820
751
|
|
|
821
752
|
### SQLite Setup (for local development)
|
|
822
753
|
|
|
823
|
-
|
|
754
|
+
The SQLite version of the schema. Equivalent to running `npx hazo_auth init-db` and identical to `src/lib/schema/sqlite_schema.ts`.
|
|
824
755
|
|
|
825
756
|
```sql
|
|
826
|
-
--
|
|
827
|
-
-- hazo_auth
|
|
828
|
-
--
|
|
757
|
+
-- ============================================================
|
|
758
|
+
-- hazo_auth canonical SQLite schema (v5.x)
|
|
759
|
+
-- ============================================================
|
|
829
760
|
|
|
830
|
-
-- Users
|
|
761
|
+
-- Users
|
|
831
762
|
CREATE TABLE IF NOT EXISTS hazo_users (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
763
|
+
id TEXT PRIMARY KEY,
|
|
764
|
+
email_address TEXT NOT NULL UNIQUE,
|
|
765
|
+
password_hash TEXT,
|
|
766
|
+
name TEXT,
|
|
767
|
+
email_verified INTEGER DEFAULT 0,
|
|
768
|
+
login_attempts INTEGER DEFAULT 0,
|
|
769
|
+
last_logon TEXT,
|
|
770
|
+
profile_picture_url TEXT,
|
|
771
|
+
profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
|
|
772
|
+
mfa_secret TEXT,
|
|
773
|
+
url_on_logon TEXT,
|
|
774
|
+
google_id TEXT UNIQUE,
|
|
775
|
+
auth_providers TEXT DEFAULT 'email',
|
|
776
|
+
user_type TEXT,
|
|
777
|
+
app_user_data TEXT,
|
|
778
|
+
status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
|
|
779
|
+
managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
780
|
+
pin_hash TEXT,
|
|
781
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
782
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
846
783
|
);
|
|
784
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
|
|
786
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
|
|
787
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
847
788
|
|
|
848
|
-
-- Refresh tokens
|
|
789
|
+
-- Refresh tokens
|
|
849
790
|
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
791
|
+
id TEXT PRIMARY KEY,
|
|
792
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
793
|
+
token TEXT NOT NULL UNIQUE,
|
|
794
|
+
token_type TEXT DEFAULT 'refresh',
|
|
795
|
+
expires_at TEXT NOT NULL,
|
|
796
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
856
797
|
);
|
|
798
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
|
|
799
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
|
|
857
800
|
|
|
858
|
-
--
|
|
859
|
-
CREATE TABLE IF NOT EXISTS
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
801
|
+
-- Roles
|
|
802
|
+
CREATE TABLE IF NOT EXISTS hazo_roles (
|
|
803
|
+
id TEXT PRIMARY KEY,
|
|
804
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
805
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
806
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
865
807
|
);
|
|
866
808
|
|
|
867
|
-
--
|
|
868
|
-
CREATE TABLE IF NOT EXISTS
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
809
|
+
-- Permissions
|
|
810
|
+
CREATE TABLE IF NOT EXISTS hazo_permissions (
|
|
811
|
+
id TEXT PRIMARY KEY,
|
|
812
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
813
|
+
description TEXT,
|
|
814
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
815
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
873
816
|
);
|
|
874
817
|
|
|
875
|
-
-- Role-
|
|
818
|
+
-- Role-permission assignments (composite PK, no id column)
|
|
876
819
|
CREATE TABLE IF NOT EXISTS hazo_role_permissions (
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
PRIMARY KEY (role_id, permission_id)
|
|
820
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
821
|
+
permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
822
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
823
|
+
PRIMARY KEY (role_id, permission_id)
|
|
882
824
|
);
|
|
883
825
|
|
|
884
|
-
--
|
|
885
|
-
CREATE TABLE IF NOT EXISTS
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
826
|
+
-- Unified scope hierarchy (firms, divisions, departments, ... with branding)
|
|
827
|
+
CREATE TABLE IF NOT EXISTS hazo_scopes (
|
|
828
|
+
id TEXT PRIMARY KEY,
|
|
829
|
+
parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
830
|
+
name TEXT NOT NULL,
|
|
831
|
+
level TEXT NOT NULL,
|
|
832
|
+
logo_url TEXT,
|
|
833
|
+
primary_color TEXT,
|
|
834
|
+
secondary_color TEXT,
|
|
835
|
+
tagline TEXT,
|
|
836
|
+
slug TEXT,
|
|
837
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
838
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
839
|
+
);
|
|
840
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
841
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
842
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
843
|
+
|
|
844
|
+
-- Reserved system scopes
|
|
845
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
846
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
|
|
847
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
848
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
|
|
849
|
+
|
|
850
|
+
-- User-scope membership (replaces v4.x hazo_user_roles)
|
|
851
|
+
CREATE TABLE IF NOT EXISTS hazo_user_scopes (
|
|
852
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
853
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
854
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
855
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
856
|
+
status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
|
|
857
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
858
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
859
|
+
PRIMARY KEY (user_id, scope_id)
|
|
860
|
+
);
|
|
861
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
862
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
863
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
864
|
+
|
|
865
|
+
-- Invitations
|
|
866
|
+
CREATE TABLE IF NOT EXISTS hazo_invitations (
|
|
867
|
+
id TEXT PRIMARY KEY,
|
|
868
|
+
email_address TEXT NOT NULL,
|
|
869
|
+
token TEXT NOT NULL UNIQUE,
|
|
870
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
871
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
872
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
873
|
+
invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
874
|
+
status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
|
|
875
|
+
expires_at TEXT NOT NULL,
|
|
876
|
+
accepted_at TEXT,
|
|
877
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
878
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
879
|
+
);
|
|
880
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
881
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
|
|
882
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
883
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
|
|
884
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
885
|
+
|
|
886
|
+
-- Managed sub-profile relationships
|
|
887
|
+
CREATE TABLE IF NOT EXISTS hazo_user_relationships (
|
|
888
|
+
id TEXT PRIMARY KEY,
|
|
889
|
+
parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
890
|
+
child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
891
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
892
|
+
can_view_progress INTEGER DEFAULT 1,
|
|
893
|
+
can_edit_profile INTEGER DEFAULT 1,
|
|
894
|
+
can_delete INTEGER DEFAULT 0,
|
|
895
|
+
is_self INTEGER DEFAULT 0,
|
|
896
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
897
|
+
UNIQUE(parent_user_id, child_user_id)
|
|
898
|
+
);
|
|
899
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
900
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
901
|
+
|
|
902
|
+
-- Built-in firm_admin role (used when a user creates their first firm)
|
|
903
|
+
INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
|
|
904
|
+
VALUES (
|
|
905
|
+
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
|
|
906
|
+
'firm_admin',
|
|
907
|
+
datetime('now'),
|
|
908
|
+
datetime('now')
|
|
891
909
|
);
|
|
892
910
|
```
|
|
893
911
|
|
|
@@ -2147,19 +2165,14 @@ scope_cache_max_entries = 5000
|
|
|
2147
2165
|
|
|
2148
2166
|
### Database Setup
|
|
2149
2167
|
|
|
2150
|
-
HRBAC
|
|
2168
|
+
As of v5.0, the HRBAC tables — `hazo_scopes`, `hazo_user_scopes`, and `hazo_invitations` — are part of the **core schema** in [Database Setup](#database-setup), so if you ran the canonical PostgreSQL/SQLite script (or `npx hazo_auth init-db`) they already exist. No additional setup is required to enable HRBAC at the database level.
|
|
2169
|
+
|
|
2170
|
+
If you're upgrading from v4.x, run the consolidation migration to drop the legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables and create the new schema in place:
|
|
2151
2171
|
|
|
2152
2172
|
```bash
|
|
2153
2173
|
npm run migrate migrations/009_scope_consolidation.sql
|
|
2154
2174
|
```
|
|
2155
2175
|
|
|
2156
|
-
This creates:
|
|
2157
|
-
- `hazo_scopes` - Unified scope hierarchy with branding support
|
|
2158
|
-
- `hazo_user_scopes` - User-scope-role assignments
|
|
2159
|
-
- `hazo_invitations` - User invitation flow
|
|
2160
|
-
|
|
2161
|
-
See `SETUP_CHECKLIST.md` for full PostgreSQL and SQLite scripts.
|
|
2162
|
-
|
|
2163
2176
|
### Using hazo_get_auth with Scope Options
|
|
2164
2177
|
|
|
2165
2178
|
When HRBAC is enabled, you can check scope access alongside permissions:
|