import { IOAuthToken } from '../types/index.js'; import { BaseDao } from './base/BaseDao.js'; import { JsonFileBaseDao } from './base/JsonFileBaseDao.js'; /** * OAuth Token DAO interface with OAuth token-specific operations */ export interface OAuthTokenDao extends BaseDao { /** * Find token by access token */ findByAccessToken(accessToken: string): Promise; /** * Find token by refresh token */ findByRefreshToken(refreshToken: string): Promise; /** * Find tokens by client ID */ findByClientId(clientId: string): Promise; /** * Find tokens by username */ findByUsername(username: string): Promise; /** * Revoke token (delete by access token or refresh token) */ revokeToken(token: string): Promise; /** * Revoke all tokens for a user */ revokeUserTokens(username: string): Promise; /** * Revoke all tokens for a client */ revokeClientTokens(clientId: string): Promise; /** * Clean up expired tokens */ cleanupExpired(): Promise; /** * Check if access token is valid (exists and not expired) */ isAccessTokenValid(accessToken: string): Promise; /** * Check if refresh token is valid (exists and not expired) */ isRefreshTokenValid(refreshToken: string): Promise; } /** * JSON file-based OAuth Token DAO implementation */ export class OAuthTokenDaoImpl extends JsonFileBaseDao implements OAuthTokenDao { protected async getAll(): Promise { const settings = await this.loadSettings(); // Convert stored dates back to Date objects return (settings.oauthTokens || []).map((token) => ({ ...token, accessTokenExpiresAt: new Date(token.accessTokenExpiresAt), refreshTokenExpiresAt: token.refreshTokenExpiresAt ? new Date(token.refreshTokenExpiresAt) : undefined, })); } protected async saveAll(tokens: IOAuthToken[]): Promise { const settings = await this.loadSettings(); settings.oauthTokens = tokens; await this.saveSettings(settings); } protected getEntityId(token: IOAuthToken): string { return token.accessToken; } protected createEntity(_data: Omit): IOAuthToken { throw new Error('accessToken must be provided'); } protected updateEntity(existing: IOAuthToken, updates: Partial): IOAuthToken { return { ...existing, ...updates, accessToken: existing.accessToken, // accessToken should not be updated }; } async findAll(): Promise { return this.getAll(); } async findById(accessToken: string): Promise { return this.findByAccessToken(accessToken); } async findByAccessToken(accessToken: string): Promise { const tokens = await this.getAll(); return tokens.find((token) => token.accessToken === accessToken) || null; } async findByRefreshToken(refreshToken: string): Promise { const tokens = await this.getAll(); return tokens.find((token) => token.refreshToken === refreshToken) || null; } async findByClientId(clientId: string): Promise { const tokens = await this.getAll(); return tokens.filter((token) => token.clientId === clientId); } async findByUsername(username: string): Promise { const tokens = await this.getAll(); return tokens.filter((token) => token.username === username); } async create(data: IOAuthToken): Promise { const tokens = await this.getAll(); // Remove any existing tokens with the same access token or refresh token const filteredTokens = tokens.filter( (t) => t.accessToken !== data.accessToken && t.refreshToken !== data.refreshToken, ); const newToken: IOAuthToken = { ...data, }; filteredTokens.push(newToken); await this.saveAll(filteredTokens); return newToken; } async update(accessToken: string, updates: Partial): Promise { const tokens = await this.getAll(); const index = tokens.findIndex((token) => token.accessToken === accessToken); if (index === -1) { return null; } // Don't allow accessToken changes const { accessToken: _, ...allowedUpdates } = updates; const updatedToken = this.updateEntity(tokens[index], allowedUpdates); tokens[index] = updatedToken; await this.saveAll(tokens); return updatedToken; } async delete(accessToken: string): Promise { const tokens = await this.getAll(); const index = tokens.findIndex((token) => token.accessToken === accessToken); if (index === -1) { return false; } tokens.splice(index, 1); await this.saveAll(tokens); return true; } async exists(accessToken: string): Promise { const token = await this.findByAccessToken(accessToken); return token !== null; } async count(): Promise { const tokens = await this.getAll(); return tokens.length; } async revokeToken(token: string): Promise { const tokens = await this.getAll(); const tokenData = tokens.find((t) => t.accessToken === token || t.refreshToken === token); if (!tokenData) { return false; } const filteredTokens = tokens.filter( (t) => t.accessToken !== tokenData.accessToken && t.refreshToken !== tokenData.refreshToken, ); await this.saveAll(filteredTokens); return true; } async revokeUserTokens(username: string): Promise { const tokens = await this.getAll(); const userTokens = tokens.filter((token) => token.username === username); const remainingTokens = tokens.filter((token) => token.username !== username); await this.saveAll(remainingTokens); return userTokens.length; } async revokeClientTokens(clientId: string): Promise { const tokens = await this.getAll(); const clientTokens = tokens.filter((token) => token.clientId === clientId); const remainingTokens = tokens.filter((token) => token.clientId !== clientId); await this.saveAll(remainingTokens); return clientTokens.length; } async cleanupExpired(): Promise { const tokens = await this.getAll(); const now = new Date(); const validTokens = tokens.filter((token) => { // Keep if access token is still valid if (token.accessTokenExpiresAt > now) { return true; } // Or if refresh token exists and is still valid if (token.refreshToken && token.refreshTokenExpiresAt && token.refreshTokenExpiresAt > now) { return true; } return false; }); const expiredCount = tokens.length - validTokens.length; if (expiredCount > 0) { await this.saveAll(validTokens); } return expiredCount; } async isAccessTokenValid(accessToken: string): Promise { const token = await this.findByAccessToken(accessToken); if (!token) { return false; } return token.accessTokenExpiresAt > new Date(); } async isRefreshTokenValid(refreshToken: string): Promise { const token = await this.findByRefreshToken(refreshToken); if (!token) { return false; } if (!token.refreshTokenExpiresAt) { return true; // No expiration means always valid } return token.refreshTokenExpiresAt > new Date(); } }