forked from wangziqi/gongxue-base
feat: add platform staff management
This commit is contained in:
@@ -22,6 +22,7 @@ const AUTH_USER_ID = '00000000-0000-0000-0000-00000000a101';
|
||||
const AUTH_TENANT_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a102';
|
||||
const AUTH_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a999';
|
||||
const AUTH_RESTRICTED_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a998';
|
||||
const AUTH_PLATFORM_STAFF_USER_ID = '00000000-0000-4000-8000-00000000a997';
|
||||
const AUTH_JWT_SECRET = 'development-jwt-secret-change-me';
|
||||
const START_SERVER = process.argv.includes('--start-server');
|
||||
const ENABLE_REAL_STORAGE_SIGN_TESTS = process.env.ENABLE_REAL_STORAGE_SIGN_TESTS === 'true';
|
||||
@@ -1179,6 +1180,247 @@ async function testPlatformAdminPermissions() {
|
||||
assert.equal(paymentDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'manual service-fee payment must require billing payment permission');
|
||||
}
|
||||
|
||||
async function testPlatformStaffManagement() {
|
||||
const adminHeaders = { 'x-platform-admin-key': 'local-platform-admin-key' };
|
||||
const staffSuffix = Date.now().toString(36);
|
||||
const staffEmail = `platform.staff.${staffSuffix}@example.test`;
|
||||
const staffPhone = `139${String(Date.now()).slice(-8)}`;
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
await pool.query(
|
||||
`
|
||||
insert into auth.users (
|
||||
id, aud, role, phone, email, phone_confirmed_at, email_confirmed_at,
|
||||
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
|
||||
)
|
||||
values (
|
||||
$1, 'authenticated', 'authenticated', $2, $3,
|
||||
now(), now(),
|
||||
'{"provider":"phone","providers":["phone"],"app_role":"platform_admin"}'::jsonb,
|
||||
'{}'::jsonb, now(), now()
|
||||
)
|
||||
on conflict (id)
|
||||
do update set phone = excluded.phone,
|
||||
email = excluded.email,
|
||||
raw_app_meta_data = excluded.raw_app_meta_data,
|
||||
updated_at = now()
|
||||
`,
|
||||
[AUTH_PLATFORM_STAFF_USER_ID, staffPhone, staffEmail],
|
||||
);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
const created = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
|
||||
username: 'platform_staff_operator',
|
||||
email: staffEmail,
|
||||
phone: staffPhone,
|
||||
name: 'Platform Staff Operator',
|
||||
status: 'active',
|
||||
platformPermissions: {
|
||||
'platform:staff:read': true,
|
||||
'platform:overview:read': true,
|
||||
'platform:tenant:read': true,
|
||||
},
|
||||
metadata: {
|
||||
title: 'operations',
|
||||
secretToken: 'should-not-be-returned-through-audit',
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.ok(created.item?.id, 'platform admin should create platform staff');
|
||||
assert.equal(created.item?.platformPermissions?.['platform:tenant:read'], true, 'platform staff response should include granted permissions');
|
||||
assert.ok(!JSON.stringify(created).includes('local-platform-admin-key'), 'platform staff response must not leak platform key');
|
||||
|
||||
const invalidPermission = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
|
||||
username: 'invalid_platform_staff',
|
||||
name: 'Invalid Platform Staff',
|
||||
platformPermissions: {
|
||||
'platform:unknown:write': true,
|
||||
},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidPermission.code, 'INVALID_PLATFORM_PERMISSION', 'platform staff permissions should reject unknown permission keys');
|
||||
|
||||
const missingAuthUser = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
authUserId: '00000000-0000-4000-8000-00000000dead',
|
||||
username: 'missing_auth_platform_staff',
|
||||
name: 'Missing Auth Platform Staff',
|
||||
platformPermissions: {
|
||||
'platform:overview:read': true,
|
||||
},
|
||||
},
|
||||
expectStatus: 404,
|
||||
});
|
||||
assert.equal(missingAuthUser.code, 'AUTH_USER_NOT_FOUND', 'platform staff must bind an existing Supabase Auth user');
|
||||
|
||||
const missingAuthBinding = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
username: 'unbound_platform_staff',
|
||||
name: 'Unbound Platform Staff',
|
||||
platformPermissions: {
|
||||
'platform:overview:read': true,
|
||||
},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(missingAuthBinding.code, 'REQUIRED_FIELD', 'platform staff authUserId should be required');
|
||||
|
||||
const emptyPermissionDenied = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
authUserId: AUTH_PLATFORM_STAFF_USER_ID,
|
||||
username: 'empty_permission_platform_staff',
|
||||
name: 'Empty Permission Platform Staff',
|
||||
status: 'active',
|
||||
platformPermissions: {},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(emptyPermissionDenied.code, 'PLATFORM_PERMISSION_EMPTY', 'active platform staff should require explicit permissions');
|
||||
|
||||
const staffList = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
query: { q: 'platform_staff_operator' },
|
||||
});
|
||||
assert.ok(staffList.items?.some(item => item.id === created.item.id), 'platform staff list should include created staff');
|
||||
|
||||
const staffJwt = await createSupabaseJwt(AUTH_PLATFORM_STAFF_USER_ID, {
|
||||
phone: staffPhone,
|
||||
appRole: 'platform_admin',
|
||||
tenantId: false,
|
||||
});
|
||||
const staffHeaders = { authorization: `Bearer ${staffJwt}` };
|
||||
|
||||
const staffPermissions = await request('/api/platform-admin/permissions', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: staffHeaders,
|
||||
});
|
||||
assert.equal(staffPermissions.item?.effective?.['platform:staff:read'], true, 'staff should expose its staff read permission');
|
||||
assert.equal(staffPermissions.item?.effective?.['platform:tenant:write'], false, 'staff should not expose ungranted tenant write permission');
|
||||
|
||||
const staffCanListTenants = await request('/api/platform-admin/tenants', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: staffHeaders,
|
||||
});
|
||||
assert.ok(Array.isArray(staffCanListTenants.items), 'staff should list tenants with tenant read permission');
|
||||
|
||||
const staffCreateTenantDenied = await request('/api/platform-admin/tenants', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: staffHeaders,
|
||||
method: 'POST',
|
||||
body: {
|
||||
slug: `staff-denied-${Date.now().toString(36)}`,
|
||||
name: 'Staff Denied Tenant',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(staffCreateTenantDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'platform staff should not create tenants without tenant write permission');
|
||||
|
||||
const staffWriteDenied = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: staffHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
username: 'staff_cannot_create_staff',
|
||||
name: 'Staff Cannot Create Staff',
|
||||
platformPermissions: { 'platform:overview:read': true },
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(staffWriteDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'staff write should require explicit platform staff write permission');
|
||||
|
||||
const platformAdminJwt = await createSupabaseJwt(AUTH_PLATFORM_ADMIN_USER_ID, {
|
||||
phone: '13999999999',
|
||||
appRole: 'platform_admin',
|
||||
tenantId: false,
|
||||
});
|
||||
const platformAdminHeaders = { authorization: `Bearer ${platformAdminJwt}` };
|
||||
|
||||
const selfDowngradeDenied = await request('/api/platform-admin/staff', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: platformAdminHeaders,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: '00000000-0000-0000-0000-000000000999',
|
||||
authUserId: AUTH_PLATFORM_ADMIN_USER_ID,
|
||||
username: 'smoke_platform_admin',
|
||||
phone: '13999999999',
|
||||
name: 'Smoke Platform Admin',
|
||||
status: 'active',
|
||||
platformPermissions: { 'platform:overview:read': true },
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(selfDowngradeDenied.code, 'CANNOT_DOWNGRADE_SELF', 'platform admin should not remove its own super permission');
|
||||
|
||||
const disabled = await request('/api/platform-admin/staff/status', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
staffId: created.item.id,
|
||||
status: 'disabled',
|
||||
reason: 'integration disable test',
|
||||
revokeSessions: true,
|
||||
},
|
||||
});
|
||||
assert.equal(disabled.item?.status, 'disabled', 'platform admin should disable platform staff');
|
||||
|
||||
const disabledStaffDenied = await request('/api/platform-admin/overview', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: staffHeaders,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(disabledStaffDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'disabled platform staff JWT should no longer authenticate as platform admin');
|
||||
|
||||
const audit = await request('/api/platform-admin/audit-logs', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: adminHeaders,
|
||||
query: { targetType: 'platform_user', q: 'platform.staff', limit: 20 },
|
||||
});
|
||||
assert.ok(
|
||||
audit.items?.some(item => item.targetId === created.item.id && item.action === 'platform.staff.status_updated'),
|
||||
'platform staff status changes should be audited',
|
||||
);
|
||||
assert.ok(!JSON.stringify(audit).includes('should-not-be-returned-through-audit'), 'platform staff audit should not leak token-like metadata');
|
||||
}
|
||||
|
||||
async function testSupabaseJwksIdentity() {
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||
const publicJwk = await exportJWK(publicKey);
|
||||
@@ -9014,6 +9256,7 @@ async function main() {
|
||||
await check('phone binding', testPhoneBinding);
|
||||
await check('Supabase JWT identity', testSupabaseJwtIdentity);
|
||||
await check('platform admin permissions', testPlatformAdminPermissions);
|
||||
await check('platform staff management', testPlatformStaffManagement);
|
||||
await check('Supabase JWKS JWT identity', testSupabaseJwksIdentity);
|
||||
await check('legacy auth headers disabled', testLegacyAuthHeadersDisabled);
|
||||
await check('platform tenant operations and audit', testPlatformTenantOperationsAndAudit);
|
||||
|
||||
Reference in New Issue
Block a user