Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,10 @@ dev: docker-check
@printf " │ $(BOLD)Gateway API:$(RESET) http://localhost:10101 │\n"
@printf " │ $(BOLD)Portal UI:$(RESET) http://localhost:10104 │\n"
@printf " │ │\n"
@printf " │ $(BOLD)Demo Account$(RESET) │\n"
@printf " │ Email: demo@weprodev.com │\n"
@printf " │ Password: secret │\n"
@printf " │ $(BOLD)Demo accounts$(RESET) (password: secret) │\n"
@printf " │ admin: demo@weprodev.com (workspace admin) │\n"
@printf " │ member: member@weprodev.com (workspace member) │\n"
@printf " │ viewer: viewer@weprodev.com (workspace viewer) │\n"
@printf " │ │\n"
@printf " ╰───────────────────────────────────────────────────╯\n"
@printf "\n"
Expand All @@ -329,15 +330,15 @@ db-init: docker-check
@docker compose exec -T -e POSTGRES_USER=gateway -e POSTGRES_DB=gateway db \
bash /docker-entrypoint-initdb.d/init-db.sh
@docker compose restart gateway
@printf "$(GREEN)✅ Database ready — gateway will apply seeds on startup (demo@weprodev.com / secret)$(RESET)\n"
@printf "$(GREEN)✅ Database ready — gateway will apply seeds on startup (demo accounts: password secret)$(RESET)\n"
@printf "\n"

## Re-apply SQL seeds by restarting the gateway (seeds run on every startup)
seed-demo: docker-check
@printf "\n"
@printf "$(BOLD)$(CYAN)🌱 Re-applying database seeds (gateway restart)...$(RESET)\n"
@docker compose restart gateway
@printf "$(GREEN)✅ Seeds applied on gateway startup (demo@weprodev.com / secret)$(RESET)\n"
@printf "$(GREEN)✅ Seeds applied on gateway startup (demo accounts: password secret)$(RESET)\n"
@printf "\n"


Expand Down
11 changes: 8 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,18 @@ make dev
| Portal UI | http://localhost:10104 |
| Gateway API | http://localhost:10101 |

### Demo account
### Demo accounts

Seeded on first DB init. Reset anytime: `make seed-demo`.
Seeded on first DB init. Reset anytime: `make seed-demo`. All portal passwords are `secret`.

| Role | Email | Demo workspace role |
|------|-------|---------------------|
| Admin | `demo@weprodev.com` | admin |
| Member | `member@weprodev.com` | member |
| Viewer | `viewer@weprodev.com` | viewer |

| | |
|--|--|
| Portal | `demo@weprodev.com` / `secret` |
| API key | `demo-client-id` / `demo-secret` |
| Workspace ID | `00000000-0000-0000-0000-000000000001` |

Expand Down
57 changes: 31 additions & 26 deletions database/migrations/20260318000000_init_gateway.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,21 @@ CREATE INDEX idx_workspaces_active_list
WHERE status = 'active';

-- ==============================================================================
-- 3. ROLES TABLE (RBAC)
-- 3. ROLES TABLE (wpd-gogate RBAC — global role catalog)
-- ==============================================================================

CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
guard_name TEXT NOT NULL DEFAULT 'msg_web',
display_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
guard_name TEXT NOT NULL DEFAULT 'msg_web',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT roles_name_guard_unique UNIQUE (name, guard_name)
);

CREATE INDEX idx_roles_guard_name ON roles (guard_name);

CREATE TRIGGER trg_roles_set_updated_at
BEFORE UPDATE ON roles
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
Expand Down Expand Up @@ -393,25 +394,27 @@ CREATE INDEX idx_workspace_access_audits_actor_user_id
WHERE actor_user_id IS NOT NULL;

-- ==============================================================================
-- 16. PERMISSIONS TABLE (RBAC Engine)
-- 16. PERMISSIONS TABLE (wpd-gogate RBAC)
-- ==============================================================================

CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
guard_name TEXT NOT NULL DEFAULT 'msg_web',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
guard_name TEXT NOT NULL DEFAULT 'msg_web',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT permissions_name_guard_unique UNIQUE (name, guard_name)
);

CREATE INDEX idx_permissions_guard_name ON permissions (guard_name);

CREATE TRIGGER trg_permissions_set_updated_at
BEFORE UPDATE ON permissions
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

-- ==============================================================================
-- 17. ROLE_HAS_PERMISSIONS JOIN TABLE (RBAC Engine)
-- 17. ROLE_HAS_PERMISSIONS (wpd-gogate RBAC)
-- ==============================================================================

CREATE TABLE role_has_permissions (
Expand All @@ -421,32 +424,34 @@ CREATE TABLE role_has_permissions (
PRIMARY KEY (permission_id, role_id)
);

CREATE INDEX idx_role_has_permissions_role_id ON role_has_permissions (role_id);

-- ==============================================================================
-- 18. MODEL_HAS_ROLES TABLE (Polymorphic RBAC mappings)
-- 18. MODEL_HAS_ROLES (wpd-gogate polymorphic role assignments; team_id = workspace)
-- ==============================================================================

CREATE TABLE model_has_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
model_type TEXT NOT NULL,
model_id UUID NOT NULL,
team_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
model_type TEXT NOT NULL,
model_id UUID NOT NULL,
team_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,

CONSTRAINT model_has_roles_unique UNIQUE NULLS NOT DISTINCT (role_id, model_id, model_type, team_id)
);

CREATE INDEX idx_model_has_roles_lookup ON model_has_roles (model_id, model_type, team_id);

-- ==============================================================================
-- 19. MODEL_HAS_PERMISSIONS TABLE (Polymorphic RBAC mappings)
-- 19. MODEL_HAS_PERMISSIONS (wpd-gogate direct permission overrides; team_id = workspace)
-- ==============================================================================

CREATE TABLE model_has_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
model_type TEXT NOT NULL,
model_id UUID NOT NULL,
team_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
model_type TEXT NOT NULL,
model_id UUID NOT NULL,
team_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,

CONSTRAINT model_has_permissions_unique UNIQUE NULLS NOT DISTINCT (permission_id, model_id, model_type, team_id)
);
Expand Down
97 changes: 63 additions & 34 deletions database/seeds/001_seed_permissions.sql
Original file line number Diff line number Diff line change
@@ -1,56 +1,85 @@
-- Default RBAC Permissions & Role Bindings for WPD Message Gateway
-- Default RBAC roles, permissions, and mappings for WPD Message Gateway.
-- Schema and assignment model follow wpd-gogate (global roles; workspace scope via team_id).
-- Guard name must match domain.RBACGuardName and gogate.DefaultGuardName in wire.go.

-- 1. Insert default roles
INSERT INTO roles (id, name, display_name, guard_name)
-- 1. Global workspace roles (available to every workspace via model_has_roles.team_id)
INSERT INTO roles (name, guard_name)
VALUES
('00000000-0000-0000-0000-000000000020', 'admin', 'Admin', 'msg_web'),
('00000000-0000-0000-0000-000000000021', 'member', 'Member', 'msg_web')
('admin', 'msg_web'),
('member', 'msg_web'),
('viewer', 'msg_web')
ON CONFLICT (name, guard_name) DO NOTHING;

