mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
refactor(tokens): async DB-touching methods; the auth token handlers await them
This commit is contained in:
@@ -306,11 +306,11 @@ export class AdminController {
|
||||
|
||||
// ── MCP tokens / OAuth sessions ──
|
||||
@Get('mcp-tokens')
|
||||
listMcpTokens() { return { tokens: this.tokens.listAllMcpTokens() }; }
|
||||
async listMcpTokens() { return { tokens: await this.tokens.listAllMcpTokens() }; }
|
||||
|
||||
@Delete('mcp-tokens/:id')
|
||||
async deleteMcpToken(@CurrentUser() user: User, @Param('id') id: string, @Req() req: Request) {
|
||||
ok(this.tokens.adminDeleteMcpToken(id));
|
||||
ok(await this.tokens.adminDeleteMcpToken(id));
|
||||
await this.audit.writeAudit({ userId: user.id, action: 'admin.mcp_token_delete', resource: String(id), ip: getClientIp(req) });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -297,16 +297,16 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Get('mcp-tokens')
|
||||
listMcpTokens(@CurrentUser() user: User) {
|
||||
return { tokens: this.tokens.listMcpTokens(user.id) };
|
||||
async listMcpTokens(@CurrentUser() user: User) {
|
||||
return { tokens: await this.tokens.listMcpTokens(user.id) };
|
||||
}
|
||||
|
||||
@ManagedForbidden('a static token never expires and carries every scope; OAuth covers the same ground')
|
||||
@Post('mcp-tokens')
|
||||
@HttpCode(201)
|
||||
createMcpToken(@CurrentUser() user: User, @Body() body: McpTokenCreateDto, @Req() req: Request) {
|
||||
async createMcpToken(@CurrentUser() user: User, @Body() body: McpTokenCreateDto, @Req() req: Request) {
|
||||
this.limit('login', req, 5);
|
||||
const result = this.tokens.createMcpToken(user.id, body.name);
|
||||
const result = await this.tokens.createMcpToken(user.id, body.name);
|
||||
if (result.error) {
|
||||
throw new HttpException({ error: result.error }, result.status!);
|
||||
}
|
||||
@@ -314,8 +314,8 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Delete('mcp-tokens/:id')
|
||||
deleteMcpToken(@CurrentUser() user: User, @Param('id') id: string) {
|
||||
const result = this.tokens.deleteMcpToken(user.id, id);
|
||||
async deleteMcpToken(@CurrentUser() user: User, @Param('id') id: string) {
|
||||
const result = await this.tokens.deleteMcpToken(user.id, id);
|
||||
if (result.error) {
|
||||
throw new HttpException({ error: result.error }, result.status!);
|
||||
}
|
||||
@@ -335,18 +335,18 @@ export class AuthController {
|
||||
* whole reason it exists.
|
||||
*/
|
||||
@Get('api-tokens')
|
||||
listApiTokens(@CurrentUser() user: User) {
|
||||
return { tokens: this.tokens.listApiTokens(user.id) };
|
||||
async listApiTokens(@CurrentUser() user: User) {
|
||||
return { tokens: await this.tokens.listApiTokens(user.id) };
|
||||
}
|
||||
|
||||
@Post('api-tokens')
|
||||
@HttpCode(201)
|
||||
createApiToken(@CurrentUser() user: User, @Body() body: ApiTokenCreateDto, @Req() req: Request) {
|
||||
async createApiToken(@CurrentUser() user: User, @Body() body: ApiTokenCreateDto, @Req() req: Request) {
|
||||
this.limit('login', req, 5);
|
||||
// No `scopes` means the key reads everything, which is what every key minted
|
||||
// before this field existed does. Narrowing stays opt-in so the change
|
||||
// cannot break an integration that is already running.
|
||||
const result = this.tokens.createApiToken(user.id, body.name, body.scopes);
|
||||
const result = await this.tokens.createApiToken(user.id, body.name, body.scopes);
|
||||
if (result.error) {
|
||||
throw new HttpException({ error: result.error }, result.status!);
|
||||
}
|
||||
@@ -354,8 +354,8 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Delete('api-tokens/:id')
|
||||
deleteApiToken(@CurrentUser() user: User, @Param('id') id: string) {
|
||||
const result = this.tokens.deleteApiToken(user.id, id);
|
||||
async deleteApiToken(@CurrentUser() user: User, @Param('id') id: string) {
|
||||
const result = await this.tokens.deleteApiToken(user.id, id);
|
||||
if (result.error) {
|
||||
throw new HttpException({ error: result.error }, result.status!);
|
||||
}
|
||||
@@ -364,14 +364,14 @@ export class AuthController {
|
||||
|
||||
@Post('ws-token')
|
||||
@HttpCode(200)
|
||||
wsToken(@CurrentUser() user: User) {
|
||||
async wsToken(@CurrentUser() user: User) {
|
||||
// Own bucket, not 'login': a client that reconnects its socket in a loop
|
||||
// must not be able to lock itself out of signing in. The ceiling is far
|
||||
// above any real client, which mints one token per socket connect, but it
|
||||
// stops a single account from filling the process-wide ephemeral store and
|
||||
// 503-ing every other user's ws and download tokens.
|
||||
this.limitUser('ws_token', user.id, 120);
|
||||
const result = this.tokens.createWsToken(user.id);
|
||||
const result = await this.tokens.createWsToken(user.id);
|
||||
if (result.error) {
|
||||
throw new HttpException({ error: result.error }, result.status!);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export class McpTransportService {
|
||||
|
||||
// Long-lived static MCP token (trek_...) — full access + deprecation notice
|
||||
if (token.startsWith('trek_')) {
|
||||
const user = this.tokens.verifyMcpToken(token);
|
||||
const user = await this.tokens.verifyMcpToken(token);
|
||||
if (!user) return null;
|
||||
return { user, scopes: null, clientId: null, isStaticToken: true };
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { TokenService } from '../tokens/token.service';
|
||||
export class ApiTokenGuard implements CanActivate {
|
||||
constructor(private readonly tokens: TokenService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest<Request>();
|
||||
const token = extractApiToken(req);
|
||||
if (!token) {
|
||||
@@ -46,7 +46,7 @@ export class ApiTokenGuard implements CanActivate {
|
||||
401,
|
||||
);
|
||||
}
|
||||
const resolved = this.tokens.verifyApiTokenWithGrant(token);
|
||||
const resolved = await this.tokens.verifyApiTokenWithGrant(token);
|
||||
if (!resolved) {
|
||||
throw new HttpException(
|
||||
{ error: 'Invalid API token', code: 'API_TOKEN_INVALID' },
|
||||
|
||||
@@ -43,8 +43,8 @@ export class TokenService {
|
||||
private readonly ephemeral: EphemeralTokenService,
|
||||
) {}
|
||||
|
||||
listMcpTokens(userId: number) {
|
||||
return this.listTokens(userId, 'mcp');
|
||||
async listMcpTokens(userId: number) {
|
||||
return await this.listTokens(userId, 'mcp');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +57,11 @@ export class TokenService {
|
||||
* radius nobody asked for, so `kind` keeps the two apart and each surface
|
||||
* verifies the one it accepts.
|
||||
*/
|
||||
listApiTokens(userId: number) {
|
||||
return this.listTokens(userId, 'api');
|
||||
async listApiTokens(userId: number) {
|
||||
return await this.listTokens(userId, 'api');
|
||||
}
|
||||
|
||||
private listTokens(userId: number, kind: TokenKind) {
|
||||
private async listTokens(userId: number, kind: TokenKind) {
|
||||
const rows = this.db.all<TokenRow>(
|
||||
'SELECT id, name, token_prefix, created_at, last_used_at, scope_mode, api_scopes FROM mcp_tokens WHERE user_id = ? AND kind = ? ORDER BY created_at DESC',
|
||||
userId, kind
|
||||
@@ -86,8 +86,8 @@ export class TokenService {
|
||||
});
|
||||
}
|
||||
|
||||
createMcpToken(userId: number, rawName: unknown) {
|
||||
return this.createToken(userId, rawName, 'mcp');
|
||||
async createMcpToken(userId: number, rawName: unknown) {
|
||||
return await this.createToken(userId, rawName, 'mcp');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,11 +97,11 @@ export class TokenService {
|
||||
* before this argument existed. Narrowing is opt-in on purpose: shipping the
|
||||
* column must not change what an already-running integration can do.
|
||||
*/
|
||||
createApiToken(userId: number, rawName: unknown, scopes?: readonly string[]) {
|
||||
return this.createToken(userId, rawName, 'api', scopes);
|
||||
async createApiToken(userId: number, rawName: unknown, scopes?: readonly string[]) {
|
||||
return await this.createToken(userId, rawName, 'api', scopes);
|
||||
}
|
||||
|
||||
private createToken(userId: number, rawName: unknown, kind: TokenKind, scopes?: readonly string[]): { error?: string; status?: number; token?: Record<string, unknown> } {
|
||||
private async createToken(userId: number, rawName: unknown, kind: TokenKind, scopes?: readonly string[]): Promise<{ error?: string; status?: number; token?: Record<string, unknown> }> {
|
||||
const name = rawName as string | undefined;
|
||||
if (!name?.trim()) return { error: 'Token name is required', status: 400 };
|
||||
if (name.trim().length > 100) return { error: 'Token name must be 100 characters or less', status: 400 };
|
||||
@@ -135,12 +135,12 @@ export class TokenService {
|
||||
return { token: { ...(token as object), ...grant, raw_token: rawToken } };
|
||||
}
|
||||
|
||||
deleteMcpToken(userId: number, tokenId: string) {
|
||||
return this.deleteToken(userId, tokenId, 'mcp');
|
||||
async deleteMcpToken(userId: number, tokenId: string) {
|
||||
return await this.deleteToken(userId, tokenId, 'mcp');
|
||||
}
|
||||
|
||||
deleteApiToken(userId: number, tokenId: string) {
|
||||
return this.deleteToken(userId, tokenId, 'api');
|
||||
async deleteApiToken(userId: number, tokenId: string) {
|
||||
return await this.deleteToken(userId, tokenId, 'api');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +148,7 @@ export class TokenService {
|
||||
* happily delete a token the MCP panel manages, and the user would find a key
|
||||
* missing from a screen they never opened.
|
||||
*/
|
||||
private deleteToken(userId: number, tokenId: string, kind: TokenKind): { error?: string; status?: number; success?: boolean } {
|
||||
private async deleteToken(userId: number, tokenId: string, kind: TokenKind): Promise<{ error?: string; status?: number; success?: boolean }> {
|
||||
const token = this.db.get('SELECT id FROM mcp_tokens WHERE id = ? AND user_id = ? AND kind = ?', tokenId, userId, kind);
|
||||
if (!token) return { error: 'Token not found', status: 404 };
|
||||
this.db.run('DELETE FROM mcp_tokens WHERE id = ?', tokenId);
|
||||
@@ -162,7 +162,7 @@ export class TokenService {
|
||||
// Ephemeral tokens
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
createWsToken(userId: number): { error?: string; status?: number; token?: string } {
|
||||
async createWsToken(userId: number): Promise<{ error?: string; status?: number; token?: string }> {
|
||||
// Bind the ws-token to the user's current password_version so a token minted
|
||||
// before a password reset is rejected on connect (defence-in-depth session gate).
|
||||
const pv = this.db.get<{ password_version?: number }>('SELECT password_version FROM users WHERE id = ?', userId)?.password_version ?? 0;
|
||||
@@ -192,7 +192,7 @@ export class TokenService {
|
||||
// user-facing one treats that as best-effort.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
listAllMcpTokens() {
|
||||
async listAllMcpTokens() {
|
||||
return this.db.all(`
|
||||
SELECT t.id, t.name, t.token_prefix, t.created_at, t.last_used_at, t.user_id, u.username
|
||||
FROM mcp_tokens t
|
||||
@@ -201,7 +201,7 @@ export class TokenService {
|
||||
`);
|
||||
}
|
||||
|
||||
adminDeleteMcpToken(id: string) {
|
||||
async adminDeleteMcpToken(id: string) {
|
||||
const token = this.db.get<{ id: number; user_id: number }>('SELECT id, user_id FROM mcp_tokens WHERE id = ?', id);
|
||||
if (!token) return { error: 'Token not found', status: 404 };
|
||||
this.db.run('DELETE FROM mcp_tokens WHERE id = ?', id);
|
||||
@@ -213,13 +213,13 @@ export class TokenService {
|
||||
// Verification
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
verifyMcpToken(rawToken: string): User | null {
|
||||
return this.verifyToken(rawToken, 'mcp');
|
||||
async verifyMcpToken(rawToken: string): Promise<User | null> {
|
||||
return await this.verifyToken(rawToken, 'mcp');
|
||||
}
|
||||
|
||||
/** Verifies an integration key. An MCP token presented here does not resolve. */
|
||||
verifyApiToken(rawToken: string): User | null {
|
||||
return this.verifyToken(rawToken, 'api');
|
||||
async verifyApiToken(rawToken: string): Promise<User | null> {
|
||||
return await this.verifyToken(rawToken, 'api');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,7 +229,7 @@ export class TokenService {
|
||||
* both halves, nothing else needs either, and the existing signature is
|
||||
* pinned by tests that assert exactly a `User`.
|
||||
*/
|
||||
verifyApiTokenWithGrant(rawToken: string): { user: User; grant: PublicApiGrant } | null {
|
||||
async verifyApiTokenWithGrant(rawToken: string): Promise<{ user: User; grant: PublicApiGrant } | null> {
|
||||
const hash = createHash('sha256').update(rawToken).digest('hex');
|
||||
const row = this.db.get<User & { scope_mode: string | null; api_scopes: string | null }>(`
|
||||
SELECT u.id, u.username, u.email, u.role, mt.scope_mode, mt.api_scopes
|
||||
@@ -251,7 +251,7 @@ export class TokenService {
|
||||
* neither the caller nor a timing measurement learns that the string was a
|
||||
* real credential for somewhere else.
|
||||
*/
|
||||
private verifyToken(rawToken: string, kind: TokenKind): User | null {
|
||||
private async verifyToken(rawToken: string, kind: TokenKind): Promise<User | null> {
|
||||
const hash = createHash('sha256').update(rawToken).digest('hex');
|
||||
const row = this.db.get<User>(`
|
||||
SELECT u.id, u.username, u.email, u.role
|
||||
|
||||
Reference in New Issue
Block a user