91 lines
2.3 KiB
TypeScript
91 lines
2.3 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
Param,
|
|
Post,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { randomBytes, createHash } from 'node:crypto';
|
|
import { eq, type Db, adminTokens } from '@mosaic/db';
|
|
import { v4 as uuid } from 'uuid';
|
|
import { DB } from '../database/database.module.js';
|
|
import { AdminGuard } from './admin.guard.js';
|
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
|
import type {
|
|
CreateTokenDto,
|
|
TokenCreatedDto,
|
|
TokenDto,
|
|
TokenListDto,
|
|
} from './admin-tokens.dto.js';
|
|
|
|
function hashToken(plaintext: string): string {
|
|
return createHash('sha256').update(plaintext).digest('hex');
|
|
}
|
|
|
|
function toTokenDto(row: typeof adminTokens.$inferSelect): TokenDto {
|
|
return {
|
|
id: row.id,
|
|
label: row.label,
|
|
scope: row.scope,
|
|
expiresAt: row.expiresAt?.toISOString() ?? null,
|
|
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
|
createdAt: row.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
@Controller('api/admin/tokens')
|
|
@UseGuards(AdminGuard)
|
|
export class AdminTokensController {
|
|
constructor(@Inject(DB) private readonly db: Db) {}
|
|
|
|
@Post()
|
|
async create(
|
|
@Body() dto: CreateTokenDto,
|
|
@CurrentUser() user: { id: string },
|
|
): Promise<TokenCreatedDto> {
|
|
const plaintext = randomBytes(32).toString('hex');
|
|
const tokenHash = hashToken(plaintext);
|
|
const id = uuid();
|
|
|
|
const expiresAt = dto.expiresInDays
|
|
? new Date(Date.now() + dto.expiresInDays * 24 * 60 * 60 * 1000)
|
|
: null;
|
|
|
|
const [row] = await this.db
|
|
.insert(adminTokens)
|
|
.values({
|
|
id,
|
|
userId: user.id,
|
|
tokenHash,
|
|
label: dto.label ?? 'CLI token',
|
|
scope: dto.scope ?? 'admin',
|
|
expiresAt,
|
|
})
|
|
.returning();
|
|
|
|
return { ...toTokenDto(row!), plaintext };
|
|
}
|
|
|
|
@Get()
|
|
async list(@CurrentUser() user: { id: string }): Promise<TokenListDto> {
|
|
const rows = await this.db
|
|
.select()
|
|
.from(adminTokens)
|
|
.where(eq(adminTokens.userId, user.id))
|
|
.orderBy(adminTokens.createdAt);
|
|
|
|
return { tokens: rows.map(toTokenDto), total: rows.length };
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async revoke(@Param('id') id: string, @CurrentUser() _user: { id: string }): Promise<void> {
|
|
await this.db.delete(adminTokens).where(eq(adminTokens.id, id));
|
|
}
|
|
}
|