Re_Backend/src/services/apiToken.service.ts

120 lines
4.0 KiB
TypeScript

import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { ApiToken } from '../models/ApiToken';
import { User } from '../models/User';
import logger from '../utils/logger';
export class ApiTokenService {
private static readonly TOKEN_PREFIX = 're_';
private static readonly TOKEN_LENGTH = 32;
/**
* Generate a new API token for a user
*/
async createToken(userId: string, name: string, expiresInDays?: number): Promise<{ token: string; apiToken: ApiToken }> {
try {
// 1. Generate secure random token
const randomBytes = crypto.randomBytes(ApiTokenService.TOKEN_LENGTH);
const randomString = randomBytes.toString('hex');
const token = `${ApiTokenService.TOKEN_PREFIX}${randomString}`;
// 2. Hash the token
const tokenHash = await bcrypt.hash(token, 10);
// 3. Calculate expiry
let expiresAt: Date | undefined;
if (expiresInDays) {
expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
}
// 4. Save to DB
const apiToken = await ApiToken.create({
userId,
name,
prefix: token.substring(0, 8), // Store first 8 chars (prefix + 5 chars) for lookup
tokenHash,
expiresAt,
isActive: true,
});
logger.info(`API Token created for user ${userId}: ${name}`);
// Return raw token ONLY here
return { token, apiToken };
} catch (error) {
logger.error('Error creating API token:', error);
throw new Error('Failed to create API token');
}
}
/**
* List all active tokens for a user
*/
async listTokens(userId: string): Promise<ApiToken[]> {
return await ApiToken.findAll({
where: { userId, isActive: true },
order: [['createdAt', 'DESC']],
attributes: { exclude: ['tokenHash'] } // Never return hash
});
}
/**
* Revoke (delete) a token
*/
async revokeToken(userId: string, tokenId: string): Promise<boolean> {
const result = await ApiToken.destroy({
where: { id: tokenId, userId }
});
return result > 0;
}
/**
* Verify an API token and return the associated user
*/
async verifyToken(rawToken: string): Promise<User | null> {
try {
if (!rawToken.startsWith(ApiTokenService.TOKEN_PREFIX)) {
return null;
}
// 1. Find potential tokens by prefix (optimization)
const prefix = rawToken.substring(0, 8);
const tokens = await ApiToken.findAll({
where: {
prefix,
isActive: true
},
include: [{
model: User,
as: 'user',
where: { isActive: true } // Ensure user is also active
}]
});
// 2. check hash for each candidate
for (const tokenRecord of tokens) {
const isValid = await bcrypt.compare(rawToken, tokenRecord.tokenHash);
if (isValid) {
// 3. Check expiry
if (tokenRecord.expiresAt && new Date() > tokenRecord.expiresAt) {
logger.warn(`Expired API token used: ${tokenRecord.id}`);
return null;
}
// 4. Update last used
await tokenRecord.update({ lastUsedAt: new Date() });
// Return the user (casted as any because include types can be tricky)
return (tokenRecord as any).user;
}
}
return null;
} catch (error) {
logger.error('Error verifying API token:', error);
return null;
}
}
}