From 07e9475b701ab688324095d87fba43d2f20136d9 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 05:57:34 +0000 Subject: [PATCH] fix(tokens): align api token storage with cosmos partitioning --- .../src/modules/tokens/repository.test.ts | 24 ++++++++++++++----- .../src/modules/tokens/repository.ts | 20 +++++++++------- .../src/modules/tokens/routes.test.ts | 9 +++++-- .../src/modules/tokens/routes.ts | 4 ++-- services/platform-service/src/server.test.ts | 2 ++ 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/services/platform-service/src/modules/tokens/repository.test.ts b/services/platform-service/src/modules/tokens/repository.test.ts index e4e8b7bf..9b5c6b6e 100644 --- a/services/platform-service/src/modules/tokens/repository.test.ts +++ b/services/platform-service/src/modules/tokens/repository.test.ts @@ -54,13 +54,13 @@ describe('tokens repository', () => { describe('getById', () => { it('returns token when found', async () => { await create(baseToken); - const result = await getById('tok_1', 'user_1'); + const result = await getById('tok_1'); expect(result).not.toBeNull(); expect(result!.id).toBe('tok_1'); }); it('returns null when not found', async () => { - const result = await getById('tok_1', 'user_1'); + const result = await getById('tok_1'); expect(result).toBeNull(); }); }); @@ -76,14 +76,20 @@ describe('tokens repository', () => { describe('revoke', () => { it('revokes an existing token', async () => { await create(baseToken); - const result = await revoke('tok_1', 'user_1'); + const result = await revoke('tok_1', 'lysnrai'); expect(result).toBe(true); - const updated = await getById('tok_1', 'user_1'); + const updated = await getById('tok_1'); expect(updated!.status).toBe('revoked'); }); it('returns false when token not found', async () => { - const result = await revoke('tok_1', 'user_1'); + const result = await revoke('tok_1', 'lysnrai'); + expect(result).toBe(false); + }); + + it('returns false when token belongs to another product', async () => { + await create(baseToken); + const result = await revoke('tok_1', 'chronomind'); expect(result).toBe(false); }); }); @@ -91,9 +97,15 @@ describe('tokens repository', () => { describe('remove', () => { it('deletes and returns true', async () => { await create(baseToken); - const result = await remove('tok_1', 'user_1'); + const result = await remove('tok_1', 'lysnrai'); expect(result).toBe(true); }); + + it('returns false when token belongs to another product', async () => { + await create(baseToken); + const result = await remove('tok_1', 'chronomind'); + expect(result).toBe(false); + }); }); describe('countActive', () => { diff --git a/services/platform-service/src/modules/tokens/repository.ts b/services/platform-service/src/modules/tokens/repository.ts index ca23e36e..602fc42e 100644 --- a/services/platform-service/src/modules/tokens/repository.ts +++ b/services/platform-service/src/modules/tokens/repository.ts @@ -7,7 +7,7 @@ import { getCollection } from '../../lib/datastore.js'; import type { ApiTokenDoc, ApiTokenResponse } from './types.js'; function collection() { - return getCollection('api_tokens', '/userId'); + return getCollection('api_tokens', '/id'); } function stripHash(doc: ApiTokenDoc): ApiTokenResponse { @@ -33,9 +33,9 @@ export async function listByUser(userId: string, productId: string): Promise { +export async function getById(id: string): Promise { try { - return await collection().findById(id, userId); + return await collection().findById(id, id); } catch { return null; } @@ -46,17 +46,19 @@ export async function create(doc: ApiTokenDoc): Promise { return stripHash(created); } -export async function revoke(id: string, userId: string): Promise { - const existing = await getById(id, userId); - if (!existing) return false; +export async function revoke(id: string, productId?: string): Promise { + const existing = await getById(id); + if (!existing || (productId && existing.productId !== productId)) return false; - await collection().update(id, userId, { status: 'revoked' } as Partial); + await collection().update(id, id, { status: 'revoked' } as Partial); return true; } -export async function remove(id: string, userId: string): Promise { +export async function remove(id: string, productId?: string): Promise { try { - await collection().delete(id, userId); + const existing = await getById(id); + if (!existing || (productId && existing.productId !== productId)) return false; + await collection().delete(id, id); return true; } catch { return false; diff --git a/services/platform-service/src/modules/tokens/routes.test.ts b/services/platform-service/src/modules/tokens/routes.test.ts index b98c5fb5..665c62a7 100644 --- a/services/platform-service/src/modules/tokens/routes.test.ts +++ b/services/platform-service/src/modules/tokens/routes.test.ts @@ -31,7 +31,12 @@ const baseToken = { lastUsed: null, }; -async function buildApp(payload?: { sub: string; productId: string; role?: string; email?: string }) { +async function buildApp(payload?: { + sub: string; + productId: string; + role?: string; + email?: string; +}) { const { tokenRoutes } = await import('./routes.js'); const app = Fastify({ logger: false }); if (payload) { @@ -171,6 +176,6 @@ describe('tokenRoutes', () => { expect(res.statusCode).toBe(200); const data = JSON.parse(res.body); expect(data.success).toBe(true); - expect(repoMock.remove).toHaveBeenCalledWith('tok_1', 'root_1'); + expect(repoMock.remove).toHaveBeenCalledWith('tok_1'); }); }); diff --git a/services/platform-service/src/modules/tokens/routes.ts b/services/platform-service/src/modules/tokens/routes.ts index b4e42ec5..187a5b29 100644 --- a/services/platform-service/src/modules/tokens/routes.ts +++ b/services/platform-service/src/modules/tokens/routes.ts @@ -96,7 +96,7 @@ export async function tokenRoutes(app: FastifyInstance) { } if (parsed.data.action === 'revoke') { - const success = await repo.revoke(id, payload.sub); + const success = await repo.revoke(id, payload.productId); if (!success) throw new BadRequestError('Token not found'); return { success: true }; } @@ -111,7 +111,7 @@ export async function tokenRoutes(app: FastifyInstance) { throw new ForbiddenError('Super admin access required'); } const { id } = req.params as { id: string }; - const success = await repo.remove(id, payload.sub); + const success = await repo.remove(id); if (!success) throw new BadRequestError('Token not found'); return { success: true }; }); diff --git a/services/platform-service/src/server.test.ts b/services/platform-service/src/server.test.ts index c9c4e056..c3c04eed 100644 --- a/services/platform-service/src/server.test.ts +++ b/services/platform-service/src/server.test.ts @@ -14,6 +14,7 @@ const startTriggerEvaluationJobMock = vi.fn(); const appMock = { register: vi.fn(async () => undefined), + addHook: vi.fn(), }; vi.mock('@bytelyst/config', () => ({ @@ -82,6 +83,7 @@ describe('server bootstrap', () => { createServiceAppMock.mockResolvedValue(appMock); appMock.register.mockReset(); appMock.register.mockResolvedValue(undefined); + appMock.addHook.mockReset(); }); it('initializes secrets, app, routes, and starts service', async () => {