feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
@@ -0,0 +1,143 @@
import {
Controller,
Get,
Post,
Put,
Body,
UseGuards,
Param,
Delete,
HttpCode,
Patch,
ForbiddenException,
Query,
} from '@nestjs/common';
import { SuperAdminService } from './super-admin.service';
import { TenantService } from '../tenant/tenant.service';
import { CombinedAuthGuard } from '../auth/combined-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
import { UserRole } from '../user/user-role.enum';
import { I18nService } from '../i18n/i18n.service';
@Controller('v1/tenants')
@UseGuards(CombinedAuthGuard, RolesGuard)
@Roles(UserRole.SUPER_ADMIN)
export class SuperAdminController {
constructor(
private readonly superAdminService: SuperAdminService,
private readonly tenantService: TenantService,
private readonly i18nService: I18nService,
) {}
@Get()
async getTenants(
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.superAdminService.getAllTenants(
page ? parseInt(page) : undefined,
limit ? parseInt(limit) : undefined,
);
}
@Post()
async createTenant(
@Body()
body: {
name: string;
domain?: string;
adminUserId?: string;
parentId?: string;
},
) {
return this.superAdminService.createTenant(
body.name,
body.domain,
body.adminUserId,
body.parentId,
);
}
@Put(':tenantId/admin')
async bindTenantAdmin(
@Param('tenantId') tenantId: string,
@Body() body: { userId: string },
) {
return this.superAdminService.assignUserToTenant(body.userId, tenantId);
}
@Post(':tenantId/admin/new')
async createTenantAdmin(
@Param('tenantId') tenantId: string,
@Body() body: { username: string; password?: string },
) {
return this.superAdminService.createTenantAdmin(
tenantId,
body.username,
body.password,
);
}
@Put(':tenantId')
async updateTenant(
@Param('tenantId') tenantId: string,
@Body() body: { name?: string; domain?: string; parentId?: string },
) {
return this.superAdminService.updateTenant(tenantId, body);
}
@Delete(':tenantId')
async deleteTenant(@Param('tenantId') tenantId: string) {
return this.superAdminService.deleteTenant(tenantId);
}
// --- Member Management ---
@Get(':tenantId/members')
async getMembers(
@Param('tenantId') tenantId: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
const p = page ? parseInt(page) : undefined;
const l = limit ? parseInt(limit) : undefined;
return this.tenantService.getMembers(tenantId, p, l);
}
@Post(':tenantId/members')
async addMember(
@Param('tenantId') tenantId: string,
@Body() body: { userId: string; role?: string },
) {
return this.tenantService.addMember(tenantId, body.userId, body.role);
}
@Delete(':tenantId/members/:userId')
@HttpCode(204)
async removeMember(
@Param('tenantId') tenantId: string,
@Param('userId') userId: string,
) {
await this.tenantService.removeMember(tenantId, userId);
}
@Patch(':tenantId/members/:userId')
async updateMemberRole(
@Param('tenantId') tenantId: string,
@Param('userId') userId: string,
@Body() body: { role: string },
) {
if (body.role !== UserRole.USER && body.role !== UserRole.TENANT_ADMIN) {
throw new ForbiddenException(
this.i18nService.getErrorMessage('invalidMemberRole'),
);
}
return this.tenantService.updateMemberRole(tenantId, userId, body.role);
}
@Get(':tenantId/members/ids')
async getMemberIds(@Param('tenantId') tenantId: string) {
return this.tenantService.getMemberIds(tenantId);
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { SuperAdminController } from './super-admin.controller';
import { SuperAdminService } from './super-admin.service';
import { TenantModule } from '../tenant/tenant.module';
import { UserModule } from '../user/user.module';
@Module({
imports: [TenantModule, UserModule],
controllers: [SuperAdminController],
providers: [SuperAdminService],
})
export class SuperAdminModule {}
@@ -0,0 +1,88 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { TenantService } from '../tenant/tenant.service';
import { UserService } from '../user/user.service';
import { User } from '../user/user.entity';
import { UserRole } from '../user/user-role.enum';
@Injectable()
export class SuperAdminService {
constructor(
private readonly tenantService: TenantService,
private readonly userService: UserService,
) {}
async getAllTenants(page?: number, limit?: number) {
return this.tenantService.findAll(page, limit);
}
async createTenant(
name: string,
domain?: string,
adminUserId?: string,
parentId?: string,
) {
const tenant = await this.tenantService.create(name, domain, parentId);
if (adminUserId) {
await this.tenantService.addMember(
tenant.id,
adminUserId,
UserRole.TENANT_ADMIN,
);
}
return tenant;
}
async assignUserToTenant(
userId: string,
tenantId: string,
role: UserRole = UserRole.TENANT_ADMIN,
) {
// Find existing members of this tenant
const members = await this.tenantService.getMembers(tenantId);
// Remove existing admins from this tenant (unlinking them, not changing their role)
for (const member of members.data) {
if (
member.role === UserRole.TENANT_ADMIN ||
member.role === UserRole.SUPER_ADMIN
) {
await this.tenantService.removeMember(tenantId, member.userId);
}
}
// Add the new admin association for this tenant
return this.tenantService.addMember(tenantId, userId, role);
}
async createTenantAdmin(
tenantId: string,
username: string,
password?: string,
) {
const defaultPassword = password || Math.random().toString(36).slice(-8);
const result = await this.userService.createUser(
username,
defaultPassword,
false, // isAdmin
tenantId,
username, // displayName
);
return {
user: result.user,
defaultPassword: defaultPassword,
};
}
async updateTenant(
tenantId: string,
data: { name?: string; domain?: string; parentId?: string },
) {
return this.tenantService.update(tenantId, data);
}
async deleteTenant(tenantId: string) {
return this.tenantService.remove(tenantId);
}
// NOTE: Model Management would be added here depending on ModelService functionality
}