hazo_auth 5.1.37 → 5.1.38
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 +261 -258
- package/SETUP_CHECKLIST.md +314 -148
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -547,347 +547,355 @@ cookie_domain = .example.com
|
|
|
547
547
|
|
|
548
548
|
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
549
|
|
|
550
|
-
|
|
550
|
+
The v5.x schema consists of **9 tables**:
|
|
551
551
|
|
|
552
|
-
|
|
552
|
+
| Table | Purpose |
|
|
553
|
+
|-------|---------|
|
|
554
|
+
| `hazo_users` | User accounts and profile data |
|
|
555
|
+
| `hazo_refresh_tokens` | Refresh, password-reset, and email-verification tokens |
|
|
556
|
+
| `hazo_roles` | Role definitions (e.g. `super_user`, `firm_admin`) |
|
|
557
|
+
| `hazo_permissions` | Permission definitions |
|
|
558
|
+
| `hazo_role_permissions` | Role → permission assignments (composite PK) |
|
|
559
|
+
| `hazo_scopes` | Unified hierarchical multi-tenancy with firm branding |
|
|
560
|
+
| `hazo_user_scopes` | User → scope membership with scope-specific role (replaces `hazo_user_roles`) |
|
|
561
|
+
| `hazo_invitations` | Invitations to onboard new users into existing scopes |
|
|
562
|
+
| `hazo_user_relationships` | Managed sub-profile parent/child links (shared-device support) |
|
|
553
563
|
|
|
554
|
-
|
|
564
|
+
> **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
565
|
|
|
556
|
-
|
|
557
|
-
-- Enum type for profile picture source
|
|
558
|
-
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
566
|
+
### Quickest path: use the CLI
|
|
559
567
|
|
|
560
|
-
|
|
561
|
-
|
|
568
|
+
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:
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
npx hazo_auth init-db # Create/recreate the SQLite database with full schema
|
|
572
|
+
npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)
|
|
562
573
|
```
|
|
563
574
|
|
|
564
|
-
|
|
575
|
+
For PostgreSQL or PostgREST deployments, run the script below.
|
|
576
|
+
|
|
577
|
+
### PostgreSQL Setup
|
|
565
578
|
|
|
566
579
|
```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
|
-
);
|
|
580
|
+
-- ============================================================
|
|
581
|
+
-- hazo_auth canonical PostgreSQL schema (v5.x)
|
|
582
|
+
-- ============================================================
|
|
580
583
|
|
|
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
|
-
```
|
|
584
|
+
SET search_path TO public;
|
|
585
585
|
|
|
586
|
-
|
|
586
|
+
-- 1. Enum types
|
|
587
|
+
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
588
|
+
CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
|
|
589
|
+
CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
|
|
590
|
+
CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
|
|
587
591
|
|
|
588
|
-
|
|
589
|
-
-- Main users table
|
|
592
|
+
-- 2. Users
|
|
590
593
|
CREATE TABLE hazo_users (
|
|
591
594
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
592
595
|
email_address TEXT NOT NULL UNIQUE,
|
|
593
|
-
password_hash TEXT,
|
|
596
|
+
password_hash TEXT, -- NULL for OAuth-only users
|
|
594
597
|
name TEXT,
|
|
595
598
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
596
|
-
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
597
599
|
login_attempts INTEGER NOT NULL DEFAULT 0,
|
|
598
600
|
last_logon TIMESTAMP WITH TIME ZONE,
|
|
599
601
|
profile_picture_url TEXT,
|
|
600
602
|
profile_source hazo_enum_profile_source_enum,
|
|
601
603
|
mfa_secret TEXT,
|
|
602
|
-
url_on_logon TEXT,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
604
|
+
url_on_logon TEXT, -- per-user post-login redirect
|
|
605
|
+
google_id TEXT UNIQUE, -- Google OAuth ID
|
|
606
|
+
auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
|
|
607
|
+
user_type TEXT, -- optional categorisation
|
|
608
|
+
app_user_data JSONB, -- consumer-app JSON blob
|
|
609
|
+
status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
|
|
610
|
+
managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
611
|
+
pin_hash TEXT, -- simple PIN auth on shared devices
|
|
608
612
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
609
613
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
610
614
|
);
|
|
611
|
-
|
|
612
|
-
-- Indexes
|
|
613
615
|
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
|
|
616
|
+
CREATE INDEX idx_hazo_users_status ON hazo_users(status);
|
|
614
617
|
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
|
|
615
618
|
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.
|
|
627
|
-
|
|
628
|
-
#### 4. Create the Refresh Tokens Table
|
|
619
|
+
CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
629
620
|
|
|
630
|
-
|
|
631
|
-
-- Refresh tokens table (used for password reset, email verification, etc.)
|
|
621
|
+
-- 3. Refresh tokens (also used for password reset / email verification)
|
|
632
622
|
CREATE TABLE hazo_refresh_tokens (
|
|
633
623
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
634
624
|
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
635
625
|
token_hash TEXT NOT NULL,
|
|
636
|
-
token_type TEXT NOT NULL,
|
|
626
|
+
token_type TEXT NOT NULL DEFAULT 'refresh',
|
|
637
627
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
638
628
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
639
629
|
);
|
|
640
|
-
|
|
641
|
-
-- Index for token lookups
|
|
642
630
|
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
|
|
643
631
|
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
#### 5. Create the Permissions Table
|
|
647
632
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
CREATE TABLE hazo_permissions (
|
|
633
|
+
-- 4. Roles
|
|
634
|
+
CREATE TABLE hazo_roles (
|
|
651
635
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
652
|
-
|
|
653
|
-
description TEXT,
|
|
636
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
654
637
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
655
638
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
656
639
|
);
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
#### 6. Create the Roles Table
|
|
660
640
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
CREATE TABLE hazo_roles (
|
|
641
|
+
-- 5. Permissions
|
|
642
|
+
CREATE TABLE hazo_permissions (
|
|
664
643
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
665
|
-
|
|
644
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
645
|
+
description TEXT,
|
|
666
646
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
667
647
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
668
648
|
);
|
|
669
|
-
```
|
|
670
649
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
```sql
|
|
674
|
-
-- Junction table linking roles to permissions
|
|
650
|
+
-- 6. Role-permission assignments (composite PK, no id column)
|
|
675
651
|
CREATE TABLE hazo_role_permissions (
|
|
676
652
|
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
677
653
|
permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
678
654
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
679
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
680
655
|
PRIMARY KEY (role_id, permission_id)
|
|
681
656
|
);
|
|
682
657
|
|
|
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 (
|
|
658
|
+
-- 7. Unified scope hierarchy (firms, divisions, departments, ... with branding)
|
|
659
|
+
CREATE TABLE hazo_scopes (
|
|
720
660
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
661
|
+
parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
721
662
|
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,
|
|
663
|
+
level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division'
|
|
664
|
+
logo_url TEXT,
|
|
665
|
+
primary_color TEXT,
|
|
666
|
+
secondary_color TEXT,
|
|
667
|
+
tagline TEXT,
|
|
668
|
+
slug TEXT, -- URL-friendly identifier
|
|
754
669
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
755
670
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
756
671
|
);
|
|
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(),
|
|
672
|
+
CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
673
|
+
CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
|
|
674
|
+
CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
675
|
+
|
|
676
|
+
-- 7a. Reserved system scopes
|
|
677
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level)
|
|
678
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
|
|
679
|
+
ON CONFLICT (id) DO NOTHING;
|
|
680
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level)
|
|
681
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
|
|
682
|
+
ON CONFLICT (id) DO NOTHING;
|
|
683
|
+
|
|
684
|
+
-- 8. User-scope membership (replaces v4.x hazo_user_roles)
|
|
685
|
+
CREATE TABLE hazo_user_scopes (
|
|
772
686
|
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,
|
|
687
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
688
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
689
|
+
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
690
|
+
status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
|
|
786
691
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
787
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
692
|
+
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
693
|
+
PRIMARY KEY (user_id, scope_id)
|
|
788
694
|
);
|
|
695
|
+
CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
696
|
+
CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
697
|
+
CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
789
698
|
|
|
790
|
-
--
|
|
791
|
-
CREATE TABLE
|
|
699
|
+
-- 9. Invitations
|
|
700
|
+
CREATE TABLE hazo_invitations (
|
|
792
701
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
793
|
-
|
|
702
|
+
email_address TEXT NOT NULL,
|
|
703
|
+
token TEXT NOT NULL UNIQUE,
|
|
704
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
705
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
706
|
+
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
707
|
+
invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
708
|
+
status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
|
|
709
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
710
|
+
accepted_at TIMESTAMP WITH TIME ZONE,
|
|
794
711
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
795
712
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
796
713
|
);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
CREATE
|
|
800
|
-
|
|
801
|
-
|
|
714
|
+
CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
715
|
+
CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
|
|
716
|
+
CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
717
|
+
CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
|
|
718
|
+
CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
719
|
+
|
|
720
|
+
-- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
|
|
721
|
+
CREATE TABLE hazo_user_relationships (
|
|
722
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
723
|
+
parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
724
|
+
child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
725
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
726
|
+
can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
|
|
727
|
+
can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
|
|
728
|
+
can_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
|
729
|
+
is_self BOOLEAN NOT NULL DEFAULT FALSE,
|
|
802
730
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
803
|
-
|
|
804
|
-
PRIMARY KEY (role_id, permission_id)
|
|
731
|
+
UNIQUE (parent_user_id, child_user_id)
|
|
805
732
|
);
|
|
806
|
-
CREATE INDEX
|
|
807
|
-
CREATE INDEX
|
|
733
|
+
CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
734
|
+
CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
808
735
|
|
|
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);
|
|
736
|
+
-- 11. Built-in firm_admin role (used when a user creates their first firm)
|
|
737
|
+
INSERT INTO hazo_roles (id, role_name)
|
|
738
|
+
VALUES (gen_random_uuid(), 'firm_admin')
|
|
739
|
+
ON CONFLICT (role_name) DO NOTHING;
|
|
819
740
|
```
|
|
820
741
|
|
|
821
742
|
### SQLite Setup (for local development)
|
|
822
743
|
|
|
823
|
-
|
|
744
|
+
The SQLite version of the schema. Equivalent to running `npx hazo_auth init-db` and identical to `src/lib/schema/sqlite_schema.ts`.
|
|
824
745
|
|
|
825
746
|
```sql
|
|
826
|
-
--
|
|
827
|
-
-- hazo_auth
|
|
828
|
-
--
|
|
747
|
+
-- ============================================================
|
|
748
|
+
-- hazo_auth canonical SQLite schema (v5.x)
|
|
749
|
+
-- ============================================================
|
|
829
750
|
|
|
830
|
-
-- Users
|
|
751
|
+
-- Users
|
|
831
752
|
CREATE TABLE IF NOT EXISTS hazo_users (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
753
|
+
id TEXT PRIMARY KEY,
|
|
754
|
+
email_address TEXT NOT NULL UNIQUE,
|
|
755
|
+
password_hash TEXT,
|
|
756
|
+
name TEXT,
|
|
757
|
+
email_verified INTEGER DEFAULT 0,
|
|
758
|
+
login_attempts INTEGER DEFAULT 0,
|
|
759
|
+
last_logon TEXT,
|
|
760
|
+
profile_picture_url TEXT,
|
|
761
|
+
profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
|
|
762
|
+
mfa_secret TEXT,
|
|
763
|
+
url_on_logon TEXT,
|
|
764
|
+
google_id TEXT UNIQUE,
|
|
765
|
+
auth_providers TEXT DEFAULT 'email',
|
|
766
|
+
user_type TEXT,
|
|
767
|
+
app_user_data TEXT,
|
|
768
|
+
status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
|
|
769
|
+
managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
770
|
+
pin_hash TEXT,
|
|
771
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
772
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
846
773
|
);
|
|
774
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
|
|
775
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
|
|
776
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
|
|
777
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
847
778
|
|
|
848
|
-
-- Refresh tokens
|
|
779
|
+
-- Refresh tokens
|
|
849
780
|
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
781
|
+
id TEXT PRIMARY KEY,
|
|
782
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
783
|
+
token TEXT NOT NULL UNIQUE,
|
|
784
|
+
token_type TEXT DEFAULT 'refresh',
|
|
785
|
+
expires_at TEXT NOT NULL,
|
|
786
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
856
787
|
);
|
|
788
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
|
|
789
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
|
|
857
790
|
|
|
858
|
-
--
|
|
859
|
-
CREATE TABLE IF NOT EXISTS
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
791
|
+
-- Roles
|
|
792
|
+
CREATE TABLE IF NOT EXISTS hazo_roles (
|
|
793
|
+
id TEXT PRIMARY KEY,
|
|
794
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
795
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
796
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
865
797
|
);
|
|
866
798
|
|
|
867
|
-
--
|
|
868
|
-
CREATE TABLE IF NOT EXISTS
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
799
|
+
-- Permissions
|
|
800
|
+
CREATE TABLE IF NOT EXISTS hazo_permissions (
|
|
801
|
+
id TEXT PRIMARY KEY,
|
|
802
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
803
|
+
description TEXT,
|
|
804
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
805
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
873
806
|
);
|
|
874
807
|
|
|
875
|
-
-- Role-
|
|
808
|
+
-- Role-permission assignments (composite PK, no id column)
|
|
876
809
|
CREATE TABLE IF NOT EXISTS hazo_role_permissions (
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
PRIMARY KEY (role_id, permission_id)
|
|
810
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
811
|
+
permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
812
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
813
|
+
PRIMARY KEY (role_id, permission_id)
|
|
882
814
|
);
|
|
883
815
|
|
|
884
|
-
--
|
|
885
|
-
CREATE TABLE IF NOT EXISTS
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
816
|
+
-- Unified scope hierarchy (firms, divisions, departments, ... with branding)
|
|
817
|
+
CREATE TABLE IF NOT EXISTS hazo_scopes (
|
|
818
|
+
id TEXT PRIMARY KEY,
|
|
819
|
+
parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
820
|
+
name TEXT NOT NULL,
|
|
821
|
+
level TEXT NOT NULL,
|
|
822
|
+
logo_url TEXT,
|
|
823
|
+
primary_color TEXT,
|
|
824
|
+
secondary_color TEXT,
|
|
825
|
+
tagline TEXT,
|
|
826
|
+
slug TEXT,
|
|
827
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
828
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
829
|
+
);
|
|
830
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
831
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
832
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
833
|
+
|
|
834
|
+
-- Reserved system scopes
|
|
835
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
836
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
|
|
837
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
838
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
|
|
839
|
+
|
|
840
|
+
-- User-scope membership (replaces v4.x hazo_user_roles)
|
|
841
|
+
CREATE TABLE IF NOT EXISTS hazo_user_scopes (
|
|
842
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
843
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
844
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
845
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
846
|
+
status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
|
|
847
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
848
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
849
|
+
PRIMARY KEY (user_id, scope_id)
|
|
850
|
+
);
|
|
851
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
852
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
853
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
854
|
+
|
|
855
|
+
-- Invitations
|
|
856
|
+
CREATE TABLE IF NOT EXISTS hazo_invitations (
|
|
857
|
+
id TEXT PRIMARY KEY,
|
|
858
|
+
email_address TEXT NOT NULL,
|
|
859
|
+
token TEXT NOT NULL UNIQUE,
|
|
860
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
861
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
862
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
863
|
+
invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
864
|
+
status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
|
|
865
|
+
expires_at TEXT NOT NULL,
|
|
866
|
+
accepted_at TEXT,
|
|
867
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
868
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
869
|
+
);
|
|
870
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
871
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
|
|
872
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
873
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
|
|
874
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
875
|
+
|
|
876
|
+
-- Managed sub-profile relationships
|
|
877
|
+
CREATE TABLE IF NOT EXISTS hazo_user_relationships (
|
|
878
|
+
id TEXT PRIMARY KEY,
|
|
879
|
+
parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
880
|
+
child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
881
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
882
|
+
can_view_progress INTEGER DEFAULT 1,
|
|
883
|
+
can_edit_profile INTEGER DEFAULT 1,
|
|
884
|
+
can_delete INTEGER DEFAULT 0,
|
|
885
|
+
is_self INTEGER DEFAULT 0,
|
|
886
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
887
|
+
UNIQUE(parent_user_id, child_user_id)
|
|
888
|
+
);
|
|
889
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
890
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
891
|
+
|
|
892
|
+
-- Built-in firm_admin role (used when a user creates their first firm)
|
|
893
|
+
INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
|
|
894
|
+
VALUES (
|
|
895
|
+
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))),
|
|
896
|
+
'firm_admin',
|
|
897
|
+
datetime('now'),
|
|
898
|
+
datetime('now')
|
|
891
899
|
);
|
|
892
900
|
```
|
|
893
901
|
|
|
@@ -2147,19 +2155,14 @@ scope_cache_max_entries = 5000
|
|
|
2147
2155
|
|
|
2148
2156
|
### Database Setup
|
|
2149
2157
|
|
|
2150
|
-
HRBAC
|
|
2158
|
+
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.
|
|
2159
|
+
|
|
2160
|
+
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
2161
|
|
|
2152
2162
|
```bash
|
|
2153
2163
|
npm run migrate migrations/009_scope_consolidation.sql
|
|
2154
2164
|
```
|
|
2155
2165
|
|
|
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
2166
|
### Using hazo_get_auth with Scope Options
|
|
2164
2167
|
|
|
2165
2168
|
When HRBAC is enabled, you can check scope access alongside permissions:
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -334,69 +334,179 @@ mkdir -p data
|
|
|
334
334
|
|
|
335
335
|
The SQLite database will be created automatically on first use if using hazo_connect's SQLite adapter.
|
|
336
336
|
|
|
337
|
+
**Recommended:** Run the canonical SQLite schema via the CLI. This is the single source of truth and creates ALL required tables (mirrors `src/lib/schema/sqlite_schema.ts`):
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npx hazo_auth init-db # Create/recreate the SQLite database with full schema
|
|
341
|
+
npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)
|
|
342
|
+
```
|
|
343
|
+
|
|
337
344
|
**Manual creation (if needed):**
|
|
338
345
|
```bash
|
|
339
|
-
# Create database with
|
|
346
|
+
# Create database with the full v5.x schema (all tables, including scopes + relationships)
|
|
340
347
|
cat << 'EOF' | sqlite3 data/hazo_auth.sqlite
|
|
348
|
+
-- ============================================================
|
|
349
|
+
-- hazo_auth canonical SQLite schema (v5.x)
|
|
350
|
+
-- Mirrors src/lib/schema/sqlite_schema.ts
|
|
351
|
+
-- ============================================================
|
|
352
|
+
|
|
353
|
+
-- Users table (status enum + managed sub-profile support)
|
|
341
354
|
CREATE TABLE IF NOT EXISTS hazo_users (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
id TEXT PRIMARY KEY,
|
|
356
|
+
email_address TEXT NOT NULL UNIQUE,
|
|
357
|
+
password_hash TEXT,
|
|
358
|
+
name TEXT,
|
|
359
|
+
email_verified INTEGER DEFAULT 0,
|
|
360
|
+
login_attempts INTEGER DEFAULT 0,
|
|
361
|
+
last_logon TEXT,
|
|
362
|
+
profile_picture_url TEXT,
|
|
363
|
+
profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
|
|
364
|
+
mfa_secret TEXT,
|
|
365
|
+
url_on_logon TEXT,
|
|
366
|
+
google_id TEXT UNIQUE,
|
|
367
|
+
auth_providers TEXT DEFAULT 'email',
|
|
368
|
+
user_type TEXT,
|
|
369
|
+
app_user_data TEXT,
|
|
370
|
+
status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
|
|
371
|
+
managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
372
|
+
pin_hash TEXT,
|
|
373
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
374
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
360
375
|
);
|
|
361
376
|
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
|
|
379
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
|
|
380
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
381
|
+
|
|
382
|
+
-- Refresh tokens (also used for password reset / email verification tokens)
|
|
362
383
|
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
id TEXT PRIMARY KEY,
|
|
385
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
386
|
+
token TEXT NOT NULL UNIQUE,
|
|
387
|
+
token_type TEXT DEFAULT 'refresh',
|
|
388
|
+
expires_at TEXT NOT NULL,
|
|
389
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
369
390
|
);
|
|
370
391
|
|
|
371
|
-
CREATE
|
|
372
|
-
|
|
373
|
-
permission_name TEXT NOT NULL UNIQUE,
|
|
374
|
-
description TEXT,
|
|
375
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
376
|
-
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
377
|
-
);
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
|
|
378
394
|
|
|
395
|
+
-- Roles
|
|
379
396
|
CREATE TABLE IF NOT EXISTS hazo_roles (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
397
|
+
id TEXT PRIMARY KEY,
|
|
398
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
399
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
400
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
384
401
|
);
|
|
385
402
|
|
|
403
|
+
-- Permissions
|
|
404
|
+
CREATE TABLE IF NOT EXISTS hazo_permissions (
|
|
405
|
+
id TEXT PRIMARY KEY,
|
|
406
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
407
|
+
description TEXT,
|
|
408
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
409
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
-- Role-permission assignments (composite PK, no id column)
|
|
386
413
|
CREATE TABLE IF NOT EXISTS hazo_role_permissions (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
PRIMARY KEY (role_id, permission_id)
|
|
414
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
415
|
+
permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
416
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
417
|
+
PRIMARY KEY (role_id, permission_id)
|
|
392
418
|
);
|
|
393
419
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
420
|
+
-- Unified scopes table (hierarchical multi-tenancy with firm branding)
|
|
421
|
+
-- v5.0+ replaces hazo_org and hazo_scopes_l1..l7 with a single self-referencing table
|
|
422
|
+
CREATE TABLE IF NOT EXISTS hazo_scopes (
|
|
423
|
+
id TEXT PRIMARY KEY,
|
|
424
|
+
parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
425
|
+
name TEXT NOT NULL,
|
|
426
|
+
level TEXT NOT NULL,
|
|
427
|
+
logo_url TEXT,
|
|
428
|
+
primary_color TEXT,
|
|
429
|
+
secondary_color TEXT,
|
|
430
|
+
tagline TEXT,
|
|
431
|
+
slug TEXT,
|
|
432
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
433
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
439
|
+
|
|
440
|
+
-- Reserved system scopes
|
|
441
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
442
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
|
|
443
|
+
|
|
444
|
+
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
445
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
|
|
446
|
+
|
|
447
|
+
-- User-scope assignments (membership model with scope-specific roles)
|
|
448
|
+
-- NOTE: replaces the old hazo_user_roles table from v4.x
|
|
449
|
+
CREATE TABLE IF NOT EXISTS hazo_user_scopes (
|
|
450
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
451
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
452
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
453
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
454
|
+
status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
|
|
455
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
456
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
457
|
+
PRIMARY KEY (user_id, scope_id)
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
461
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
462
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
463
|
+
|
|
464
|
+
-- Invitations (onboard new users into existing scopes)
|
|
465
|
+
CREATE TABLE IF NOT EXISTS hazo_invitations (
|
|
466
|
+
id TEXT PRIMARY KEY,
|
|
467
|
+
email_address TEXT NOT NULL,
|
|
468
|
+
token TEXT NOT NULL UNIQUE,
|
|
469
|
+
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
470
|
+
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
471
|
+
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
472
|
+
invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
473
|
+
status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
|
|
474
|
+
expires_at TEXT NOT NULL,
|
|
475
|
+
accepted_at TEXT,
|
|
476
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
477
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
|
|
484
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
485
|
+
|
|
486
|
+
-- Managed sub-profile relationships (parent/child accounts on shared devices)
|
|
487
|
+
CREATE TABLE IF NOT EXISTS hazo_user_relationships (
|
|
488
|
+
id TEXT PRIMARY KEY,
|
|
489
|
+
parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
490
|
+
child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
491
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
492
|
+
can_view_progress INTEGER DEFAULT 1,
|
|
493
|
+
can_edit_profile INTEGER DEFAULT 1,
|
|
494
|
+
can_delete INTEGER DEFAULT 0,
|
|
495
|
+
is_self INTEGER DEFAULT 0,
|
|
496
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
497
|
+
UNIQUE(parent_user_id, child_user_id)
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
501
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
502
|
+
|
|
503
|
+
-- Built-in firm_admin role (used when a user creates their first firm)
|
|
504
|
+
INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
|
|
505
|
+
VALUES (
|
|
506
|
+
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))),
|
|
507
|
+
'firm_admin',
|
|
508
|
+
datetime('now'),
|
|
509
|
+
datetime('now')
|
|
400
510
|
);
|
|
401
511
|
EOF
|
|
402
512
|
```
|
|
@@ -404,82 +514,53 @@ EOF
|
|
|
404
514
|
**Verify SQLite database:**
|
|
405
515
|
```bash
|
|
406
516
|
sqlite3 data/hazo_auth.sqlite ".tables"
|
|
407
|
-
# Expected
|
|
517
|
+
# Expected (9 tables):
|
|
518
|
+
# hazo_users hazo_refresh_tokens hazo_roles hazo_permissions hazo_role_permissions
|
|
519
|
+
# hazo_scopes hazo_user_scopes hazo_invitations hazo_user_relationships
|
|
408
520
|
```
|
|
409
521
|
|
|
522
|
+
> **v4.x → v5.x:** the legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables have been removed. Roles are now assigned per-scope via `hazo_user_scopes.role_id`. If upgrading, run `migrations/009_scope_consolidation.sql` first.
|
|
523
|
+
|
|
410
524
|
### Option B: PostgreSQL (Production)
|
|
411
525
|
|
|
412
|
-
Run this SQL script in your PostgreSQL database
|
|
526
|
+
Run this SQL script in your PostgreSQL database. It creates the full v5.x schema, including the unified `hazo_scopes` model, `hazo_user_scopes`, `hazo_invitations`, and `hazo_user_relationships`.
|
|
413
527
|
|
|
414
|
-
**Important:** Run the entire script in order
|
|
528
|
+
**Important:** Run the entire script in order — enum types and parent tables must be created before tables that depend on them.
|
|
415
529
|
|
|
416
530
|
```sql
|
|
417
|
-
--
|
|
531
|
+
-- ============================================================
|
|
532
|
+
-- hazo_auth canonical PostgreSQL schema (v5.x)
|
|
533
|
+
-- Single source of truth for production deployments
|
|
534
|
+
-- ============================================================
|
|
535
|
+
|
|
418
536
|
SET search_path TO public;
|
|
419
537
|
|
|
420
|
-
--
|
|
421
|
-
DROP TYPE IF EXISTS hazo_enum_profile_source_enum CASCADE;
|
|
538
|
+
-- 1. Enum types
|
|
422
539
|
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
540
|
+
CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
|
|
541
|
+
CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
|
|
542
|
+
CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
|
|
423
543
|
|
|
424
|
-
|
|
425
|
-
CREATE TYPE hazo_enum_scope_types AS ENUM (
|
|
426
|
-
'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
|
|
427
|
-
'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
DROP TYPE IF EXISTS hazo_enum_notify_chain_status CASCADE;
|
|
431
|
-
CREATE TYPE hazo_enum_notify_chain_status AS ENUM ('draft', 'published', 'inactive');
|
|
432
|
-
|
|
433
|
-
DROP TYPE IF EXISTS hazo_enum_notify_email_type CASCADE;
|
|
434
|
-
CREATE TYPE hazo_enum_notify_email_type AS ENUM ('system', 'user');
|
|
435
|
-
|
|
436
|
-
DROP TYPE IF EXISTS hazo_enum_group_type CASCADE;
|
|
437
|
-
CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
|
|
438
|
-
|
|
439
|
-
DROP TYPE IF EXISTS hazo_enum_group_role CASCADE;
|
|
440
|
-
CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
|
|
441
|
-
|
|
442
|
-
DROP TYPE IF EXISTS hazo_enum_chat_type CASCADE;
|
|
443
|
-
CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');
|
|
444
|
-
|
|
445
|
-
-- Create organization table (multi-tenancy) - MUST be created before hazo_users
|
|
446
|
-
CREATE TABLE hazo_org (
|
|
447
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
448
|
-
name TEXT NOT NULL,
|
|
449
|
-
parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
450
|
-
root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
451
|
-
user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited
|
|
452
|
-
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
453
|
-
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
454
|
-
created_by UUID, -- FK added after hazo_users exists
|
|
455
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
456
|
-
changed_by UUID -- FK added after hazo_users exists
|
|
457
|
-
);
|
|
458
|
-
CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
|
|
459
|
-
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
|
|
460
|
-
CREATE INDEX idx_hazo_org_active ON hazo_org(active);
|
|
461
|
-
CREATE INDEX idx_hazo_org_name ON hazo_org(name);
|
|
462
|
-
|
|
463
|
-
-- Create users table
|
|
544
|
+
-- 2. Users (status enum + managed sub-profile support)
|
|
464
545
|
CREATE TABLE hazo_users (
|
|
465
546
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
466
547
|
email_address TEXT NOT NULL UNIQUE,
|
|
467
548
|
password_hash TEXT, -- NULL for OAuth-only users
|
|
468
549
|
name TEXT,
|
|
469
550
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
470
|
-
status TEXT NOT NULL DEFAULT 'ACTIVE', -- 'PENDING', 'ACTIVE', or 'BLOCKED'
|
|
471
551
|
login_attempts INTEGER NOT NULL DEFAULT 0,
|
|
472
552
|
last_logon TIMESTAMP WITH TIME ZONE,
|
|
473
553
|
profile_picture_url TEXT,
|
|
474
554
|
profile_source hazo_enum_profile_source_enum,
|
|
475
555
|
mfa_secret TEXT,
|
|
476
556
|
url_on_logon TEXT,
|
|
477
|
-
user_type TEXT, -- Optional user categorization
|
|
478
|
-
app_user_data TEXT, -- Custom JSON data for consuming apps
|
|
479
557
|
google_id TEXT UNIQUE, -- Google OAuth ID
|
|
480
558
|
auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
|
|
481
|
-
|
|
482
|
-
|
|
559
|
+
user_type TEXT, -- Optional user categorization
|
|
560
|
+
app_user_data JSONB, -- Custom JSON data for consuming apps
|
|
561
|
+
status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
|
|
562
|
+
managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL, -- managed sub-profiles
|
|
563
|
+
pin_hash TEXT, -- simple PIN auth on shared devices
|
|
483
564
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
484
565
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
485
566
|
);
|
|
@@ -487,71 +568,142 @@ CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
|
|
|
487
568
|
CREATE INDEX idx_hazo_users_status ON hazo_users(status);
|
|
488
569
|
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
|
|
489
570
|
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
|
|
490
|
-
CREATE INDEX
|
|
491
|
-
CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
|
|
571
|
+
CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
|
|
492
572
|
|
|
493
|
-
--
|
|
494
|
-
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
|
|
495
|
-
FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
496
|
-
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
|
|
497
|
-
FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
498
|
-
|
|
499
|
-
-- Create refresh tokens table
|
|
573
|
+
-- 3. Refresh tokens (also used for password reset / email verification)
|
|
500
574
|
CREATE TABLE hazo_refresh_tokens (
|
|
501
575
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
502
576
|
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
503
577
|
token_hash TEXT NOT NULL,
|
|
504
|
-
token_type TEXT NOT NULL,
|
|
578
|
+
token_type TEXT NOT NULL DEFAULT 'refresh', -- 'refresh' | 'password_reset' | 'email_verification'
|
|
505
579
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
506
580
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
507
581
|
);
|
|
508
582
|
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
|
|
509
583
|
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
|
|
510
584
|
|
|
511
|
-
--
|
|
512
|
-
CREATE TABLE
|
|
585
|
+
-- 4. Roles
|
|
586
|
+
CREATE TABLE hazo_roles (
|
|
513
587
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
514
|
-
|
|
515
|
-
description TEXT,
|
|
588
|
+
role_name TEXT NOT NULL UNIQUE,
|
|
516
589
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
517
590
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
518
591
|
);
|
|
519
592
|
|
|
520
|
-
--
|
|
521
|
-
CREATE TABLE
|
|
593
|
+
-- 5. Permissions
|
|
594
|
+
CREATE TABLE hazo_permissions (
|
|
522
595
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
523
|
-
|
|
596
|
+
permission_name TEXT NOT NULL UNIQUE,
|
|
597
|
+
description TEXT,
|
|
524
598
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
525
599
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
526
600
|
);
|
|
527
601
|
|
|
528
|
-
--
|
|
602
|
+
-- 6. Role-permission assignments (composite PK, NO id column)
|
|
529
603
|
CREATE TABLE hazo_role_permissions (
|
|
530
604
|
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
531
605
|
permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
|
|
532
606
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
533
|
-
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
534
607
|
PRIMARY KEY (role_id, permission_id)
|
|
535
608
|
);
|
|
536
|
-
CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
|
|
537
|
-
CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
|
|
538
609
|
|
|
539
|
-
--
|
|
540
|
-
|
|
610
|
+
-- 7. Unified scopes (hierarchical multi-tenancy with firm branding)
|
|
611
|
+
-- v5.0+ replaces the old hazo_org and hazo_scopes_l1..l7 tables.
|
|
612
|
+
CREATE TABLE hazo_scopes (
|
|
613
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
614
|
+
parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
615
|
+
name TEXT NOT NULL,
|
|
616
|
+
level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division', 'Team'
|
|
617
|
+
logo_url TEXT,
|
|
618
|
+
primary_color TEXT,
|
|
619
|
+
secondary_color TEXT,
|
|
620
|
+
tagline TEXT,
|
|
621
|
+
slug TEXT,
|
|
622
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
623
|
+
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
624
|
+
);
|
|
625
|
+
CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
|
|
626
|
+
CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
|
|
627
|
+
CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
628
|
+
|
|
629
|
+
-- 7a. Reserved system scopes
|
|
630
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
631
|
+
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
|
|
632
|
+
ON CONFLICT (id) DO NOTHING;
|
|
633
|
+
|
|
634
|
+
INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
635
|
+
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
|
|
636
|
+
ON CONFLICT (id) DO NOTHING;
|
|
637
|
+
|
|
638
|
+
-- 8. User-scope assignments (membership model with scope-specific roles)
|
|
639
|
+
-- Replaces the v4.x hazo_user_roles table.
|
|
640
|
+
CREATE TABLE hazo_user_scopes (
|
|
541
641
|
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
642
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
643
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
542
644
|
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
645
|
+
status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
|
|
543
646
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
544
647
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
545
|
-
PRIMARY KEY (user_id,
|
|
648
|
+
PRIMARY KEY (user_id, scope_id)
|
|
649
|
+
);
|
|
650
|
+
CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
|
|
651
|
+
CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
|
|
652
|
+
CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
|
|
653
|
+
|
|
654
|
+
-- 9. Invitations (onboard new users into existing scopes)
|
|
655
|
+
CREATE TABLE hazo_invitations (
|
|
656
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
657
|
+
email_address TEXT NOT NULL,
|
|
658
|
+
token TEXT NOT NULL UNIQUE,
|
|
659
|
+
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
660
|
+
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
|
|
661
|
+
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
|
|
662
|
+
invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
|
|
663
|
+
status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
|
|
664
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
665
|
+
accepted_at TIMESTAMP WITH TIME ZONE,
|
|
666
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
667
|
+
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
668
|
+
);
|
|
669
|
+
CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
|
|
670
|
+
CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
|
|
671
|
+
CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
|
|
672
|
+
CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
|
|
673
|
+
CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
|
|
674
|
+
|
|
675
|
+
-- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
|
|
676
|
+
CREATE TABLE hazo_user_relationships (
|
|
677
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
678
|
+
parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
679
|
+
child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
680
|
+
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
681
|
+
can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
|
|
682
|
+
can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
|
|
683
|
+
can_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
|
684
|
+
is_self BOOLEAN NOT NULL DEFAULT FALSE,
|
|
685
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
686
|
+
UNIQUE (parent_user_id, child_user_id)
|
|
546
687
|
);
|
|
547
|
-
CREATE INDEX
|
|
548
|
-
CREATE INDEX
|
|
688
|
+
CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
|
|
689
|
+
CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
|
|
690
|
+
|
|
691
|
+
-- 11. Built-in firm_admin role (used when a user creates their first firm)
|
|
692
|
+
INSERT INTO hazo_roles (id, role_name, created_at, changed_at)
|
|
693
|
+
VALUES (gen_random_uuid(), 'firm_admin', NOW(), NOW())
|
|
694
|
+
ON CONFLICT (role_name) DO NOTHING;
|
|
549
695
|
```
|
|
550
696
|
|
|
697
|
+
> **Migrating from v4.x?** The legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables have been removed. Run `migrations/009_scope_consolidation.sql` against your existing database — it drops the old tables and creates the new schema in place.
|
|
698
|
+
|
|
551
699
|
**Verify PostgreSQL tables:**
|
|
552
700
|
```sql
|
|
553
|
-
SELECT table_name FROM information_schema.tables
|
|
554
|
-
|
|
701
|
+
SELECT table_name FROM information_schema.tables
|
|
702
|
+
WHERE table_schema = 'public' AND table_name LIKE 'hazo_%'
|
|
703
|
+
ORDER BY table_name;
|
|
704
|
+
-- Expected (9 tables):
|
|
705
|
+
-- hazo_invitations, hazo_permissions, hazo_refresh_tokens, hazo_role_permissions,
|
|
706
|
+
-- hazo_roles, hazo_scopes, hazo_users, hazo_user_relationships, hazo_user_scopes
|
|
555
707
|
```
|
|
556
708
|
|
|
557
709
|
**Grant access to admin user:**
|
|
@@ -565,16 +717,19 @@ GRANT USAGE ON SCHEMA public TO your_admin_user;
|
|
|
565
717
|
-- Grant all privileges on all hazo_* tables
|
|
566
718
|
GRANT ALL PRIVILEGES ON TABLE hazo_users TO your_admin_user;
|
|
567
719
|
GRANT ALL PRIVILEGES ON TABLE hazo_refresh_tokens TO your_admin_user;
|
|
568
|
-
GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO your_admin_user;
|
|
569
720
|
GRANT ALL PRIVILEGES ON TABLE hazo_roles TO your_admin_user;
|
|
721
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO your_admin_user;
|
|
570
722
|
GRANT ALL PRIVILEGES ON TABLE hazo_role_permissions TO your_admin_user;
|
|
571
|
-
GRANT ALL PRIVILEGES ON TABLE
|
|
723
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_scopes TO your_admin_user;
|
|
724
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO your_admin_user;
|
|
725
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_invitations TO your_admin_user;
|
|
726
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_user_relationships TO your_admin_user;
|
|
572
727
|
|
|
573
|
-
-- Grant usage on
|
|
728
|
+
-- Grant usage on enum types
|
|
574
729
|
GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO your_admin_user;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
730
|
+
GRANT USAGE ON TYPE hazo_enum_user_status TO your_admin_user;
|
|
731
|
+
GRANT USAGE ON TYPE hazo_enum_user_scope_status_type TO your_admin_user;
|
|
732
|
+
GRANT USAGE ON TYPE hazo_enum_invitation_status TO your_admin_user;
|
|
578
733
|
|
|
579
734
|
-- Optional: Grant privileges on future tables (if you plan to add more hazo_* tables)
|
|
580
735
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO your_admin_user;
|
|
@@ -598,7 +753,7 @@ GRANT SELECT ON TABLE hazo_users TO anon;
|
|
|
598
753
|
GRANT SELECT ON TABLE hazo_permissions TO anon;
|
|
599
754
|
GRANT SELECT ON TABLE hazo_roles TO anon;
|
|
600
755
|
GRANT SELECT ON TABLE hazo_role_permissions TO anon;
|
|
601
|
-
GRANT SELECT ON TABLE
|
|
756
|
+
GRANT SELECT ON TABLE hazo_scopes TO anon;
|
|
602
757
|
|
|
603
758
|
-- Grant full access to authenticated users (adjust based on your RLS policies)
|
|
604
759
|
GRANT ALL PRIVILEGES ON TABLE hazo_users TO authenticated;
|
|
@@ -606,30 +761,39 @@ GRANT ALL PRIVILEGES ON TABLE hazo_refresh_tokens TO authenticated;
|
|
|
606
761
|
GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO authenticated;
|
|
607
762
|
GRANT ALL PRIVILEGES ON TABLE hazo_roles TO authenticated;
|
|
608
763
|
GRANT ALL PRIVILEGES ON TABLE hazo_role_permissions TO authenticated;
|
|
609
|
-
GRANT ALL PRIVILEGES ON TABLE
|
|
764
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_scopes TO authenticated;
|
|
765
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO authenticated;
|
|
766
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_invitations TO authenticated;
|
|
767
|
+
GRANT ALL PRIVILEGES ON TABLE hazo_user_relationships TO authenticated;
|
|
610
768
|
|
|
611
|
-
-- Grant usage on enum
|
|
769
|
+
-- Grant usage on enum types
|
|
612
770
|
GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO anon, authenticated;
|
|
771
|
+
GRANT USAGE ON TYPE hazo_enum_user_status TO anon, authenticated;
|
|
772
|
+
GRANT USAGE ON TYPE hazo_enum_user_scope_status_type TO anon, authenticated;
|
|
773
|
+
GRANT USAGE ON TYPE hazo_enum_invitation_status TO anon, authenticated;
|
|
613
774
|
```
|
|
614
775
|
|
|
615
776
|
**Checklist:**
|
|
616
777
|
- [ ] Database created (SQLite file or PostgreSQL)
|
|
617
778
|
- [ ] All enum types created (PostgreSQL only):
|
|
618
779
|
- [ ] `hazo_enum_profile_source_enum`
|
|
619
|
-
- [ ] `
|
|
620
|
-
- [ ] `
|
|
621
|
-
- [ ] `
|
|
622
|
-
|
|
623
|
-
- [ ] `
|
|
624
|
-
- [ ] `hazo_enum_chat_type`
|
|
625
|
-
- [ ] All core tables exist:
|
|
626
|
-
- [ ] `hazo_org` (multi-tenancy - must be created before hazo_users)
|
|
627
|
-
- [ ] `hazo_users` (with status, google_id, auth_providers, app_user_data, org_id, root_org_id, user_type fields)
|
|
780
|
+
- [ ] `hazo_enum_user_status`
|
|
781
|
+
- [ ] `hazo_enum_user_scope_status_type`
|
|
782
|
+
- [ ] `hazo_enum_invitation_status`
|
|
783
|
+
- [ ] All core tables exist (9 total):
|
|
784
|
+
- [ ] `hazo_users` (with `status`, `google_id`, `auth_providers`, `app_user_data`, `user_type`, `managed_by_user_id`, `pin_hash` fields)
|
|
628
785
|
- [ ] `hazo_refresh_tokens`
|
|
629
|
-
- [ ] `hazo_permissions`
|
|
630
786
|
- [ ] `hazo_roles`
|
|
631
|
-
- [ ] `
|
|
632
|
-
- [ ] `
|
|
787
|
+
- [ ] `hazo_permissions`
|
|
788
|
+
- [ ] `hazo_role_permissions` (composite PK, no `id` column)
|
|
789
|
+
- [ ] `hazo_scopes` (unified hierarchy with branding + `slug`)
|
|
790
|
+
- [ ] `hazo_user_scopes` (composite PK on `user_id`, `scope_id`)
|
|
791
|
+
- [ ] `hazo_invitations`
|
|
792
|
+
- [ ] `hazo_user_relationships` (managed sub-profile parent/child links)
|
|
793
|
+
- [ ] Reserved system scopes inserted:
|
|
794
|
+
- [ ] `00000000-0000-0000-0000-000000000000` (Super Admin)
|
|
795
|
+
- [ ] `00000000-0000-0000-0000-000000000001` (System / non-multi-tenancy default)
|
|
796
|
+
- [ ] `firm_admin` role inserted into `hazo_roles`
|
|
633
797
|
|
|
634
798
|
---
|
|
635
799
|
|
|
@@ -1439,7 +1603,9 @@ application_permission_list_defaults = admin_user_management,admin_role_manageme
|
|
|
1439
1603
|
|
|
1440
1604
|
### Step 7.3: Create HRBAC Database Tables
|
|
1441
1605
|
|
|
1442
|
-
**
|
|
1606
|
+
> **Note (v5.0+):** `hazo_scopes`, `hazo_user_scopes`, and `hazo_invitations` are now part of the **core schema** in Phase 3. If you ran the canonical SQLite or PostgreSQL script there (or `npx hazo_auth init-db`), these tables already exist — you can skip ahead to Step 7.4. The scripts below are kept for upgrades from v4.x or for partial recovery.
|
|
1607
|
+
|
|
1608
|
+
**Upgrading from v4.x?** Run the consolidation migration which drops the legacy tables (`hazo_org`, `hazo_scopes_l1..l7`, `hazo_user_roles`) and creates the new schema:
|
|
1443
1609
|
|
|
1444
1610
|
```bash
|
|
1445
1611
|
npm run migrate migrations/009_scope_consolidation.sql
|
package/package.json
CHANGED