fix(tokens): align api token storage with cosmos partitioning
This commit is contained in:
parent
a76b932502
commit
07e9475b70
@ -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', () => {
|
||||
|
||||
@ -7,7 +7,7 @@ import { getCollection } from '../../lib/datastore.js';
|
||||
import type { ApiTokenDoc, ApiTokenResponse } from './types.js';
|
||||
|
||||
function collection() {
|
||||
return getCollection<ApiTokenDoc>('api_tokens', '/userId');
|
||||
return getCollection<ApiTokenDoc>('api_tokens', '/id');
|
||||
}
|
||||
|
||||
function stripHash(doc: ApiTokenDoc): ApiTokenResponse {
|
||||
@ -33,9 +33,9 @@ export async function listByUser(userId: string, productId: string): Promise<Api
|
||||
return results.map(stripHash);
|
||||
}
|
||||
|
||||
export async function getById(id: string, userId: string): Promise<ApiTokenDoc | null> {
|
||||
export async function getById(id: string): Promise<ApiTokenDoc | null> {
|
||||
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<ApiTokenResponse> {
|
||||
return stripHash(created);
|
||||
}
|
||||
|
||||
export async function revoke(id: string, userId: string): Promise<boolean> {
|
||||
const existing = await getById(id, userId);
|
||||
if (!existing) return false;
|
||||
export async function revoke(id: string, productId?: string): Promise<boolean> {
|
||||
const existing = await getById(id);
|
||||
if (!existing || (productId && existing.productId !== productId)) return false;
|
||||
|
||||
await collection().update(id, userId, { status: 'revoked' } as Partial<ApiTokenDoc>);
|
||||
await collection().update(id, id, { status: 'revoked' } as Partial<ApiTokenDoc>);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function remove(id: string, userId: string): Promise<boolean> {
|
||||
export async function remove(id: string, productId?: string): Promise<boolean> {
|
||||
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;
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
});
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user