- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
InternalServerErrorException,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { eq, type Db, users as usersTable } from '@mosaicstack/db';
|
|
import type { Auth } from '@mosaicstack/auth';
|
|
import { AUTH } from '../auth/auth.tokens.js';
|
|
import { DB } from '../database/database.module.js';
|
|
import { AdminGuard } from './admin.guard.js';
|
|
import type {
|
|
BanUserDto,
|
|
CreateUserDto,
|
|
UpdateUserRoleDto,
|
|
UserDto,
|
|
UserListDto,
|
|
} from './admin.dto.js';
|
|
|
|
type UserRow = typeof usersTable.$inferSelect;
|
|
|
|
function toUserDto(u: UserRow): UserDto {
|
|
return {
|
|
id: u.id,
|
|
name: u.name,
|
|
email: u.email,
|
|
role: u.role,
|
|
banned: u.banned ?? false,
|
|
banReason: u.banReason ?? null,
|
|
createdAt: u.createdAt.toISOString(),
|
|
updatedAt: u.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
async function requireUpdated(
|
|
db: Db,
|
|
id: string,
|
|
update: Partial<Omit<UserRow, 'id' | 'createdAt'>>,
|
|
): Promise<UserDto> {
|
|
const [updated] = await db
|
|
.update(usersTable)
|
|
.set({ ...update, updatedAt: new Date() })
|
|
.where(eq(usersTable.id, id))
|
|
.returning();
|
|
if (!updated) throw new InternalServerErrorException('Update returned no rows');
|
|
return toUserDto(updated);
|
|
}
|
|
|
|
@Controller('api/admin/users')
|
|
@UseGuards(AdminGuard)
|
|
export class AdminController {
|
|
constructor(
|
|
@Inject(DB) private readonly db: Db,
|
|
@Inject(AUTH) private readonly auth: Auth,
|
|
) {}
|
|
|
|
@Get()
|
|
async listUsers(): Promise<UserListDto> {
|
|
const rows = await this.db.select().from(usersTable).orderBy(usersTable.createdAt);
|
|
const userList: UserDto[] = rows.map(toUserDto);
|
|
return { users: userList, total: userList.length };
|
|
}
|
|
|
|
@Get(':id')
|
|
async getUser(@Param('id') id: string): Promise<UserDto> {
|
|
const [user] = await this.db.select().from(usersTable).where(eq(usersTable.id, id)).limit(1);
|
|
if (!user) throw new NotFoundException('User not found');
|
|
return toUserDto(user);
|
|
}
|
|
|
|
@Post()
|
|
async createUser(@Body() body: CreateUserDto): Promise<UserDto> {
|
|
// Use auth API to create user so password is properly hashed
|
|
const authApi = this.auth.api as unknown as {
|
|
createUser: (opts: {
|
|
body: { name: string; email: string; password: string; role?: string };
|
|
}) => Promise<{
|
|
user: { id: string; name: string; email: string; createdAt: unknown; updatedAt: unknown };
|
|
}>;
|
|
};
|
|
|
|
const result = await authApi.createUser({
|
|
body: {
|
|
name: body.name,
|
|
email: body.email,
|
|
password: body.password,
|
|
role: body.role ?? 'member',
|
|
},
|
|
});
|
|
|
|
// Re-fetch from DB to get full row with our schema
|
|
const [user] = await this.db
|
|
.select()
|
|
.from(usersTable)
|
|
.where(eq(usersTable.id, result.user.id))
|
|
.limit(1);
|
|
|
|
if (!user) throw new InternalServerErrorException('User created but not found in DB');
|
|
return toUserDto(user);
|
|
}
|
|
|
|
@Patch(':id/role')
|
|
async setRole(@Param('id') id: string, @Body() body: UpdateUserRoleDto): Promise<UserDto> {
|
|
await this.ensureExists(id);
|
|
return requireUpdated(this.db, id, { role: body.role });
|
|
}
|
|
|
|
@Post(':id/ban')
|
|
@HttpCode(HttpStatus.OK)
|
|
async banUser(@Param('id') id: string, @Body() body: BanUserDto): Promise<UserDto> {
|
|
await this.ensureExists(id);
|
|
return requireUpdated(this.db, id, { banned: true, banReason: body.reason ?? null });
|
|
}
|
|
|
|
@Post(':id/unban')
|
|
@HttpCode(HttpStatus.OK)
|
|
async unbanUser(@Param('id') id: string): Promise<UserDto> {
|
|
await this.ensureExists(id);
|
|
return requireUpdated(this.db, id, { banned: false, banReason: null, banExpires: null });
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async deleteUser(@Param('id') id: string): Promise<void> {
|
|
await this.ensureExists(id);
|
|
await this.db.delete(usersTable).where(eq(usersTable.id, id));
|
|
}
|
|
|
|
private async ensureExists(id: string): Promise<void> {
|
|
const [existing] = await this.db
|
|
.select({ id: usersTable.id })
|
|
.from(usersTable)
|
|
.where(eq(usersTable.id, id))
|
|
.limit(1);
|
|
if (!existing) throw new NotFoundException('User not found');
|
|
}
|
|
}
|