feat: org admin can create chapter (tenant) admins

Add POST /org/tenants/:id/admins so org admins can provision the first
login for a chapter's admin team. New chapter admins can then sign in at
/login with email + password.

- OrgService.createTenantAdmin — validates chapter belongs to org, hashes
  password, creates Admin record
- OrgController POST tenants/:id/admins endpoint
- BFF route /api/org/tenants/[id]/admins
- Add Admin form on /org/tenants/[id] page (email, password, role select)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 17:21:50 +05:30
parent b26a635283
commit 6bff55db64
4 changed files with 116 additions and 1 deletions
@@ -32,6 +32,15 @@ export class OrgController {
return this.orgService.getTenant(admin.organizationId, id);
}
@Post('tenants/:id/admins')
createTenantAdmin(
@CurrentOrgAdmin() admin: OrgAdminJwtPayload,
@Param('id') id: string,
@Body() body: { email: string; password: string; role?: string },
) {
return this.orgService.createTenantAdmin(admin.organizationId, id, body.email, body.password, body.role);
}
@Get('rules')
getRules(@CurrentOrgAdmin() admin: OrgAdminJwtPayload) {
return this.orgService.getRules(admin.organizationId);
+22
View File
@@ -162,4 +162,26 @@ export class OrgService {
if (!rule) throw new NotFoundException('Rule not found');
await this.prisma.orgRule.delete({ where: { id: ruleId } });
}
async createTenantAdmin(
organizationId: string,
tenantId: string,
email: string,
password: string,
role?: string,
) {
const tenant = await this.prisma.tenant.findFirst({ where: { id: tenantId, organizationId } });
if (!tenant) throw new NotFoundException('Chapter not found or does not belong to this org');
const existing = await this.prisma.admin.findUnique({
where: { tenantId_email: { tenantId, email } },
});
if (existing) throw new ConflictException('An admin with this email already exists for this chapter');
const passwordHash = await bcrypt.hash(password, 10);
return this.prisma.admin.create({
data: { tenantId, email, passwordHash, ...(role ? { role: role as any } : {}) },
select: { id: true, email: true, role: true, createdAt: true },
});
}
}