@varshylinc/team-management 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +18 -0
- package/CHANGELOG.md +159 -0
- package/LICENSE +6 -0
- package/README.md +97 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server/crypto.d.ts +6 -0
- package/dist/server/crypto.d.ts.map +1 -0
- package/dist/server/crypto.js +42 -0
- package/dist/server/crypto.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +114 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware/require-membership.d.ts +10 -0
- package/dist/server/middleware/require-membership.d.ts.map +1 -0
- package/dist/server/middleware/require-membership.js +33 -0
- package/dist/server/middleware/require-membership.js.map +1 -0
- package/dist/server/middleware/require-role.d.ts +4 -0
- package/dist/server/middleware/require-role.d.ts.map +1 -0
- package/dist/server/middleware/require-role.js +16 -0
- package/dist/server/middleware/require-role.js.map +1 -0
- package/dist/server/middleware/require-super-admin.d.ts +5 -0
- package/dist/server/middleware/require-super-admin.d.ts.map +1 -0
- package/dist/server/middleware/require-super-admin.js +27 -0
- package/dist/server/middleware/require-super-admin.js.map +1 -0
- package/dist/server/migrations/0001_create_tm_schema_migrations.sql +13 -0
- package/dist/server/migrations/0002_create_tm_organizations.sql +14 -0
- package/dist/server/migrations/0003_create_tm_memberships.sql +24 -0
- package/dist/server/migrations/0004_create_tm_invitations.sql +22 -0
- package/dist/server/migrations/0005_create_tm_audit_events.sql +17 -0
- package/dist/server/migrations/0006_create_tm_email_change_requests.sql +13 -0
- package/dist/server/migrations/0007_create_tm_ownership_transfers.sql +22 -0
- package/dist/server/migrations/0008_create_tm_super_admins.sql +8 -0
- package/dist/server/migrations/0009_create_tm_password_reset_requests.sql +9 -0
- package/dist/server/migrations/0010_create_tm_shared_access.sql +8 -0
- package/dist/server/migrations/0011_seed_super_admin.sql +15 -0
- package/dist/server/migrations/0012_create_tm_user_locks.sql +7 -0
- package/dist/server/routes/admin.routes.d.ts +5 -0
- package/dist/server/routes/admin.routes.d.ts.map +1 -0
- package/dist/server/routes/admin.routes.js +262 -0
- package/dist/server/routes/admin.routes.js.map +1 -0
- package/dist/server/routes/audit.routes.d.ts +5 -0
- package/dist/server/routes/audit.routes.d.ts.map +1 -0
- package/dist/server/routes/audit.routes.js +70 -0
- package/dist/server/routes/audit.routes.js.map +1 -0
- package/dist/server/routes/health.routes.d.ts +8 -0
- package/dist/server/routes/health.routes.d.ts.map +1 -0
- package/dist/server/routes/health.routes.js +39 -0
- package/dist/server/routes/health.routes.js.map +1 -0
- package/dist/server/routes/invitations.routes.d.ts +5 -0
- package/dist/server/routes/invitations.routes.d.ts.map +1 -0
- package/dist/server/routes/invitations.routes.js +232 -0
- package/dist/server/routes/invitations.routes.js.map +1 -0
- package/dist/server/routes/me.routes.d.ts +5 -0
- package/dist/server/routes/me.routes.d.ts.map +1 -0
- package/dist/server/routes/me.routes.js +188 -0
- package/dist/server/routes/me.routes.js.map +1 -0
- package/dist/server/routes/orgs.routes.d.ts +5 -0
- package/dist/server/routes/orgs.routes.d.ts.map +1 -0
- package/dist/server/routes/orgs.routes.js +371 -0
- package/dist/server/routes/orgs.routes.js.map +1 -0
- package/dist/server/routes/transfer.routes.d.ts +5 -0
- package/dist/server/routes/transfer.routes.d.ts.map +1 -0
- package/dist/server/routes/transfer.routes.js +108 -0
- package/dist/server/routes/transfer.routes.js.map +1 -0
- package/dist/server/services/audit.service.d.ts +20 -0
- package/dist/server/services/audit.service.d.ts.map +1 -0
- package/dist/server/services/audit.service.js +23 -0
- package/dist/server/services/audit.service.js.map +1 -0
- package/dist/server/services/email-change.service.d.ts +16 -0
- package/dist/server/services/email-change.service.d.ts.map +1 -0
- package/dist/server/services/email-change.service.js +107 -0
- package/dist/server/services/email-change.service.js.map +1 -0
- package/dist/server/services/invitations.service.d.ts +41 -0
- package/dist/server/services/invitations.service.d.ts.map +1 -0
- package/dist/server/services/invitations.service.js +214 -0
- package/dist/server/services/invitations.service.js.map +1 -0
- package/dist/server/services/memberships.service.d.ts +27 -0
- package/dist/server/services/memberships.service.d.ts.map +1 -0
- package/dist/server/services/memberships.service.js +69 -0
- package/dist/server/services/memberships.service.js.map +1 -0
- package/dist/server/services/organizations.service.d.ts +19 -0
- package/dist/server/services/organizations.service.d.ts.map +1 -0
- package/dist/server/services/organizations.service.js +61 -0
- package/dist/server/services/organizations.service.js.map +1 -0
- package/dist/server/services/ownership.service.d.ts +19 -0
- package/dist/server/services/ownership.service.d.ts.map +1 -0
- package/dist/server/services/ownership.service.js +102 -0
- package/dist/server/services/ownership.service.js.map +1 -0
- package/dist/server/services/password-reset.service.d.ts +12 -0
- package/dist/server/services/password-reset.service.d.ts.map +1 -0
- package/dist/server/services/password-reset.service.js +54 -0
- package/dist/server/services/password-reset.service.js.map +1 -0
- package/dist/server/services/super-admin.service.d.ts +59 -0
- package/dist/server/services/super-admin.service.d.ts.map +1 -0
- package/dist/server/services/super-admin.service.js +187 -0
- package/dist/server/services/super-admin.service.js.map +1 -0
- package/dist/server/types.d.ts +186 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +6 -0
- package/dist/server/types.js.map +1 -0
- package/dist/shared/types.d.ts +23 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +6 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +56 -0
- package/src/client/api.ts +314 -0
- package/src/client/components/AuditEventRow.tsx +59 -0
- package/src/client/components/CascadePreview.tsx +36 -0
- package/src/client/components/DangerZoneCard.tsx +103 -0
- package/src/client/components/InvitationCodeDisplay.tsx +48 -0
- package/src/client/components/InviteForm.tsx +77 -0
- package/src/client/components/MemberRow.tsx +69 -0
- package/src/client/components/PendingTransferBanner.tsx +98 -0
- package/src/client/components/PlaceholderCard.tsx +26 -0
- package/src/client/components/RoleBadge.tsx +26 -0
- package/src/client/components/RoleSelect.tsx +35 -0
- package/src/client/hooks/.gitkeep +0 -0
- package/src/client/hooks/useCurrentMembership.ts +24 -0
- package/src/client/hooks/useMembers.ts +24 -0
- package/src/client/hooks/usePendingInvitations.ts +24 -0
- package/src/client/hooks/usePendingTransfer.ts +27 -0
- package/src/client/index.ts +80 -0
- package/src/client/pages/AuditLogPage.tsx +164 -0
- package/src/client/pages/EmailChangePage.tsx +144 -0
- package/src/client/pages/InvitationAcceptPage.tsx +163 -0
- package/src/client/pages/InvitationCodePage.tsx +108 -0
- package/src/client/pages/MembersPage.tsx +290 -0
- package/src/client/pages/OrgSettingsPage.tsx +185 -0
- package/src/client/pages/OwnershipTransferPage.tsx +163 -0
- package/src/client/pages/PasswordResetPage.tsx +104 -0
- package/src/client/pages/PasswordResetRequestPage.tsx +71 -0
- package/src/client/pages/PlaceholderPage.tsx +20 -0
- package/src/client/pages/SuperAdminDashboard.tsx +401 -0
- package/src/client/types.ts +78 -0
- package/src/index.ts +24 -0
- package/src/server/crypto.ts +47 -0
- package/src/server/index.ts +167 -0
- package/src/server/middleware/require-membership.ts +48 -0
- package/src/server/middleware/require-role.ts +19 -0
- package/src/server/middleware/require-super-admin.ts +32 -0
- package/src/server/migrations/0001_create_tm_schema_migrations.sql +13 -0
- package/src/server/migrations/0002_create_tm_organizations.sql +14 -0
- package/src/server/migrations/0003_create_tm_memberships.sql +24 -0
- package/src/server/migrations/0004_create_tm_invitations.sql +22 -0
- package/src/server/migrations/0005_create_tm_audit_events.sql +17 -0
- package/src/server/migrations/0006_create_tm_email_change_requests.sql +13 -0
- package/src/server/migrations/0007_create_tm_ownership_transfers.sql +22 -0
- package/src/server/migrations/0008_create_tm_super_admins.sql +8 -0
- package/src/server/migrations/0009_create_tm_password_reset_requests.sql +9 -0
- package/src/server/migrations/0010_create_tm_shared_access.sql +8 -0
- package/src/server/migrations/0011_seed_super_admin.sql +15 -0
- package/src/server/migrations/0012_create_tm_user_locks.sql +7 -0
- package/src/server/routes/admin.routes.ts +208 -0
- package/src/server/routes/audit.routes.ts +93 -0
- package/src/server/routes/health.routes.ts +46 -0
- package/src/server/routes/invitations.routes.ts +252 -0
- package/src/server/routes/me.routes.ts +143 -0
- package/src/server/routes/orgs.routes.ts +428 -0
- package/src/server/routes/transfer.routes.ts +110 -0
- package/src/server/services/.gitkeep +0 -0
- package/src/server/services/audit.service.ts +49 -0
- package/src/server/services/email-change.service.ts +178 -0
- package/src/server/services/invitations.service.ts +316 -0
- package/src/server/services/memberships.service.ts +129 -0
- package/src/server/services/organizations.service.ts +110 -0
- package/src/server/services/ownership.service.ts +170 -0
- package/src/server/services/password-reset.service.ts +94 -0
- package/src/server/services/super-admin.service.ts +321 -0
- package/src/server/sql/.gitkeep +0 -0
- package/src/server/types.ts +145 -0
- package/src/shared/types.ts +24 -0
- package/tests/integration/audit-fires.test.ts +288 -0
- package/tests/integration/cascade-preview.test.ts +157 -0
- package/tests/integration/email-change.test.ts +190 -0
- package/tests/integration/feature-flags.test.ts +213 -0
- package/tests/integration/invitations-code.test.ts +218 -0
- package/tests/integration/invitations-expiry.test.ts +216 -0
- package/tests/integration/invitations-resend.test.ts +241 -0
- package/tests/integration/invitations-revoke.test.ts +226 -0
- package/tests/integration/invitations-switch-org.test.ts +156 -0
- package/tests/integration/invitations-token.test.ts +221 -0
- package/tests/integration/migrations.test.ts +119 -0
- package/tests/integration/only-owner-protections.test.ts +130 -0
- package/tests/integration/org-lifecycle.test.ts +169 -0
- package/tests/integration/ownership-transfer-cancel.test.ts +171 -0
- package/tests/integration/ownership-transfer-expire.test.ts +171 -0
- package/tests/integration/ownership-transfer-happy.test.ts +184 -0
- package/tests/integration/ownership-transfer-locks.test.ts +146 -0
- package/tests/integration/password-reset.test.ts +200 -0
- package/tests/integration/super-admin-actions.test.ts +180 -0
- package/tests/integration/super-admin-restrictions.test.ts +209 -0
- package/tests/setup/global-setup.ts +20 -0
- package/tests/unit/adapter-shape.test.ts +330 -0
- package/tests/unit/role-permissions.test.ts +236 -0
- package/tests/unit/validation.test.ts +304 -0
- package/tsconfig.client.json +13 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function requireSuperAdmin(pool, adapter, flags) {
|
|
2
|
+
return async (req, res, next) => {
|
|
3
|
+
if (!flags.enableSuperAdmin) {
|
|
4
|
+
res.status(404).json({ error: 'Not found' });
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const userId = await adapter.getCurrentUserId(req);
|
|
9
|
+
if (!userId) {
|
|
10
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const result = await pool.query(`SELECT user_id FROM tm_super_admins WHERE user_id = $1 AND revoked_at IS NULL`, [userId]);
|
|
14
|
+
if (result.rows.length === 0) {
|
|
15
|
+
res.status(403).json({ error: 'Super-admin access required' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
req.superAdminUserId = userId;
|
|
19
|
+
next();
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
adapter.logger.error('[requireSuperAdmin]', { error: e.message });
|
|
23
|
+
res.status(500).json({ error: 'Authorization check failed' });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=require-super-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-super-admin.js","sourceRoot":"","sources":["../../../src/server/middleware/require-super-admin.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,iBAAiB,CAAC,IAAU,EAAE,OAA4B,EAAE,KAAiC;IAC3G,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAiB,EAAE;QAC9E,IAAI,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;YAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;gBAC3D,OAAO;YACT,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B,+EAA+E,EAC/E,CAAC,MAAM,CAAC,CACT,CAAC;YACF,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,6BAA6B,EAAE,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YACA,GAA8C,CAAC,gBAAgB,GAAG,MAAM,CAAC;YAC1E,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Migration: 0001_create_tm_schema_migrations
|
|
2
|
+
-- Creates the ledger table that tracks which team-management
|
|
3
|
+
-- migrations have been applied. All tm_* tables are owned by
|
|
4
|
+
-- the team-management module — host products never query them directly.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS tm_schema_migrations (
|
|
7
|
+
id SERIAL PRIMARY KEY,
|
|
8
|
+
migration VARCHAR(255) NOT NULL UNIQUE,
|
|
9
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_tm_schema_migrations_migration
|
|
13
|
+
ON tm_schema_migrations (migration);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_organizations (
|
|
2
|
+
id SERIAL PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
slug TEXT NOT NULL UNIQUE,
|
|
5
|
+
owner_user_id INTEGER NOT NULL, -- denormalized for quick lookup
|
|
6
|
+
settings JSONB NOT NULL DEFAULT '{}',
|
|
7
|
+
deleted_at TIMESTAMPTZ,
|
|
8
|
+
delete_scheduled_for TIMESTAMPTZ,
|
|
9
|
+
deleted_by_user_id INTEGER,
|
|
10
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
11
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
12
|
+
);
|
|
13
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tm_orgs_slug ON tm_organizations(slug) WHERE deleted_at IS NULL;
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_tm_orgs_owner ON tm_organizations(owner_user_id);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- If the type already exists, ignore
|
|
2
|
+
DO $$ BEGIN
|
|
3
|
+
CREATE TYPE tm_role AS ENUM ('owner', 'admin', 'member', 'viewer');
|
|
4
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
5
|
+
END $$;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS tm_memberships (
|
|
8
|
+
id SERIAL PRIMARY KEY,
|
|
9
|
+
org_id INTEGER NOT NULL REFERENCES tm_organizations(id) ON DELETE CASCADE,
|
|
10
|
+
user_id INTEGER NOT NULL,
|
|
11
|
+
role tm_role NOT NULL DEFAULT 'member',
|
|
12
|
+
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
13
|
+
removed_at TIMESTAMPTZ,
|
|
14
|
+
removed_by_user_id INTEGER,
|
|
15
|
+
removal_reason TEXT,
|
|
16
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
17
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
18
|
+
);
|
|
19
|
+
-- Only one active membership per user per org
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tm_memberships_active ON tm_memberships(org_id, user_id) WHERE removed_at IS NULL;
|
|
21
|
+
-- Only one owner per org (active)
|
|
22
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tm_memberships_owner ON tm_memberships(org_id) WHERE role = 'owner' AND removed_at IS NULL;
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_tm_memberships_user ON tm_memberships(user_id);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_tm_memberships_org ON tm_memberships(org_id);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_invitations (
|
|
2
|
+
id SERIAL PRIMARY KEY,
|
|
3
|
+
org_id INTEGER NOT NULL REFERENCES tm_organizations(id) ON DELETE CASCADE,
|
|
4
|
+
invited_by_user_id INTEGER NOT NULL,
|
|
5
|
+
email TEXT NOT NULL,
|
|
6
|
+
role tm_role NOT NULL DEFAULT 'member',
|
|
7
|
+
token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of the magic-link token
|
|
8
|
+
code_encrypted TEXT NOT NULL, -- AES-256-GCM encrypted 6-digit code
|
|
9
|
+
expires_at TIMESTAMPTZ NOT NULL, -- NOW() + 7 days
|
|
10
|
+
accepted_at TIMESTAMPTZ,
|
|
11
|
+
revoked_at TIMESTAMPTZ,
|
|
12
|
+
revoked_by_user_id INTEGER,
|
|
13
|
+
resent_count INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
16
|
+
);
|
|
17
|
+
-- Only one pending (non-accepted, non-revoked) invite per email per org
|
|
18
|
+
-- Note: expires_at check omitted from predicate (NOW() is not IMMUTABLE); enforced at query time
|
|
19
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tm_invitations_pending ON tm_invitations(org_id, email) WHERE accepted_at IS NULL AND revoked_at IS NULL;
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_tm_invitations_org ON tm_invitations(org_id);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_tm_invitations_email ON tm_invitations(email);
|
|
22
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_audit_events (
|
|
2
|
+
id BIGSERIAL PRIMARY KEY,
|
|
3
|
+
org_id INTEGER REFERENCES tm_organizations(id) ON DELETE SET NULL,
|
|
4
|
+
actor_user_id INTEGER,
|
|
5
|
+
actor_type TEXT NOT NULL DEFAULT 'user' CHECK (actor_type IN ('user', 'super_admin')),
|
|
6
|
+
action TEXT NOT NULL,
|
|
7
|
+
target_type TEXT,
|
|
8
|
+
target_id TEXT,
|
|
9
|
+
before_state JSONB,
|
|
10
|
+
after_state JSONB,
|
|
11
|
+
ip TEXT,
|
|
12
|
+
user_agent TEXT,
|
|
13
|
+
reason TEXT, -- required for super_admin actions
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_tm_audit_org ON tm_audit_events(org_id, created_at DESC);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_tm_audit_actor ON tm_audit_events(actor_user_id);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_email_change_requests (
|
|
2
|
+
id SERIAL PRIMARY KEY,
|
|
3
|
+
user_id INTEGER NOT NULL,
|
|
4
|
+
old_email TEXT,
|
|
5
|
+
new_email TEXT NOT NULL,
|
|
6
|
+
verify_token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of verification token
|
|
7
|
+
cancel_token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of cancellation token
|
|
8
|
+
expires_at TIMESTAMPTZ NOT NULL, -- NOW() + 24 hours
|
|
9
|
+
verified_at TIMESTAMPTZ,
|
|
10
|
+
cancelled_at TIMESTAMPTZ,
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_tm_email_change_user ON tm_email_change_requests(user_id, created_at DESC);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
DO $$ BEGIN
|
|
2
|
+
CREATE TYPE tm_transfer_status AS ENUM ('pending', 'accepted', 'cancelled', 'expired');
|
|
3
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
4
|
+
END $$;
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS tm_ownership_transfers (
|
|
7
|
+
id SERIAL PRIMARY KEY,
|
|
8
|
+
org_id INTEGER NOT NULL REFERENCES tm_organizations(id) ON DELETE CASCADE,
|
|
9
|
+
from_user_id INTEGER NOT NULL,
|
|
10
|
+
to_user_id INTEGER NOT NULL,
|
|
11
|
+
status tm_transfer_status NOT NULL DEFAULT 'pending',
|
|
12
|
+
initiated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
13
|
+
accepted_at TIMESTAMPTZ,
|
|
14
|
+
cancelled_at TIMESTAMPTZ,
|
|
15
|
+
cancelled_by_user_id INTEGER,
|
|
16
|
+
expires_at TIMESTAMPTZ NOT NULL, -- NOW() + 7 days
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
18
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
19
|
+
);
|
|
20
|
+
-- Only one pending transfer per org
|
|
21
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tm_transfers_pending ON tm_ownership_transfers(org_id) WHERE status = 'pending';
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_tm_transfers_org ON tm_ownership_transfers(org_id);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_super_admins (
|
|
2
|
+
user_id INTEGER PRIMARY KEY,
|
|
3
|
+
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
4
|
+
granted_by_user_id INTEGER,
|
|
5
|
+
notes TEXT,
|
|
6
|
+
revoked_at TIMESTAMPTZ
|
|
7
|
+
);
|
|
8
|
+
CREATE INDEX IF NOT EXISTS idx_tm_super_admins_active ON tm_super_admins(user_id) WHERE revoked_at IS NULL;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tm_password_reset_requests (
|
|
2
|
+
id SERIAL PRIMARY KEY,
|
|
3
|
+
user_id INTEGER NOT NULL,
|
|
4
|
+
token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of reset token
|
|
5
|
+
expires_at TIMESTAMPTZ NOT NULL, -- NOW() + 1 hour
|
|
6
|
+
used_at TIMESTAMPTZ,
|
|
7
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
8
|
+
);
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_tm_pwd_reset_user ON tm_password_reset_requests(user_id, created_at DESC);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- tm_shared_access: reserved for v0.2.0 (cross-org vendor access).
|
|
2
|
+
-- Empty scaffold -- no columns beyond id. Do not add anything here in v0.1.0.
|
|
3
|
+
CREATE TABLE IF NOT EXISTS tm_shared_access (
|
|
4
|
+
id SERIAL PRIMARY KEY,
|
|
5
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
6
|
+
);
|
|
7
|
+
-- NOTE: This table is intentionally empty in v0.1.0.
|
|
8
|
+
-- Schema will be defined in the v0.2.0 migration.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Seed super-admin for vaakapila@gmail.com.
|
|
2
|
+
-- This migration is ALWAYS run but only inserts if:
|
|
3
|
+
-- 1. A user with that email exists in the host app's user table.
|
|
4
|
+
-- 2. The row does not already exist in tm_super_admins.
|
|
5
|
+
-- The host adapter is responsible for user existence; this migration
|
|
6
|
+
-- does nothing if the user does not exist (host-managed users table
|
|
7
|
+
-- is outside module scope). The demo-host seeds this user before
|
|
8
|
+
-- running migrations.
|
|
9
|
+
--
|
|
10
|
+
-- Since tm_super_admins uses host user_id (INTEGER), and we cannot
|
|
11
|
+
-- JOIN across host tables from inside the module, this seed is
|
|
12
|
+
-- handled by the createServerModule boot sequence when
|
|
13
|
+
-- enableSuperAdmin=true (see index.ts seedSuperAdmin()).
|
|
14
|
+
-- This file is a no-op placeholder to reserve the migration slot.
|
|
15
|
+
SELECT 1; -- placeholder: actual seed runs in application boot
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { Pool } from 'pg';
|
|
3
|
+
import type { ServerModuleAdapter, TeamManagementFeatureFlags } from '../types.js';
|
|
4
|
+
export declare function createAdminRouter(pool: Pool, adapter: ServerModuleAdapter, flags: TeamManagementFeatureFlags, baseUrl: string): Router;
|
|
5
|
+
//# sourceMappingURL=admin.routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.routes.d.ts","sourceRoot":"","sources":["../../../src/server/routes/admin.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAW,MAAM,aAAa,CAAC;AAgB5F,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,0BAA0B,EACjC,OAAO,EAAE,MAAM,GACd,MAAM,CAuLR"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { requireSuperAdmin } from '../middleware/require-super-admin.js';
|
|
3
|
+
import { listAllOrgs, getOrgForAdmin, getUserForAdmin, restoreOrg, appointOwner, hardDeleteOrg, addMemberAdmin, removeMemberAdmin, lockUser, unlockUser, triggerPasswordReset, } from '../services/super-admin.service.js';
|
|
4
|
+
export function createAdminRouter(pool, adapter, flags, baseUrl) {
|
|
5
|
+
const router = Router();
|
|
6
|
+
const superAdminMiddleware = requireSuperAdmin(pool, adapter, flags);
|
|
7
|
+
// GET /admin/orgs
|
|
8
|
+
router.get('/orgs', superAdminMiddleware, async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const orgs = await listAllOrgs(pool);
|
|
11
|
+
res.json({ orgs });
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
adapter.logger.error('[admin] GET /orgs', { error: e.message });
|
|
15
|
+
res.status(500).json({ error: 'Failed to list organizations' });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
// GET /admin/orgs/:id
|
|
19
|
+
router.get('/orgs/:id', superAdminMiddleware, async (req, res) => {
|
|
20
|
+
const orgId = parseInt(req.params.id, 10);
|
|
21
|
+
if (isNaN(orgId)) {
|
|
22
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const org = await getOrgForAdmin(pool, orgId);
|
|
27
|
+
res.json({ org });
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
const msg = e.message;
|
|
31
|
+
adapter.logger.error('[admin] GET /orgs/:id', { error: msg });
|
|
32
|
+
if (msg.includes('not found')) {
|
|
33
|
+
res.status(404).json({ error: msg });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
res.status(500).json({ error: 'Failed to fetch organization' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// GET /admin/users/:id
|
|
41
|
+
router.get('/users/:id', superAdminMiddleware, async (req, res) => {
|
|
42
|
+
const userId = parseInt(req.params.id, 10);
|
|
43
|
+
if (isNaN(userId)) {
|
|
44
|
+
res.status(400).json({ error: 'Invalid user ID' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const user = await getUserForAdmin(pool, adapter, userId);
|
|
49
|
+
res.json({ user });
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
const msg = e.message;
|
|
53
|
+
adapter.logger.error('[admin] GET /users/:id', { error: msg });
|
|
54
|
+
if (msg.includes('not found')) {
|
|
55
|
+
res.status(404).json({ error: msg });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
res.status(500).json({ error: 'Failed to fetch user' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// POST /admin/orgs/:id/restore
|
|
63
|
+
router.post('/orgs/:id/restore', superAdminMiddleware, async (req, res) => {
|
|
64
|
+
const orgId = parseInt(req.params.id, 10);
|
|
65
|
+
if (isNaN(orgId)) {
|
|
66
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { reason } = req.body;
|
|
70
|
+
if (!reason) {
|
|
71
|
+
res.status(400).json({ error: 'reason is required' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const saUserId = req.superAdminUserId;
|
|
75
|
+
try {
|
|
76
|
+
await restoreOrg(pool, { orgId, superAdminUserId: saUserId, reason });
|
|
77
|
+
res.json({ message: 'Organization restored' });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const msg = e.message;
|
|
81
|
+
adapter.logger.error('[admin] POST /orgs/:id/restore', { error: msg });
|
|
82
|
+
if (msg.includes('not found') || msg.includes('not deleted')) {
|
|
83
|
+
res.status(404).json({ error: msg });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
res.status(500).json({ error: 'Failed to restore organization' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// POST /admin/orgs/:id/appoint-owner
|
|
91
|
+
router.post('/orgs/:id/appoint-owner', superAdminMiddleware, async (req, res) => {
|
|
92
|
+
const orgId = parseInt(req.params.id, 10);
|
|
93
|
+
if (isNaN(orgId)) {
|
|
94
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { targetUserId, reason } = req.body;
|
|
98
|
+
if (!targetUserId || !reason) {
|
|
99
|
+
res.status(400).json({ error: 'targetUserId and reason are required' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const saUserId = req.superAdminUserId;
|
|
103
|
+
try {
|
|
104
|
+
await appointOwner(pool, { orgId, targetUserId, superAdminUserId: saUserId, reason });
|
|
105
|
+
res.json({ message: 'Owner appointed' });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
adapter.logger.error('[admin] POST /orgs/:id/appoint-owner', { error: e.message });
|
|
109
|
+
res.status(500).json({ error: 'Failed to appoint owner' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// POST /admin/orgs/:id/hard-delete
|
|
113
|
+
router.post('/orgs/:id/hard-delete', superAdminMiddleware, async (req, res) => {
|
|
114
|
+
if (!flags.enableHardDelete) {
|
|
115
|
+
res.status(403).json({ error: 'Hard delete is not enabled' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const orgId = parseInt(req.params.id, 10);
|
|
119
|
+
if (isNaN(orgId)) {
|
|
120
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const { legalBasis } = req.body;
|
|
124
|
+
if (!legalBasis) {
|
|
125
|
+
res.status(400).json({ error: 'legalBasis is required' });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const saUserId = req.superAdminUserId;
|
|
129
|
+
try {
|
|
130
|
+
await hardDeleteOrg(pool, { orgId, superAdminUserId: saUserId, legalBasis });
|
|
131
|
+
res.json({ message: 'Organization permanently deleted' });
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
const msg = e.message;
|
|
135
|
+
adapter.logger.error('[admin] POST /orgs/:id/hard-delete', { error: msg });
|
|
136
|
+
if (msg.includes('legal basis')) {
|
|
137
|
+
res.status(400).json({ error: msg });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
res.status(500).json({ error: 'Failed to hard delete organization' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// POST /admin/orgs/:id/members/add
|
|
145
|
+
router.post('/orgs/:id/members/add', superAdminMiddleware, async (req, res) => {
|
|
146
|
+
const orgId = parseInt(req.params.id, 10);
|
|
147
|
+
if (isNaN(orgId)) {
|
|
148
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const { userId, role, reason } = req.body;
|
|
152
|
+
if (!userId || !role || !reason) {
|
|
153
|
+
res.status(400).json({ error: 'userId, role, and reason are required' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const saUserId = req.superAdminUserId;
|
|
157
|
+
try {
|
|
158
|
+
await addMemberAdmin(pool, { orgId, userId, role: role, superAdminUserId: saUserId, reason });
|
|
159
|
+
res.json({ message: 'Member added' });
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
adapter.logger.error('[admin] POST /orgs/:id/members/add', { error: e.message });
|
|
163
|
+
res.status(500).json({ error: 'Failed to add member' });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// POST /admin/orgs/:id/members/remove
|
|
167
|
+
router.post('/orgs/:id/members/remove', superAdminMiddleware, async (req, res) => {
|
|
168
|
+
const orgId = parseInt(req.params.id, 10);
|
|
169
|
+
if (isNaN(orgId)) {
|
|
170
|
+
res.status(400).json({ error: 'Invalid org ID' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const { userId, reason } = req.body;
|
|
174
|
+
if (!userId || !reason) {
|
|
175
|
+
res.status(400).json({ error: 'userId and reason are required' });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const saUserId = req.superAdminUserId;
|
|
179
|
+
try {
|
|
180
|
+
await removeMemberAdmin(pool, { orgId, userId, superAdminUserId: saUserId, reason });
|
|
181
|
+
res.json({ message: 'Member removed' });
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
adapter.logger.error('[admin] POST /orgs/:id/members/remove', { error: e.message });
|
|
185
|
+
res.status(500).json({ error: 'Failed to remove member' });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// POST /admin/users/:id/lock
|
|
189
|
+
router.post('/users/:id/lock', superAdminMiddleware, async (req, res) => {
|
|
190
|
+
const userId = parseInt(req.params.id, 10);
|
|
191
|
+
if (isNaN(userId)) {
|
|
192
|
+
res.status(400).json({ error: 'Invalid user ID' });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const { reason } = req.body;
|
|
196
|
+
if (!reason) {
|
|
197
|
+
res.status(400).json({ error: 'reason is required' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const saUserId = req.superAdminUserId;
|
|
201
|
+
try {
|
|
202
|
+
await lockUser(pool, adapter, { userId, superAdminUserId: saUserId, reason });
|
|
203
|
+
res.json({ message: 'User locked and sessions invalidated' });
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
adapter.logger.error('[admin] POST /users/:id/lock', { error: e.message });
|
|
207
|
+
res.status(500).json({ error: 'Failed to lock user' });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// POST /admin/users/:id/unlock
|
|
211
|
+
router.post('/users/:id/unlock', superAdminMiddleware, async (req, res) => {
|
|
212
|
+
const userId = parseInt(req.params.id, 10);
|
|
213
|
+
if (isNaN(userId)) {
|
|
214
|
+
res.status(400).json({ error: 'Invalid user ID' });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const { reason } = req.body;
|
|
218
|
+
if (!reason) {
|
|
219
|
+
res.status(400).json({ error: 'reason is required' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const saUserId = req.superAdminUserId;
|
|
223
|
+
try {
|
|
224
|
+
await unlockUser(pool, adapter, { userId, superAdminUserId: saUserId, reason });
|
|
225
|
+
res.json({ message: 'User unlocked' });
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
adapter.logger.error('[admin] POST /users/:id/unlock', { error: e.message });
|
|
229
|
+
res.status(500).json({ error: 'Failed to unlock user' });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// POST /admin/users/:id/password-reset
|
|
233
|
+
router.post('/users/:id/password-reset', superAdminMiddleware, async (req, res) => {
|
|
234
|
+
const userId = parseInt(req.params.id, 10);
|
|
235
|
+
if (isNaN(userId)) {
|
|
236
|
+
res.status(400).json({ error: 'Invalid user ID' });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { reason } = req.body;
|
|
240
|
+
if (!reason) {
|
|
241
|
+
res.status(400).json({ error: 'reason is required' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const saUserId = req.superAdminUserId;
|
|
245
|
+
try {
|
|
246
|
+
await triggerPasswordReset(pool, adapter, { userId, superAdminUserId: saUserId, reason, baseUrl });
|
|
247
|
+
res.json({ message: 'Password reset email sent' });
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const msg = e.message;
|
|
251
|
+
adapter.logger.error('[admin] POST /users/:id/password-reset', { error: msg });
|
|
252
|
+
if (msg.includes('not found')) {
|
|
253
|
+
res.status(404).json({ error: msg });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
res.status(500).json({ error: 'Failed to trigger password reset' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return router;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=admin.routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.routes.js","sourceRoot":"","sources":["../../../src/server/routes/admin.routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { Pool } from 'pg';
|
|
3
|
+
import type { ServerModuleAdapter, TeamManagementFeatureFlags } from '../types.js';
|
|
4
|
+
export declare function createAuditRouter(pool: Pool, adapter: ServerModuleAdapter, flags: TeamManagementFeatureFlags): Router;
|
|
5
|
+
//# sourceMappingURL=audit.routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.routes.d.ts","sourceRoot":"","sources":["../../../src/server/routes/audit.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAQnF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,0BAA0B,GAChC,MAAM,CA8ER"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { requireMembership } from '../middleware/require-membership.js';
|
|
3
|
+
import { requireRole } from '../middleware/require-role.js';
|
|
4
|
+
const SUPER_ADMIN_DISPLAY_NAME = 'Varshyl Support';
|
|
5
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
6
|
+
const MAX_PAGE_LIMIT = 200;
|
|
7
|
+
export function createAuditRouter(pool, adapter, flags) {
|
|
8
|
+
const router = Router({ mergeParams: true });
|
|
9
|
+
const authMiddleware = requireMembership(pool, adapter);
|
|
10
|
+
// GET /orgs/:orgId/audit — paginated audit log (admin+)
|
|
11
|
+
router.get('/:orgId/audit', authMiddleware, requireRole('admin'), async (req, res) => {
|
|
12
|
+
if (!flags.enableAuditLog) {
|
|
13
|
+
res.status(501).json({ error: 'Audit log is not enabled' });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { orgId } = req;
|
|
17
|
+
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
|
18
|
+
const rawLimit = parseInt(req.query.limit, 10) || DEFAULT_PAGE_LIMIT;
|
|
19
|
+
const limit = Math.min(rawLimit, MAX_PAGE_LIMIT);
|
|
20
|
+
const offset = (page - 1) * limit;
|
|
21
|
+
const action = req.query.action;
|
|
22
|
+
try {
|
|
23
|
+
const params = [orgId, limit, offset];
|
|
24
|
+
let actionFilter = '';
|
|
25
|
+
if (action) {
|
|
26
|
+
params.push(action);
|
|
27
|
+
actionFilter = `AND ae.action = $${params.length}`;
|
|
28
|
+
}
|
|
29
|
+
const result = await pool.query(`SELECT ae.*, sa.user_id AS is_super_admin_actor
|
|
30
|
+
FROM tm_audit_events ae
|
|
31
|
+
LEFT JOIN tm_super_admins sa ON sa.user_id = ae.actor_user_id AND sa.revoked_at IS NULL
|
|
32
|
+
WHERE ae.org_id = $1
|
|
33
|
+
${actionFilter}
|
|
34
|
+
ORDER BY ae.created_at DESC
|
|
35
|
+
LIMIT $2 OFFSET $3`, params);
|
|
36
|
+
const countResult = await pool.query(`SELECT COUNT(*) FROM tm_audit_events WHERE org_id = $1 ${action ? 'AND action = $2' : ''}`, action ? [orgId, action] : [orgId]);
|
|
37
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
38
|
+
// Collect user IDs for enrichment (non-super-admin actors)
|
|
39
|
+
const userIds = [
|
|
40
|
+
...new Set(result.rows
|
|
41
|
+
.filter(r => r.actor_user_id && !r.is_super_admin_actor)
|
|
42
|
+
.map(r => r.actor_user_id)),
|
|
43
|
+
];
|
|
44
|
+
const users = userIds.length > 0 ? await adapter.getUsersByIds(userIds) : [];
|
|
45
|
+
const userMap = new Map(users.map(u => [u.id, u]));
|
|
46
|
+
const events = result.rows.map(row => {
|
|
47
|
+
const { is_super_admin_actor, ...event } = row;
|
|
48
|
+
const actorDisplay = is_super_admin_actor
|
|
49
|
+
? { id: row.actor_user_id, name: SUPER_ADMIN_DISPLAY_NAME, email: null }
|
|
50
|
+
: userMap.get(row.actor_user_id) ?? { id: row.actor_user_id, name: null, email: null };
|
|
51
|
+
return { ...event, actor: actorDisplay };
|
|
52
|
+
});
|
|
53
|
+
res.json({
|
|
54
|
+
events,
|
|
55
|
+
pagination: {
|
|
56
|
+
page,
|
|
57
|
+
limit,
|
|
58
|
+
total,
|
|
59
|
+
totalPages: Math.ceil(total / limit),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
adapter.logger.error('[audit] GET /:orgId/audit', { error: e.message });
|
|
65
|
+
res.status(500).json({ error: 'Failed to fetch audit log' });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return router;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=audit.routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.routes.js","sourceRoot":"","sources":["../../../src/server/routes/audit.routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { Pool } from 'pg';
|
|
3
|
+
import type { TeamManagementConfig } from '../types.js';
|
|
4
|
+
export declare function createHealthRouter(pool: Pool, config: TeamManagementConfig): {
|
|
5
|
+
router: Router;
|
|
6
|
+
handler: import('express').RequestHandler;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=health.routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.routes.d.ts","sourceRoot":"","sources":["../../../src/server/routes/health.routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAIxD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,oBAAoB,GAC3B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,SAAS,EAAE,cAAc,CAAA;CAAE,CAoC/D"}
|