forked from hangshuo652/aurak
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:
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Response } from 'express';
|
||||
import { AdminService } from './admin.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';
|
||||
|
||||
@Controller('v1/admin')
|
||||
@UseGuards(CombinedAuthGuard, RolesGuard)
|
||||
export class AdminController {
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
@Get('users')
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
async getUsers(
|
||||
@Request() req: any,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
|
||||
return this.adminService.getTenantUsers(
|
||||
isSuperAdmin ? undefined : req.user.tenantId,
|
||||
page ? parseInt(page) : undefined,
|
||||
limit ? parseInt(limit) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('users/export')
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
async getUsersExport(@Request() req: any, @Res() res: Response) {
|
||||
const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
|
||||
const buffer = await this.adminService.exportUsers(
|
||||
isSuperAdmin ? undefined : req.user.tenantId,
|
||||
);
|
||||
res.set({
|
||||
'Content-Type':
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition': 'attachment; filename="users_export.xlsx"',
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
res.end(buffer);
|
||||
}
|
||||
|
||||
@Post('users/import')
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importUsers(@Request() req: any, @UploadedFile() file: any) {
|
||||
const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
|
||||
return this.adminService.importUsers(
|
||||
isSuperAdmin ? undefined : req.user.tenantId,
|
||||
file,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('settings')
|
||||
async getSettings(@Request() req: any) {
|
||||
return this.adminService.getTenantSettings(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Put('settings')
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
async updateSettings(@Request() req: any, @Body() body: any) {
|
||||
return this.adminService.updateTenantSettings(req.user.tenantId, body);
|
||||
}
|
||||
|
||||
@Get('pending-shares')
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
async getPendingShares(@Request() req: any) {
|
||||
return this.adminService.getPendingShares(req.user.tenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule, TenantModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import { I18nService } from '../i18n/i18n.service';
|
||||
|
||||
interface UserImportRow {
|
||||
Username?: string | number;
|
||||
username?: string | number;
|
||||
DisplayName?: string | number;
|
||||
displayName?: string | number;
|
||||
Name?: string | number;
|
||||
name?: string | number;
|
||||
Password?: string | number;
|
||||
password?: string | number;
|
||||
IsAdmin?: string | number | boolean;
|
||||
isAdmin?: string | number | boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly tenantService: TenantService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async getTenantUsers(tenantId?: string, page?: number, limit?: number) {
|
||||
if (!tenantId) {
|
||||
return this.userService.findAll(page, limit);
|
||||
}
|
||||
return this.userService.findByTenantId(tenantId, page, limit);
|
||||
}
|
||||
|
||||
async exportUsers(tenantId?: string): Promise<Buffer> {
|
||||
const { data: users } = tenantId
|
||||
? await this.userService.findByTenantId(tenantId)
|
||||
: await this.userService.findAll();
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(
|
||||
users.map((u) => ({
|
||||
Username: u.username,
|
||||
DisplayName: u.displayName || '',
|
||||
IsAdmin: u.isAdmin ? 'Yes' : 'No',
|
||||
CreatedAt: u.createdAt,
|
||||
Password: '', // Placeholder for new users
|
||||
})),
|
||||
);
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Users');
|
||||
|
||||
return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
}
|
||||
|
||||
async importUsers(tenantId?: string, file?: any) {
|
||||
if (!file)
|
||||
throw new BadRequestException(
|
||||
this.i18nService.getMessage('uploadNoFile'),
|
||||
);
|
||||
|
||||
const workbook = XLSX.read(file.buffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json<UserImportRow>(worksheet);
|
||||
|
||||
const results = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
for (const row of data) {
|
||||
try {
|
||||
const username = (row.Username || row.username)?.toString();
|
||||
const displayName = (
|
||||
row.DisplayName ||
|
||||
row.displayName ||
|
||||
row.Name ||
|
||||
row.name
|
||||
)?.toString();
|
||||
const password = (row.Password || row.password)?.toString();
|
||||
const isAdminStr = (row.IsAdmin || row.isAdmin || 'No').toString();
|
||||
const isAdmin =
|
||||
isAdminStr.toLowerCase() === 'yes' ||
|
||||
isAdminStr === 'true' ||
|
||||
isAdminStr === '1';
|
||||
|
||||
if (!username) {
|
||||
throw new Error(this.i18nService.getMessage('usernameRequired'));
|
||||
}
|
||||
|
||||
const existingUser = await this.userService.findOneByUsername(username);
|
||||
|
||||
if (existingUser) {
|
||||
await this.userService.updateUser(existingUser.id, {
|
||||
displayName: displayName || existingUser.displayName,
|
||||
password: password || undefined,
|
||||
// We avoid changing isAdmin status via import for security unless explicitly required
|
||||
});
|
||||
} else {
|
||||
if (!password) {
|
||||
throw new Error(
|
||||
this.i18nService.formatMessage('passwordRequiredForNewUser', {
|
||||
username,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await this.userService.createUser(
|
||||
username,
|
||||
password,
|
||||
isAdmin,
|
||||
tenantId,
|
||||
displayName,
|
||||
);
|
||||
}
|
||||
results.success++;
|
||||
} catch (e: any) {
|
||||
results.failed++;
|
||||
results.errors.push(`${row.Username || 'Unknown'}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getTenantSettings(tenantId: string) {
|
||||
return this.tenantService.getSettings(tenantId);
|
||||
}
|
||||
|
||||
async updateTenantSettings(tenantId: string, data: any) {
|
||||
return this.tenantService.updateSettings(tenantId, data);
|
||||
}
|
||||
|
||||
// Notebook sharing approval and model assignments would go here
|
||||
async getPendingShares(tenantId: string) {
|
||||
// Mock implementation for pending shares to satisfy UI.
|
||||
// Needs proper schema/entity support in the future.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user