Files
registry/ts/services/token.service.ts
Juergen Kunz ab88ac896f feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality.
- Introduced TokensComponent for managing API tokens, including creation and revocation.
- Created LayoutComponent for consistent application layout with navigation and user information.
- Established main application structure in index.html and main.ts.
- Integrated Tailwind CSS for styling and responsive design.
- Configured TypeScript settings for strict type checking and module resolution.
2025-11-27 22:15:38 +00:00

210 lines
5.5 KiB
TypeScript

/**
* TokenService - API token management with secure hashing
*/
import * as plugins from '../plugins.ts';
import type { ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { ApiToken, User } from '../models/index.ts';
import { AuditService } from './audit.service.ts';
export interface ICreateTokenOptions {
userId: string;
name: string;
protocols: TRegistryProtocol[];
scopes: ITokenScope[];
expiresInDays?: number;
createdIp?: string;
}
export interface ITokenValidationResult {
valid: boolean;
token?: ApiToken;
user?: User;
errorCode?: string;
errorMessage?: string;
}
export class TokenService {
private auditService: AuditService;
constructor(auditService?: AuditService) {
this.auditService = auditService || new AuditService({ actorType: 'system' });
}
/**
* Generate a new API token
* Returns the raw token (only shown once) and the saved token record
*/
public async createToken(options: ICreateTokenOptions): Promise<{ rawToken: string; token: ApiToken }> {
// Generate secure random token: srg_{64 hex chars}
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const hexToken = Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
const rawToken = `srg_${hexToken}`;
// Hash the token for storage
const tokenHash = await this.hashToken(rawToken);
const tokenPrefix = rawToken.substring(0, 12); // "srg_" + first 8 hex chars
// Create token record
const token = new ApiToken();
token.id = await ApiToken.getNewId();
token.userId = options.userId;
token.name = options.name;
token.tokenHash = tokenHash;
token.tokenPrefix = tokenPrefix;
token.protocols = options.protocols;
token.scopes = options.scopes;
token.createdAt = new Date();
token.createdIp = options.createdIp;
token.usageCount = 0;
token.isRevoked = false;
if (options.expiresInDays) {
token.expiresAt = new Date(Date.now() + options.expiresInDays * 24 * 60 * 60 * 1000);
}
await token.save();
// Audit log
await this.auditService.logTokenCreated(token.id, token.name);
return { rawToken, token };
}
/**
* Validate a raw token and return the token record and user
*/
public async validateToken(rawToken: string, ip?: string): Promise<ITokenValidationResult> {
// Check token format
if (!rawToken || !rawToken.startsWith('srg_') || rawToken.length !== 68) {
return {
valid: false,
errorCode: 'INVALID_TOKEN_FORMAT',
errorMessage: 'Invalid token format',
};
}
// Hash and lookup
const tokenHash = await this.hashToken(rawToken);
const token = await ApiToken.findByHash(tokenHash);
if (!token) {
return {
valid: false,
errorCode: 'TOKEN_NOT_FOUND',
errorMessage: 'Token not found',
};
}
// Check validity
if (!token.isValid()) {
if (token.isRevoked) {
return {
valid: false,
errorCode: 'TOKEN_REVOKED',
errorMessage: 'Token has been revoked',
};
}
return {
valid: false,
errorCode: 'TOKEN_EXPIRED',
errorMessage: 'Token has expired',
};
}
// Get user
const user = await User.findById(token.userId);
if (!user) {
return {
valid: false,
errorCode: 'USER_NOT_FOUND',
errorMessage: 'Token owner not found',
};
}
if (!user.isActive) {
return {
valid: false,
errorCode: 'USER_INACTIVE',
errorMessage: 'Token owner account is inactive',
};
}
// Record usage
await token.recordUsage(ip);
return {
valid: true,
token,
user,
};
}
/**
* Get all tokens for a user (without sensitive data)
*/
public async getUserTokens(userId: string): Promise<ApiToken[]> {
return await ApiToken.getUserTokens(userId);
}
/**
* Revoke a token
*/
public async revokeToken(tokenId: string, reason?: string): Promise<boolean> {
const token = await ApiToken.getInstance({ id: tokenId });
if (!token) return false;
await token.revoke(reason);
await this.auditService.logTokenRevoked(token.id, token.name);
return true;
}
/**
* Revoke all tokens for a user
*/
public async revokeAllUserTokens(userId: string, reason?: string): Promise<number> {
const tokens = await ApiToken.getUserTokens(userId);
for (const token of tokens) {
await token.revoke(reason);
await this.auditService.logTokenRevoked(token.id, token.name);
}
return tokens.length;
}
/**
* Check if token has permission for a specific action
*/
public checkTokenPermission(
token: ApiToken,
protocol: TRegistryProtocol,
organizationId?: string,
repositoryId?: string,
action?: string
): boolean {
if (!token.hasProtocol(protocol)) return false;
return token.hasScope(protocol, organizationId, repositoryId, action);
}
/**
* Hash a token using SHA-256
*/
private async hashToken(rawToken: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(rawToken);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
/**
* Generate token prefix for display
*/
public static getTokenDisplay(tokenPrefix: string): string {
return `${tokenPrefix}...`;
}
}