@varshylinc/team-management 0.1.0 → 0.2.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/dist/client/actions.d.ts +20 -0
- package/dist/client/actions.d.ts.map +1 -0
- package/dist/client/actions.js +23 -0
- package/dist/client/actions.js.map +1 -0
- package/dist/client/api.d.ts +74 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +245 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/components/AddMemberForm.d.ts +12 -0
- package/dist/client/components/AddMemberForm.d.ts.map +1 -0
- package/dist/client/components/AddMemberForm.js +37 -0
- package/dist/client/components/AddMemberForm.js.map +1 -0
- package/dist/client/components/AuditEventRow.d.ts +7 -0
- package/dist/client/components/AuditEventRow.d.ts.map +1 -0
- package/dist/client/components/AuditEventRow.js +20 -0
- package/dist/client/components/AuditEventRow.js.map +1 -0
- package/dist/client/components/CascadePreview.d.ts +11 -0
- package/dist/client/components/CascadePreview.d.ts.map +1 -0
- package/dist/client/components/CascadePreview.js +7 -0
- package/dist/client/components/CascadePreview.js.map +1 -0
- package/dist/client/components/DangerZoneCard.d.ts +10 -0
- package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
- package/dist/client/components/DangerZoneCard.js +28 -0
- package/dist/client/components/DangerZoneCard.js.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
- package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
- package/dist/client/components/InvitationCodeDisplay.js +26 -0
- package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
- package/dist/client/components/InviteForm.d.ts +7 -0
- package/dist/client/components/InviteForm.d.ts.map +1 -0
- package/dist/client/components/InviteForm.js +35 -0
- package/dist/client/components/InviteForm.js.map +1 -0
- package/dist/client/components/MemberRow.d.ts +10 -0
- package/dist/client/components/MemberRow.d.ts.map +1 -0
- package/dist/client/components/MemberRow.js +17 -0
- package/dist/client/components/MemberRow.js.map +1 -0
- package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
- package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
- package/dist/client/components/OrgPeopleRoster.js +13 -0
- package/dist/client/components/OrgPeopleRoster.js.map +1 -0
- package/dist/client/components/PendingTransferBanner.d.ts +10 -0
- package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
- package/dist/client/components/PendingTransferBanner.js +48 -0
- package/dist/client/components/PendingTransferBanner.js.map +1 -0
- package/dist/client/components/PlaceholderCard.d.ts +7 -0
- package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
- package/dist/client/components/PlaceholderCard.js +15 -0
- package/dist/client/components/PlaceholderCard.js.map +1 -0
- package/dist/client/components/RoleBadge.d.ts +5 -0
- package/dist/client/components/RoleBadge.d.ts.map +1 -0
- package/dist/client/components/RoleBadge.js +17 -0
- package/dist/client/components/RoleBadge.js.map +1 -0
- package/dist/client/components/RoleSelect.d.ts +10 -0
- package/dist/client/components/RoleSelect.d.ts.map +1 -0
- package/dist/client/components/RoleSelect.js +12 -0
- package/dist/client/components/RoleSelect.js.map +1 -0
- package/dist/client/components/SeatUsagePanel.d.ts +6 -0
- package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
- package/dist/client/components/SeatUsagePanel.js +13 -0
- package/dist/client/components/SeatUsagePanel.js.map +1 -0
- package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
- package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
- package/dist/client/hooks/useCurrentMembership.js +20 -0
- package/dist/client/hooks/useCurrentMembership.js.map +1 -0
- package/dist/client/hooks/useMembers.d.ts +10 -0
- package/dist/client/hooks/useMembers.d.ts.map +1 -0
- package/dist/client/hooks/useMembers.js +20 -0
- package/dist/client/hooks/useMembers.js.map +1 -0
- package/dist/client/hooks/useOrgMembers.d.ts +20 -0
- package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
- package/dist/client/hooks/useOrgMembers.js +63 -0
- package/dist/client/hooks/useOrgMembers.js.map +1 -0
- package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
- package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
- package/dist/client/hooks/usePendingInvitations.js +20 -0
- package/dist/client/hooks/usePendingInvitations.js.map +1 -0
- package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
- package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
- package/dist/client/hooks/usePendingTransfer.js +23 -0
- package/dist/client/hooks/usePendingTransfer.js.map +1 -0
- package/dist/client/index.d.ts +31 -0
- package/dist/client/index.d.ts.map +1 -0
- package/{src/client/index.ts → dist/client/index.js} +6 -54
- package/dist/client/index.js.map +1 -0
- package/dist/client/pages/AuditLogPage.d.ts +6 -0
- package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
- package/dist/client/pages/AuditLogPage.js +51 -0
- package/dist/client/pages/AuditLogPage.js.map +1 -0
- package/dist/client/pages/EmailChangePage.d.ts +8 -0
- package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
- package/dist/client/pages/EmailChangePage.js +52 -0
- package/dist/client/pages/EmailChangePage.js.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
- package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
- package/dist/client/pages/InvitationAcceptPage.js +42 -0
- package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
- package/dist/client/pages/InvitationCodePage.d.ts +6 -0
- package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
- package/dist/client/pages/InvitationCodePage.js +28 -0
- package/dist/client/pages/InvitationCodePage.js.map +1 -0
- package/dist/client/pages/MembersPage.d.ts +6 -0
- package/dist/client/pages/MembersPage.d.ts.map +1 -0
- package/dist/client/pages/MembersPage.js +67 -0
- package/dist/client/pages/MembersPage.js.map +1 -0
- package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
- package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
- package/dist/client/pages/OrgPeoplePage.js +40 -0
- package/dist/client/pages/OrgPeoplePage.js.map +1 -0
- package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
- package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
- package/dist/client/pages/OrgSettingsPage.js +78 -0
- package/dist/client/pages/OrgSettingsPage.js.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
- package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
- package/dist/client/pages/OwnershipTransferPage.js +68 -0
- package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
- package/dist/client/pages/PasswordResetPage.d.ts +6 -0
- package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetPage.js +34 -0
- package/dist/client/pages/PasswordResetPage.js.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
- package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
- package/dist/client/pages/PasswordResetRequestPage.js +27 -0
- package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
- package/dist/client/pages/PlaceholderPage.d.ts +7 -0
- package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
- package/dist/client/pages/PlaceholderPage.js +16 -0
- package/dist/client/pages/PlaceholderPage.js.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
- package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
- package/dist/client/pages/SuperAdminDashboard.js +123 -0
- package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
- package/dist/client/theme.d.ts +12 -0
- package/dist/client/theme.d.ts.map +1 -0
- package/dist/client/theme.js +16 -0
- package/dist/client/theme.js.map +1 -0
- package/dist/client/types.d.ts +78 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/org-admin.d.ts +45 -0
- package/dist/server/org-admin.d.ts.map +1 -0
- package/dist/server/org-admin.js +63 -0
- package/dist/server/org-admin.js.map +1 -0
- package/dist/server/routes/orgs.routes.d.ts.map +1 -1
- package/dist/server/routes/orgs.routes.js +81 -12
- package/dist/server/routes/orgs.routes.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +18 -11
- package/.eslintrc.cjs +0 -18
- package/CHANGELOG.md +0 -159
- package/src/client/api.ts +0 -314
- package/src/client/components/AuditEventRow.tsx +0 -59
- package/src/client/components/CascadePreview.tsx +0 -36
- package/src/client/components/DangerZoneCard.tsx +0 -103
- package/src/client/components/InvitationCodeDisplay.tsx +0 -48
- package/src/client/components/InviteForm.tsx +0 -77
- package/src/client/components/MemberRow.tsx +0 -69
- package/src/client/components/PendingTransferBanner.tsx +0 -98
- package/src/client/components/PlaceholderCard.tsx +0 -26
- package/src/client/components/RoleBadge.tsx +0 -26
- package/src/client/components/RoleSelect.tsx +0 -35
- package/src/client/hooks/.gitkeep +0 -0
- package/src/client/hooks/useCurrentMembership.ts +0 -24
- package/src/client/hooks/useMembers.ts +0 -24
- package/src/client/hooks/usePendingInvitations.ts +0 -24
- package/src/client/hooks/usePendingTransfer.ts +0 -27
- package/src/client/pages/AuditLogPage.tsx +0 -164
- package/src/client/pages/EmailChangePage.tsx +0 -144
- package/src/client/pages/InvitationAcceptPage.tsx +0 -163
- package/src/client/pages/InvitationCodePage.tsx +0 -108
- package/src/client/pages/MembersPage.tsx +0 -290
- package/src/client/pages/OrgSettingsPage.tsx +0 -185
- package/src/client/pages/OwnershipTransferPage.tsx +0 -163
- package/src/client/pages/PasswordResetPage.tsx +0 -104
- package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
- package/src/client/pages/PlaceholderPage.tsx +0 -20
- package/src/client/pages/SuperAdminDashboard.tsx +0 -401
- package/src/client/types.ts +0 -78
- package/src/index.ts +0 -24
- package/src/server/crypto.ts +0 -47
- package/src/server/index.ts +0 -167
- package/src/server/middleware/require-membership.ts +0 -48
- package/src/server/middleware/require-role.ts +0 -19
- package/src/server/middleware/require-super-admin.ts +0 -32
- package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
- package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
- package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
- package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
- package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
- package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
- package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
- package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
- package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
- package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
- package/src/server/migrations/0011_seed_super_admin.sql +0 -15
- package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
- package/src/server/routes/admin.routes.ts +0 -208
- package/src/server/routes/audit.routes.ts +0 -93
- package/src/server/routes/health.routes.ts +0 -46
- package/src/server/routes/invitations.routes.ts +0 -252
- package/src/server/routes/me.routes.ts +0 -143
- package/src/server/routes/orgs.routes.ts +0 -428
- package/src/server/routes/transfer.routes.ts +0 -110
- package/src/server/services/.gitkeep +0 -0
- package/src/server/services/audit.service.ts +0 -49
- package/src/server/services/email-change.service.ts +0 -178
- package/src/server/services/invitations.service.ts +0 -316
- package/src/server/services/memberships.service.ts +0 -129
- package/src/server/services/organizations.service.ts +0 -110
- package/src/server/services/ownership.service.ts +0 -170
- package/src/server/services/password-reset.service.ts +0 -94
- package/src/server/services/super-admin.service.ts +0 -321
- package/src/server/sql/.gitkeep +0 -0
- package/src/server/types.ts +0 -145
- package/src/shared/types.ts +0 -24
- package/tests/integration/audit-fires.test.ts +0 -288
- package/tests/integration/cascade-preview.test.ts +0 -157
- package/tests/integration/email-change.test.ts +0 -190
- package/tests/integration/feature-flags.test.ts +0 -213
- package/tests/integration/invitations-code.test.ts +0 -218
- package/tests/integration/invitations-expiry.test.ts +0 -216
- package/tests/integration/invitations-resend.test.ts +0 -241
- package/tests/integration/invitations-revoke.test.ts +0 -226
- package/tests/integration/invitations-switch-org.test.ts +0 -156
- package/tests/integration/invitations-token.test.ts +0 -221
- package/tests/integration/migrations.test.ts +0 -119
- package/tests/integration/only-owner-protections.test.ts +0 -130
- package/tests/integration/org-lifecycle.test.ts +0 -169
- package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
- package/tests/integration/ownership-transfer-expire.test.ts +0 -171
- package/tests/integration/ownership-transfer-happy.test.ts +0 -184
- package/tests/integration/ownership-transfer-locks.test.ts +0 -146
- package/tests/integration/password-reset.test.ts +0 -200
- package/tests/integration/super-admin-actions.test.ts +0 -180
- package/tests/integration/super-admin-restrictions.test.ts +0 -209
- package/tests/setup/global-setup.ts +0 -20
- package/tests/unit/adapter-shape.test.ts +0 -330
- package/tests/unit/role-permissions.test.ts +0 -236
- package/tests/unit/validation.test.ts +0 -304
- package/tsconfig.client.json +0 -13
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -13
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import request from 'supertest';
|
|
4
|
-
import { Pool } from 'pg';
|
|
5
|
-
import { createServerModule } from '../../src/server/index.js';
|
|
6
|
-
import type { ServerModuleAdapter, OrgRole } from '../../src/server/types.js';
|
|
7
|
-
|
|
8
|
-
const DATABASE_URL = process.env.DATABASE_URL;
|
|
9
|
-
const describeWithDb = DATABASE_URL ? describe : describe.skip;
|
|
10
|
-
|
|
11
|
-
let currentUserId: number | null = 1;
|
|
12
|
-
let currentOrgId: number | null = 1;
|
|
13
|
-
|
|
14
|
-
const testAdapter: ServerModuleAdapter = {
|
|
15
|
-
getCurrentUserId: async () => currentUserId,
|
|
16
|
-
getOrganizationIdForUser: async () => currentOrgId,
|
|
17
|
-
isUserOrgAdmin: async (userId) => userId <= 2,
|
|
18
|
-
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
|
19
|
-
getUserById: async (id) => ({ id, email: `u${id}@test.com`, name: `User${id}` }),
|
|
20
|
-
getUsersByIds: async (ids) => ids.map(id => ({ id, email: `u${id}@test.com`, name: `User${id}` })),
|
|
21
|
-
findUserByEmail: async (email) => {
|
|
22
|
-
const m = email.match(/^u(\d+)@test\.com$/);
|
|
23
|
-
return m ? { id: parseInt(m[1]), email } : null;
|
|
24
|
-
},
|
|
25
|
-
createUserFromInvite: async ({ email }: { email: string; orgId: number; role: OrgRole }) => ({ id: 99, email }),
|
|
26
|
-
setUserPassword: async () => {},
|
|
27
|
-
hashPassword: async (p) => `h:${p}`,
|
|
28
|
-
verifyPassword: async (p, h) => h === `h:${p}`,
|
|
29
|
-
invalidateAllUserSessions: async () => {},
|
|
30
|
-
sendInviteEmail: vi.fn().mockResolvedValue(undefined),
|
|
31
|
-
sendOwnershipTransferEmail: async () => {},
|
|
32
|
-
sendEmailChangeVerification: async () => {},
|
|
33
|
-
sendEmailChangeOldNotice: async () => {},
|
|
34
|
-
sendEmailChangedFinalNotice: async () => {},
|
|
35
|
-
sendPasswordResetEmail: vi.fn().mockResolvedValue(undefined),
|
|
36
|
-
sendOrgDeletionNotice: async () => {},
|
|
37
|
-
emitNotification: async () => {},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
describeWithDb('super-admin restrictions (integration)', () => {
|
|
41
|
-
let pool: Pool;
|
|
42
|
-
let app: express.Express;
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
pool = new Pool({ connectionString: DATABASE_URL });
|
|
46
|
-
const tm = createServerModule({
|
|
47
|
-
adapter: testAdapter,
|
|
48
|
-
db: pool,
|
|
49
|
-
config: {
|
|
50
|
-
featureFlags: {
|
|
51
|
-
enableInvites: true,
|
|
52
|
-
enableAuditLog: true,
|
|
53
|
-
enableSuperAdmin: true,
|
|
54
|
-
enableOwnershipTransfer: true,
|
|
55
|
-
enableEmailChange: true,
|
|
56
|
-
enablePasswordReset: true,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
await tm.runMigrations();
|
|
61
|
-
app = express();
|
|
62
|
-
app.use(express.json());
|
|
63
|
-
app.use('/api/team', tm.router);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
beforeEach(async () => {
|
|
67
|
-
await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
|
|
68
|
-
tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
|
|
69
|
-
tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
|
|
70
|
-
// Seed org + super-admin (user 1 is super-admin)
|
|
71
|
-
await pool.query(`INSERT INTO tm_organizations (id, name, slug, owner_user_id, settings)
|
|
72
|
-
VALUES (1, 'Test Org', 'test-org', 1, '{}')`);
|
|
73
|
-
await pool.query(`INSERT INTO tm_memberships (org_id, user_id, role)
|
|
74
|
-
VALUES (1,1,'owner'),(1,2,'admin'),(1,3,'member'),(1,4,'viewer')`);
|
|
75
|
-
await pool.query(`INSERT INTO tm_super_admins (user_id, granted_by_user_id)
|
|
76
|
-
VALUES (1, 1)`);
|
|
77
|
-
currentUserId = 1;
|
|
78
|
-
currentOrgId = 1;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
afterAll(async () => {
|
|
82
|
-
await pool.query(`TRUNCATE tm_super_admins, tm_password_reset_requests,
|
|
83
|
-
tm_email_change_requests, tm_ownership_transfers, tm_audit_events,
|
|
84
|
-
tm_invitations, tm_memberships, tm_organizations RESTART IDENTITY CASCADE`);
|
|
85
|
-
await pool.end();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('super-admin cannot impersonate users (no login-as endpoint exists)', async () => {
|
|
89
|
-
// There should be no /admin/users/:id/impersonate or /admin/login-as endpoint
|
|
90
|
-
const res = await request(app)
|
|
91
|
-
.post('/api/team/admin/users/3/impersonate')
|
|
92
|
-
.set('Accept', 'application/json');
|
|
93
|
-
// 404 = route doesn't exist (correct — impersonation is banned by spec)
|
|
94
|
-
expect(res.status).toBe(404);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('super-admin cannot set user passwords directly (only trigger reset link)', async () => {
|
|
98
|
-
// There should be no /admin/users/:id/set-password endpoint
|
|
99
|
-
const res = await request(app)
|
|
100
|
-
.post('/api/team/admin/users/3/set-password')
|
|
101
|
-
.send({ password: 'newpassword123' })
|
|
102
|
-
.set('Accept', 'application/json');
|
|
103
|
-
// 404 = route doesn't exist
|
|
104
|
-
expect(res.status).toBe(404);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('password-reset via admin triggers email but adapter.setUserPassword is NOT called directly', async () => {
|
|
108
|
-
vi.mocked(testAdapter.sendPasswordResetEmail).mockClear();
|
|
109
|
-
const res = await request(app)
|
|
110
|
-
.post('/api/team/admin/users/3/password-reset')
|
|
111
|
-
.send({ reason: 'user requested support' })
|
|
112
|
-
.set('Accept', 'application/json');
|
|
113
|
-
|
|
114
|
-
expect([200, 201]).toContain(res.status);
|
|
115
|
-
// sendPasswordResetEmail SHOULD be called (sends link to user)
|
|
116
|
-
expect(vi.mocked(testAdapter.sendPasswordResetEmail)).toHaveBeenCalledTimes(1);
|
|
117
|
-
// But the password reset should go to the USER's email, not be set directly
|
|
118
|
-
const callArg = vi.mocked(testAdapter.sendPasswordResetEmail).mock.calls[0]?.[0];
|
|
119
|
-
expect(callArg).toBeDefined();
|
|
120
|
-
expect(callArg?.to).toMatch(/@test\.com$/); // goes to user's email
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('super-admin cannot access product data beyond org+member info', async () => {
|
|
124
|
-
// There should be no /admin/orgs/:id/invoices, /admin/orgs/:id/projects, etc.
|
|
125
|
-
const productDataRoutes = [
|
|
126
|
-
'/api/team/admin/orgs/1/invoices',
|
|
127
|
-
'/api/team/admin/orgs/1/projects',
|
|
128
|
-
'/api/team/admin/orgs/1/pay-apps',
|
|
129
|
-
'/api/team/admin/orgs/1/documents',
|
|
130
|
-
];
|
|
131
|
-
for (const route of productDataRoutes) {
|
|
132
|
-
const res = await request(app).get(route).set('Accept', 'application/json');
|
|
133
|
-
// 404 = route doesn't exist (product data is out of scope for this module)
|
|
134
|
-
expect(res.status).toBe(404);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('super-admin actions require a reason text (cannot omit reason)', async () => {
|
|
139
|
-
// appoint-owner without reason → 400
|
|
140
|
-
const resNoReason = await request(app)
|
|
141
|
-
.post('/api/team/admin/orgs/1/appoint-owner')
|
|
142
|
-
.send({ target_user_id: 2 }) // missing reason
|
|
143
|
-
.set('Accept', 'application/json');
|
|
144
|
-
expect(resNoReason.status).toBe(400);
|
|
145
|
-
|
|
146
|
-
// lock without reason → 400
|
|
147
|
-
const resLockNoReason = await request(app)
|
|
148
|
-
.post('/api/team/admin/users/3/lock')
|
|
149
|
-
.send({}) // missing reason
|
|
150
|
-
.set('Accept', 'application/json');
|
|
151
|
-
expect(resLockNoReason.status).toBe(400);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('non-super-admin cannot access /admin routes even with org membership', async () => {
|
|
155
|
-
currentUserId = 2; // Mike is admin but NOT a super-admin
|
|
156
|
-
const res = await request(app)
|
|
157
|
-
.get('/api/team/admin/orgs')
|
|
158
|
-
.set('Accept', 'application/json');
|
|
159
|
-
expect(res.status).toBe(403);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('super-admin audit events store reason and have actor_type super_admin', async () => {
|
|
163
|
-
// Appoint new owner with reason
|
|
164
|
-
await request(app)
|
|
165
|
-
.post('/api/team/admin/orgs/1/appoint-owner')
|
|
166
|
-
.send({ target_user_id: 2, reason: 'original owner unreachable - legal request' })
|
|
167
|
-
.set('Accept', 'application/json');
|
|
168
|
-
|
|
169
|
-
const events = await pool.query(
|
|
170
|
-
`SELECT action, actor_type, reason FROM tm_audit_events WHERE org_id = 1 ORDER BY id DESC LIMIT 1`
|
|
171
|
-
);
|
|
172
|
-
if (events.rows.length > 0) {
|
|
173
|
-
const event = events.rows[0];
|
|
174
|
-
expect(event.actor_type).toBe('super_admin');
|
|
175
|
-
expect(event.reason).toContain('legal request');
|
|
176
|
-
expect(event.action).toMatch(/owner|appoint/i);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('audit events from super-admin show actor as super_admin (not their real identity)', async () => {
|
|
181
|
-
// Fire a super-admin action
|
|
182
|
-
await request(app)
|
|
183
|
-
.post('/api/team/admin/users/3/lock')
|
|
184
|
-
.send({ reason: 'abuse report' })
|
|
185
|
-
.set('Accept', 'application/json');
|
|
186
|
-
|
|
187
|
-
// Check audit event: actor_type should be 'super_admin', actor_user_id stored internally
|
|
188
|
-
// But the API route GET /orgs/:id/audit should show "Varshyl Support" not the real name
|
|
189
|
-
currentUserId = 1; // owner can see audit log
|
|
190
|
-
currentOrgId = 1;
|
|
191
|
-
const auditRes = await request(app)
|
|
192
|
-
.get('/api/team/orgs/1/audit')
|
|
193
|
-
.set('Accept', 'application/json');
|
|
194
|
-
|
|
195
|
-
if (auditRes.status === 200 && Array.isArray(auditRes.body)) {
|
|
196
|
-
const superAdminEvents = auditRes.body.filter((e: Record<string, unknown>) =>
|
|
197
|
-
e['actor_type'] === 'super_admin' || e['actor_display_name'] === 'Varshyl Support'
|
|
198
|
-
);
|
|
199
|
-
// If super-admin events are returned, the display should be "Varshyl Support"
|
|
200
|
-
for (const event of superAdminEvents) {
|
|
201
|
-
if (event['actor_display_name']) {
|
|
202
|
-
expect(event['actor_display_name']).toBe('Varshyl Support');
|
|
203
|
-
}
|
|
204
|
-
// Real user_id should NOT be exposed in the public audit API
|
|
205
|
-
expect(event).not.toHaveProperty('super_admin_user_id');
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vitest global setup — runs once before any test suite.
|
|
3
|
-
* Applies all team-management migrations so integration tests have a
|
|
4
|
-
* fully-migrated database without each test file needing to call runMigrations().
|
|
5
|
-
*/
|
|
6
|
-
import { Pool } from 'pg';
|
|
7
|
-
|
|
8
|
-
export async function setup(): Promise<void> {
|
|
9
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
10
|
-
if (!databaseUrl) return; // unit-only runs skip this
|
|
11
|
-
|
|
12
|
-
// Dynamically import to avoid issues with import.meta.url in globalSetup context
|
|
13
|
-
const { runMigrations } = await import('../../src/server/index.js');
|
|
14
|
-
const pool = new Pool({ connectionString: databaseUrl });
|
|
15
|
-
try {
|
|
16
|
-
await runMigrations(pool);
|
|
17
|
-
} finally {
|
|
18
|
-
await pool.end();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ROLE_HIERARCHY, roleAtLeast } from '../../src/server/types.js';
|
|
3
|
-
import type {
|
|
4
|
-
ServerModuleAdapter,
|
|
5
|
-
OrgRole,
|
|
6
|
-
TeamManagementConfig,
|
|
7
|
-
} from '../../src/server/types.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Compile-time shape check: build a complete valid adapter literal.
|
|
11
|
-
// If any required method is missing or mis-typed, tsc will error here.
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
const mockAdapter: ServerModuleAdapter = {
|
|
14
|
-
async getCurrentUserId(_req: unknown) {
|
|
15
|
-
return 1;
|
|
16
|
-
},
|
|
17
|
-
async getOrganizationIdForUser(_userId: number) {
|
|
18
|
-
return 1;
|
|
19
|
-
},
|
|
20
|
-
async isUserOrgAdmin(_userId: number, _orgId: number) {
|
|
21
|
-
return false;
|
|
22
|
-
},
|
|
23
|
-
logger: {
|
|
24
|
-
info: (..._args: unknown[]) => {},
|
|
25
|
-
warn: (..._args: unknown[]) => {},
|
|
26
|
-
error: (..._args: unknown[]) => {},
|
|
27
|
-
},
|
|
28
|
-
async getUserById(_userId: number) {
|
|
29
|
-
return { id: 1, email: 'test@example.com', name: 'Test User' };
|
|
30
|
-
},
|
|
31
|
-
async getUsersByIds(_userIds: number[]) {
|
|
32
|
-
return [{ id: 1, email: 'test@example.com' }];
|
|
33
|
-
},
|
|
34
|
-
async findUserByEmail(_email: string) {
|
|
35
|
-
return { id: 1, email: 'test@example.com' };
|
|
36
|
-
},
|
|
37
|
-
async createUserFromInvite(_params: { email: string; orgId: number; role: OrgRole }) {
|
|
38
|
-
return { id: 2, email: 'invited@example.com' };
|
|
39
|
-
},
|
|
40
|
-
async setUserPassword(_userId: number, _hash: string) {},
|
|
41
|
-
async hashPassword(_plain: string) {
|
|
42
|
-
return 'hashed';
|
|
43
|
-
},
|
|
44
|
-
async verifyPassword(_plain: string, _hash: string) {
|
|
45
|
-
return true;
|
|
46
|
-
},
|
|
47
|
-
async invalidateAllUserSessions(_userId: number) {},
|
|
48
|
-
async sendInviteEmail(_params: {
|
|
49
|
-
to: string;
|
|
50
|
-
orgName: string;
|
|
51
|
-
inviterName: string;
|
|
52
|
-
role: OrgRole;
|
|
53
|
-
magicLinkUrl: string;
|
|
54
|
-
code: string;
|
|
55
|
-
}) {},
|
|
56
|
-
async sendOwnershipTransferEmail(_params: {
|
|
57
|
-
to: string;
|
|
58
|
-
orgName: string;
|
|
59
|
-
fromName: string;
|
|
60
|
-
transferUrl: string;
|
|
61
|
-
}) {},
|
|
62
|
-
async sendEmailChangeVerification(_params: { to: string; verifyUrl: string }) {},
|
|
63
|
-
async sendEmailChangeOldNotice(_params: { to: string; newEmail: string; cancelUrl: string }) {},
|
|
64
|
-
async sendEmailChangedFinalNotice(_params: {
|
|
65
|
-
to: string;
|
|
66
|
-
oldEmail: string;
|
|
67
|
-
newEmail: string;
|
|
68
|
-
}) {},
|
|
69
|
-
async sendPasswordResetEmail(_params: { to: string; resetUrl: string }) {},
|
|
70
|
-
async sendOrgDeletionNotice(_params: {
|
|
71
|
-
to: string;
|
|
72
|
-
orgName: string;
|
|
73
|
-
scheduledFor: Date;
|
|
74
|
-
}) {},
|
|
75
|
-
async emitNotification(_params: { userId: number; type: string; payload: Record<string, unknown> }) {},
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Suppress unused variable warning
|
|
79
|
-
void mockAdapter;
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// Tests
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
describe('ServerModuleAdapter — shape (v0.1.0)', () => {
|
|
86
|
-
it('has getCurrentUserId', () => {
|
|
87
|
-
expect(typeof mockAdapter.getCurrentUserId).toBe('function');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('has getOrganizationIdForUser', () => {
|
|
91
|
-
expect(typeof mockAdapter.getOrganizationIdForUser).toBe('function');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('has isUserOrgAdmin', () => {
|
|
95
|
-
expect(typeof mockAdapter.isUserOrgAdmin).toBe('function');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('has logger with info, warn, error', () => {
|
|
99
|
-
expect(typeof mockAdapter.logger.info).toBe('function');
|
|
100
|
-
expect(typeof mockAdapter.logger.warn).toBe('function');
|
|
101
|
-
expect(typeof mockAdapter.logger.error).toBe('function');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('has getUserById', () => {
|
|
105
|
-
expect(typeof mockAdapter.getUserById).toBe('function');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('has getUsersByIds', () => {
|
|
109
|
-
expect(typeof mockAdapter.getUsersByIds).toBe('function');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('has findUserByEmail', () => {
|
|
113
|
-
expect(typeof mockAdapter.findUserByEmail).toBe('function');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('has createUserFromInvite', () => {
|
|
117
|
-
expect(typeof mockAdapter.createUserFromInvite).toBe('function');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('has setUserPassword', () => {
|
|
121
|
-
expect(typeof mockAdapter.setUserPassword).toBe('function');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('has hashPassword', () => {
|
|
125
|
-
expect(typeof mockAdapter.hashPassword).toBe('function');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('has verifyPassword', () => {
|
|
129
|
-
expect(typeof mockAdapter.verifyPassword).toBe('function');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('has invalidateAllUserSessions', () => {
|
|
133
|
-
expect(typeof mockAdapter.invalidateAllUserSessions).toBe('function');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('has sendInviteEmail', () => {
|
|
137
|
-
expect(typeof mockAdapter.sendInviteEmail).toBe('function');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('has sendOwnershipTransferEmail', () => {
|
|
141
|
-
expect(typeof mockAdapter.sendOwnershipTransferEmail).toBe('function');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('has sendEmailChangeVerification', () => {
|
|
145
|
-
expect(typeof mockAdapter.sendEmailChangeVerification).toBe('function');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('has sendEmailChangeOldNotice', () => {
|
|
149
|
-
expect(typeof mockAdapter.sendEmailChangeOldNotice).toBe('function');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('has sendEmailChangedFinalNotice', () => {
|
|
153
|
-
expect(typeof mockAdapter.sendEmailChangedFinalNotice).toBe('function');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('has sendPasswordResetEmail', () => {
|
|
157
|
-
expect(typeof mockAdapter.sendPasswordResetEmail).toBe('function');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('has sendOrgDeletionNotice', () => {
|
|
161
|
-
expect(typeof mockAdapter.sendOrgDeletionNotice).toBe('function');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('has emitNotification', () => {
|
|
165
|
-
expect(typeof mockAdapter.emitNotification).toBe('function');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('TeamManagementConfig — feature flags', () => {
|
|
170
|
-
it('accepts config with all flags enabled', () => {
|
|
171
|
-
const config: TeamManagementConfig = {
|
|
172
|
-
featureFlags: {
|
|
173
|
-
enableInvites: true,
|
|
174
|
-
enableAuditLog: true,
|
|
175
|
-
enableOwnershipTransfer: true,
|
|
176
|
-
enableEmailChange: true,
|
|
177
|
-
enablePasswordReset: true,
|
|
178
|
-
enableSuperAdmin: true,
|
|
179
|
-
enableSharedAccess: true,
|
|
180
|
-
enableHardDelete: true,
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
expect(config.featureFlags?.enableInvites).toBe(true);
|
|
184
|
-
expect(config.featureFlags?.enableAuditLog).toBe(true);
|
|
185
|
-
expect(config.featureFlags?.enableOwnershipTransfer).toBe(true);
|
|
186
|
-
expect(config.featureFlags?.enableEmailChange).toBe(true);
|
|
187
|
-
expect(config.featureFlags?.enablePasswordReset).toBe(true);
|
|
188
|
-
expect(config.featureFlags?.enableSuperAdmin).toBe(true);
|
|
189
|
-
expect(config.featureFlags?.enableSharedAccess).toBe(true);
|
|
190
|
-
expect(config.featureFlags?.enableHardDelete).toBe(true);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('accepts config with all flags disabled', () => {
|
|
194
|
-
const config: TeamManagementConfig = {
|
|
195
|
-
featureFlags: {
|
|
196
|
-
enableInvites: false,
|
|
197
|
-
enableAuditLog: false,
|
|
198
|
-
enableOwnershipTransfer: false,
|
|
199
|
-
enableEmailChange: false,
|
|
200
|
-
enablePasswordReset: false,
|
|
201
|
-
enableSuperAdmin: false,
|
|
202
|
-
enableSharedAccess: false,
|
|
203
|
-
enableHardDelete: false,
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
expect(config.featureFlags?.enableInvites).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('accepts config with no featureFlags (all optional)', () => {
|
|
210
|
-
const config: TeamManagementConfig = {
|
|
211
|
-
};
|
|
212
|
-
expect(config.featureFlags).toBeUndefined();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('accepts config with partial featureFlags', () => {
|
|
216
|
-
const config: TeamManagementConfig = {
|
|
217
|
-
featureFlags: {
|
|
218
|
-
enableInvites: true,
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
expect(config.featureFlags?.enableInvites).toBe(true);
|
|
222
|
-
expect(config.featureFlags?.enableAuditLog).toBeUndefined();
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('OrgRole — valid type values', () => {
|
|
227
|
-
it('owner is a valid OrgRole', () => {
|
|
228
|
-
const role: OrgRole = 'owner';
|
|
229
|
-
expect(role).toBe('owner');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('admin is a valid OrgRole', () => {
|
|
233
|
-
const role: OrgRole = 'admin';
|
|
234
|
-
expect(role).toBe('admin');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('member is a valid OrgRole', () => {
|
|
238
|
-
const role: OrgRole = 'member';
|
|
239
|
-
expect(role).toBe('member');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('viewer is a valid OrgRole', () => {
|
|
243
|
-
const role: OrgRole = 'viewer';
|
|
244
|
-
expect(role).toBe('viewer');
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('ROLE_HIERARCHY — numeric values', () => {
|
|
249
|
-
it('owner = 40', () => {
|
|
250
|
-
expect(ROLE_HIERARCHY['owner']).toBe(40);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('admin = 30', () => {
|
|
254
|
-
expect(ROLE_HIERARCHY['admin']).toBe(30);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('member = 20', () => {
|
|
258
|
-
expect(ROLE_HIERARCHY['member']).toBe(20);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('viewer = 10', () => {
|
|
262
|
-
expect(ROLE_HIERARCHY['viewer']).toBe(10);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe('roleAtLeast — permission matrix', () => {
|
|
267
|
-
it('roleAtLeast("owner", "viewer") = true', () => {
|
|
268
|
-
expect(roleAtLeast('owner', 'viewer')).toBe(true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('roleAtLeast("viewer", "admin") = false', () => {
|
|
272
|
-
expect(roleAtLeast('viewer', 'admin')).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('roleAtLeast("admin", "admin") = true (equal roles)', () => {
|
|
276
|
-
expect(roleAtLeast('admin', 'admin')).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('roleAtLeast("owner", "owner") = true', () => {
|
|
280
|
-
expect(roleAtLeast('owner', 'owner')).toBe(true);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('roleAtLeast("owner", "admin") = true', () => {
|
|
284
|
-
expect(roleAtLeast('owner', 'admin')).toBe(true);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('roleAtLeast("owner", "member") = true', () => {
|
|
288
|
-
expect(roleAtLeast('owner', 'member')).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('roleAtLeast("admin", "member") = true', () => {
|
|
292
|
-
expect(roleAtLeast('admin', 'member')).toBe(true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('roleAtLeast("admin", "viewer") = true', () => {
|
|
296
|
-
expect(roleAtLeast('admin', 'viewer')).toBe(true);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('roleAtLeast("member", "member") = true', () => {
|
|
300
|
-
expect(roleAtLeast('member', 'member')).toBe(true);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('roleAtLeast("member", "viewer") = true', () => {
|
|
304
|
-
expect(roleAtLeast('member', 'viewer')).toBe(true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('roleAtLeast("viewer", "viewer") = true', () => {
|
|
308
|
-
expect(roleAtLeast('viewer', 'viewer')).toBe(true);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('roleAtLeast("viewer", "member") = false', () => {
|
|
312
|
-
expect(roleAtLeast('viewer', 'member')).toBe(false);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('roleAtLeast("viewer", "owner") = false', () => {
|
|
316
|
-
expect(roleAtLeast('viewer', 'owner')).toBe(false);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('roleAtLeast("member", "admin") = false', () => {
|
|
320
|
-
expect(roleAtLeast('member', 'admin')).toBe(false);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('roleAtLeast("member", "owner") = false', () => {
|
|
324
|
-
expect(roleAtLeast('member', 'owner')).toBe(false);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('roleAtLeast("admin", "owner") = false', () => {
|
|
328
|
-
expect(roleAtLeast('admin', 'owner')).toBe(false);
|
|
329
|
-
});
|
|
330
|
-
});
|