-- 2. Insert permissions
INSERT INTO permissions (id, name, guard_name)
-- 2. Portal permissions
INSERT INTO permissions (name, guard_name)
VALUES
(gen_random_uuid(), 'workspaces.read', 'msg_web'),
(gen_random_uuid(), 'workspaces.write', 'msg_web'),
(gen_random_uuid(), 'members.read', 'msg_web'),
(gen_random_uuid(), 'members.write', 'msg_web'),
(gen_random_uuid(), 'apikeys.read', 'msg_web'),
(gen_random_uuid(), 'apikeys.write', 'msg_web'),
(gen_random_uuid(), 'logs.read', 'msg_web'),
(gen_random_uuid(), 'integrations.read', 'msg_web'),
(gen_random_uuid(), 'integrations.write', 'msg_web'),
(gen_random_uuid(), 'templates.read', 'msg_web'),
(gen_random_uuid(), 'templates.write', 'msg_web'),
(gen_random_uuid(), 'settings.read', 'msg_web'),
(gen_random_uuid(), 'settings.write', 'msg_web'),
(gen_random_uuid(), 'invitations.read', 'msg_web'),
(gen_random_uuid(), 'invitations.write', 'msg_web'),
(gen_random_uuid(), 'inbox.write', 'msg_web')
('workspaces.read', 'msg_web'),
('workspaces.write', 'msg_web'),
('members.read', 'msg_web'),
('members.write', 'msg_web'),
('apikeys.read', 'msg_web'),
('apikeys.write', 'msg_web'),
('logs.read', 'msg_web'),
('integrations.read', 'msg_web'),
('integrations.write', 'msg_web'),
('templates.read', 'msg_web'),
('templates.write', 'msg_web'),
('settings.read', 'msg_web'),
('settings.write', 'msg_web'),
('invitations.read', 'msg_web'),
('invitations.write', 'msg_web'),
('inbox.write', 'msg_web')
ON CONFLICT (name, guard_name) DO NOTHING;

-- 3. Bind permissions to 'admin' role
INSERT INTO role_has_permissions (permission_id, role_id)
SELECT p.id, r.id
FROM permissions p
CROSS JOIN roles r
WHERE r.name = 'admin'
-- 3. Admin — full access
INSERT INTO role_has_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
CROSS JOIN permissions p
WHERE r.name = 'admin' AND r.guard_name = 'msg_web' AND p.guard_name = 'msg_web'
ON CONFLICT (permission_id, role_id) DO NOTHING;

-- 4. Bind permissions to 'member' role (read-only + inbox.write)
INSERT INTO role_has_permissions (permission_id, role_id)
SELECT p.id, r.id
FROM permissions p
CROSS JOIN roles r
-- 4. Member — manage workspace resources; cannot change workspace, members, or invitations
INSERT INTO role_has_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
CROSS JOIN permissions p
WHERE r.name = 'member'
AND r.guard_name = 'msg_web'
AND p.guard_name = 'msg_web'
AND p.name IN (
'workspaces.read',
'members.read',
'apikeys.read',
'apikeys.write',
'logs.read',
'integrations.read',
'integrations.write',
'templates.read',
'templates.write',
'settings.read',
'settings.write',
'invitations.read',
'inbox.write'
)
ON CONFLICT (permission_id, role_id) DO NOTHING;

-- 5. Viewer — read-only
INSERT INTO role_has_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
CROSS JOIN permissions p
WHERE r.name = 'viewer'
AND r.guard_name = 'msg_web'
AND p.guard_name = 'msg_web'
AND p.name IN (
'workspaces.read',
'members.read',
'apikeys.read',
'logs.read',
'integrations.read',
'templates.read',
'settings.read',
'invitations.read'
)
ON CONFLICT (permission_id, role_id) DO NOTHING;
88 changes: 64 additions & 24 deletions database/seeds/004_demo_workspace.sql
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
-- Demo workspace (idempotent — safe to re-run via init-db.sh, run_migrations.sh, or make seed-demo).
-- Portal: demo@weprodev.com / secret
-- API key: demo-client-id / demo-secret
-- Requires 001_seed_permissions.sql and 002_seed_providers.sql first.
--
-- Portal accounts (password for all: secret)
-- demo@weprodev.com → admin on Demo Workspace
-- member@weprodev.com → member on Demo Workspace
-- viewer@weprodev.com → viewer on Demo Workspace
--
-- API key: demo-client-id / demo-secret
-- Workspace: demo (00000000-0000-0000-0000-000000000001)

