feat(tokens): classify machine credentials
This commit is contained in:
parent
3398d1400f
commit
507f0fdd1f
@ -14,6 +14,8 @@ const baseToken: ApiTokenDoc = {
|
||||
userId: 'user_1',
|
||||
userName: 'Test User',
|
||||
name: 'Test Token',
|
||||
tokenType: 'user_api',
|
||||
environment: 'prod',
|
||||
tokenHash: 'hash_abc',
|
||||
prefix: 'lysnr_',
|
||||
status: 'active',
|
||||
|
||||
@ -23,6 +23,8 @@ const baseToken = {
|
||||
userId: 'admin_1',
|
||||
userName: 'admin@example.com',
|
||||
name: 'ci-token',
|
||||
tokenType: 'service_api',
|
||||
environment: 'prod',
|
||||
prefix: 'wai_12345678',
|
||||
status: 'active',
|
||||
scopes: ['read'],
|
||||
@ -117,6 +119,12 @@ describe('tokenRoutes', () => {
|
||||
const data = JSON.parse(res.body);
|
||||
expect(data).toHaveProperty('rawToken');
|
||||
expect(data.rawToken.startsWith('wai_')).toBe(true);
|
||||
expect(repoMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tokenType: 'user_api',
|
||||
environment: 'prod',
|
||||
})
|
||||
);
|
||||
expect(repoMock.hashToken).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ export async function tokenRoutes(app: FastifyInstance) {
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const { name, scopes, expiresInDays } = parsed.data;
|
||||
const { name, tokenType, environment, scopes, expiresInDays } = parsed.data;
|
||||
|
||||
const rawToken = `wai_${crypto.randomBytes(32).toString('hex')}`;
|
||||
const prefix = rawToken.slice(0, 12);
|
||||
@ -64,6 +64,8 @@ export async function tokenRoutes(app: FastifyInstance) {
|
||||
userId: payload.sub,
|
||||
userName: payload.email ?? '',
|
||||
name,
|
||||
tokenType,
|
||||
environment,
|
||||
prefix,
|
||||
tokenHash,
|
||||
status: 'active',
|
||||
|
||||
@ -12,6 +12,8 @@ describe('CreateTokenSchema', () => {
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe('CI Pipeline');
|
||||
expect(result.data.tokenType).toBe('user_api');
|
||||
expect(result.data.environment).toBe('prod');
|
||||
expect(result.data.scopes).toEqual(['read']);
|
||||
expect(result.data.expiresInDays).toBe(90);
|
||||
}
|
||||
@ -20,11 +22,15 @@ describe('CreateTokenSchema', () => {
|
||||
it('accepts full input', () => {
|
||||
const result = CreateTokenSchema.safeParse({
|
||||
name: 'Deploy Key',
|
||||
tokenType: 'service_api',
|
||||
environment: 'staging',
|
||||
scopes: ['read', 'write', 'admin'],
|
||||
expiresInDays: 30,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.tokenType).toBe('service_api');
|
||||
expect(result.data.environment).toBe('staging');
|
||||
expect(result.data.scopes).toEqual(['read', 'write', 'admin']);
|
||||
expect(result.data.expiresInDays).toBe(30);
|
||||
}
|
||||
@ -102,6 +108,8 @@ describe('ApiTokenDoc type shape', () => {
|
||||
userId: 'user_1',
|
||||
userName: 'admin@example.com',
|
||||
name: 'CI Token',
|
||||
tokenType: 'service_api',
|
||||
environment: 'prod',
|
||||
prefix: 'wai_abc12345',
|
||||
tokenHash: '$2a$10$hashedvalue',
|
||||
status: 'active',
|
||||
@ -113,6 +121,8 @@ describe('ApiTokenDoc type shape', () => {
|
||||
};
|
||||
|
||||
expect(doc.status).toBe('active');
|
||||
expect(doc.tokenType).toBe('service_api');
|
||||
expect(doc.environment).toBe('prod');
|
||||
expect(doc.scopes).toContain('read');
|
||||
expect(doc.lastUsed).toBeNull();
|
||||
});
|
||||
@ -126,6 +136,8 @@ describe('ApiTokenDoc type shape', () => {
|
||||
userId: 'u1',
|
||||
userName: 'test',
|
||||
name: 'test',
|
||||
tokenType: 'user_api',
|
||||
environment: 'prod',
|
||||
prefix: 'wai_',
|
||||
tokenHash: 'hash',
|
||||
status,
|
||||
@ -147,6 +159,8 @@ describe('ApiTokenResponse type shape', () => {
|
||||
userId: 'u1',
|
||||
userName: 'test@test.com',
|
||||
name: 'My Token',
|
||||
tokenType: 'product_api',
|
||||
environment: 'staging',
|
||||
prefix: 'wai_abc',
|
||||
tokenHash: 'secret_hash',
|
||||
status: 'active',
|
||||
@ -157,11 +171,14 @@ describe('ApiTokenResponse type shape', () => {
|
||||
};
|
||||
|
||||
// Simulate what stripHash does
|
||||
const { tokenHash: _hash, ...response } = doc;
|
||||
const { tokenHash: _tokenHash, ...response } = doc;
|
||||
void _tokenHash;
|
||||
const apiResponse: ApiTokenResponse = response;
|
||||
|
||||
expect(apiResponse.id).toBe('tok_1');
|
||||
expect(apiResponse.name).toBe('My Token');
|
||||
expect(apiResponse.tokenType).toBe('product_api');
|
||||
expect(apiResponse.environment).toBe('staging');
|
||||
expect('tokenHash' in apiResponse).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,6 +10,8 @@ export interface ApiTokenDoc {
|
||||
userId: string;
|
||||
userName: string;
|
||||
name: string;
|
||||
tokenType: 'user_api' | 'product_api' | 'service_api';
|
||||
environment: 'dev' | 'staging' | 'prod';
|
||||
prefix: string;
|
||||
tokenHash: string;
|
||||
status: 'active' | 'revoked' | 'expired';
|
||||
@ -24,6 +26,8 @@ export type ApiTokenResponse = Omit<ApiTokenDoc, 'tokenHash'>;
|
||||
|
||||
export const CreateTokenSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
tokenType: z.enum(['user_api', 'product_api', 'service_api']).default('user_api'),
|
||||
environment: z.enum(['dev', 'staging', 'prod']).default('prod'),
|
||||
scopes: z.array(z.string()).default(['read']),
|
||||
expiresInDays: z.number().int().min(1).max(365).default(90),
|
||||
});
|
||||
|
||||
@ -101,7 +101,7 @@ describe('server bootstrap', () => {
|
||||
expect(appMock.register).toHaveBeenCalled();
|
||||
expect(appMock.register.mock.calls.length).toBeGreaterThan(15);
|
||||
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4003, host: '0.0.0.0' });
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it('registers optional jwt parsing with the shared fastify-core helper', async () => {
|
||||
await import('./server.js');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user