-- Drop conflicting rows so fixed demo UUIDs stay stable after manual sign-up.
-- Fixed demo UUIDs
-- workspace : 00000000-0000-0000-0000-000000000001
-- admin user: 00000000-0000-0000-0000-000000000010
-- member : 00000000-0000-0000-0000-000000000011
-- viewer : 00000000-0000-0000-0000-000000000012

-- bcrypt hash for password "secret" (cost 14)
-- Remove conflicting sign-ups so fixed UUIDs stay stable.
DELETE FROM users
WHERE email = 'demo@weprodev.com'
AND id <> '00000000-0000-0000-0000-000000000010';
WHERE email IN ('demo@weprodev.com', 'member@weprodev.com', 'viewer@weprodev.com')
AND id NOT IN (
'00000000-0000-0000-0000-000000000010',
'00000000-0000-0000-0000-000000000011',
'00000000-0000-0000-0000-000000000012'
);

DELETE FROM workspaces
WHERE slug = 'demo'
AND id <> '00000000-0000-0000-0000-000000000001';

INSERT INTO users (id, email, password_hash, first_name, last_name, email_verified)
VALUES (
'00000000-0000-0000-0000-000000000010',
'demo@weprodev.com',
'$2a$14$fBL/4GbbqSCMTVOYotuUa.Qx0DwnRMpkZaOHxkD3h1X4gMESUhjD.',
'Demo',
'User',
true
)
VALUES
(
'00000000-0000-0000-0000-000000000010',
'demo@weprodev.com',
'$2a$14$fBL/4GbbqSCMTVOYotuUa.Qx0DwnRMpkZaOHxkD3h1X4gMESUhjD.',
'Demo',
'Admin',
true
),
(
'00000000-0000-0000-0000-000000000011',
'member@weprodev.com',
'$2a$14$fBL/4GbbqSCMTVOYotuUa.Qx0DwnRMpkZaOHxkD3h1X4gMESUhjD.',
'Demo',
'Member',
true
),
(
'00000000-0000-0000-0000-000000000012',
'viewer@weprodev.com',
'$2a$14$fBL/4GbbqSCMTVOYotuUa.Qx0DwnRMpkZaOHxkD3h1X4gMESUhjD.',
'Demo',
'Viewer',
true
)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
Expand All @@ -46,22 +80,28 @@ ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
is_private = EXCLUDED.is_private;

-- Workspace membership + wpd-gogate role assignments (admin, member, viewer)
INSERT INTO workspace_members (workspace_id, user_id, role_id)
VALUES (
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000010',
'00000000-0000-0000-0000-000000000020'
)
SELECT '00000000-0000-0000-0000-000000000001', seed.user_id, r.id
FROM (
VALUES
('00000000-0000-0000-0000-000000000010'::uuid, 'admin'),
('00000000-0000-0000-0000-000000000011'::uuid, 'member'),
('00000000-0000-0000-0000-000000000012'::uuid, 'viewer')
) AS seed(user_id, role_name)
JOIN roles r ON r.name = seed.role_name AND r.guard_name = 'msg_web'
ON CONFLICT (workspace_id, user_id) DO UPDATE SET
role_id = EXCLUDED.role_id;

INSERT INTO model_has_roles (role_id, model_type, model_id, team_id)
VALUES (
'00000000-0000-0000-0000-000000000020',
'users',
'00000000-0000-0000-0000-000000000010',
'00000000-0000-0000-0000-000000000001'
)
SELECT r.id, 'users', seed.user_id, '00000000-0000-0000-0000-000000000001'
FROM (
VALUES
('00000000-0000-0000-0000-000000000010'::uuid, 'admin'),
('00000000-0000-0000-0000-000000000011'::uuid, 'member'),
('00000000-0000-0000-0000-000000000012'::uuid, 'viewer')
) AS seed(user_id, role_name)
JOIN roles r ON r.name = seed.role_name AND r.guard_name = 'msg_web'
ON CONFLICT (role_id, model_id, model_type, team_id) DO NOTHING;

INSERT INTO workspace_channels (workspace_id, channel_type, enabled)
Expand Down
Loading
Loading