import { prisma } from '../prisma'; import { SessionUser } from '../auth/session'; export interface UpsertUserParams { vkUserId: string; firstName?: string; lastName?: string; username?: string; avatarUrl?: string; encryptedAccessToken: string; encryptedRefreshToken?: string; expiresIn?: number; scope?: string; } export interface IUserRepository { upsertUserWithTokens(params: UpsertUserParams): Promise; getUserById(id: string): Promise; getUserByVkId(vkUserId: string): Promise; getUserCredentials(userId: string): Promise<{ encryptedAccessToken: string; encryptedRefreshToken?: string | null; expiresAt?: Date | null; scope?: string | null; updatedAt: Date; } | null>; updateCredentialConditionally( userId: string, update: { encryptedAccessToken: string; encryptedRefreshToken: string; expiresAt: Date | null; scope?: string | null; }, expectedUpdatedAt: Date ): Promise; } export class PrismaUserRepository implements IUserRepository { public async upsertUserWithTokens(params: UpsertUserParams): Promise { const expiresAt = params.expiresIn !== undefined ? new Date(Date.now() + params.expiresIn * 1000) : null; const user = await prisma.user.upsert({ where: { vkUserId: params.vkUserId }, update: { firstName: params.firstName, lastName: params.lastName, username: params.username, avatarUrl: params.avatarUrl, credentials: { upsert: { create: { encryptedAccessToken: params.encryptedAccessToken, encryptedRefreshToken: params.encryptedRefreshToken, expiresAt, scope: params.scope, }, update: { encryptedAccessToken: params.encryptedAccessToken, encryptedRefreshToken: params.encryptedRefreshToken, expiresAt, scope: params.scope, }, }, }, }, create: { vkUserId: params.vkUserId, firstName: params.firstName, lastName: params.lastName, username: params.username, avatarUrl: params.avatarUrl, credentials: { create: { encryptedAccessToken: params.encryptedAccessToken, encryptedRefreshToken: params.encryptedRefreshToken, expiresAt, scope: params.scope, }, }, }, }); return { id: user.id, vkUserId: user.vkUserId, firstName: user.firstName || undefined, lastName: user.lastName || undefined, username: user.username || undefined, avatarUrl: user.avatarUrl || undefined, }; } public async getUserById(id: string): Promise { const user = await prisma.user.findUnique({ where: { id } }); if (!user) return null; return { id: user.id, vkUserId: user.vkUserId, firstName: user.firstName || undefined, lastName: user.lastName || undefined, username: user.username || undefined, avatarUrl: user.avatarUrl || undefined, }; } public async getUserByVkId(vkUserId: string): Promise { const user = await prisma.user.findUnique({ where: { vkUserId } }); if (!user) return null; return { id: user.id, vkUserId: user.vkUserId, firstName: user.firstName || undefined, lastName: user.lastName || undefined, username: user.username || undefined, avatarUrl: user.avatarUrl || undefined, }; } public async getUserCredentials(userId: string) { const cred = await prisma.userCredential.findUnique({ where: { userId } }); if (!cred) return null; return { encryptedAccessToken: cred.encryptedAccessToken, encryptedRefreshToken: cred.encryptedRefreshToken, expiresAt: cred.expiresAt, scope: cred.scope, updatedAt: cred.updatedAt, }; } public async updateCredentialConditionally( userId: string, update: { encryptedAccessToken: string; encryptedRefreshToken: string; expiresAt: Date | null; scope?: string | null; }, expectedUpdatedAt: Date ): Promise { const result = await prisma.userCredential.updateMany({ where: { userId, updatedAt: expectedUpdatedAt }, data: { encryptedAccessToken: update.encryptedAccessToken, encryptedRefreshToken: update.encryptedRefreshToken, expiresAt: update.expiresAt, scope: update.scope ?? undefined, }, }); return result.count > 0; } } export class MemoryUserRepository implements IUserRepository { private users = new Map(); private credentials = new Map(); public async upsertUserWithTokens(params: UpsertUserParams): Promise { let existingUser: SessionUser | undefined; for (const u of this.users.values()) { if (u.vkUserId === params.vkUserId) { existingUser = u; break; } } const id = existingUser ? existingUser.id : `user_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; const user: SessionUser = { id, vkUserId: params.vkUserId, firstName: params.firstName, lastName: params.lastName, username: params.username, avatarUrl: params.avatarUrl, }; this.users.set(id, user); const expiresAt = params.expiresIn !== undefined ? new Date(Date.now() + params.expiresIn * 1000) : null; this.credentials.set(id, { encryptedAccessToken: params.encryptedAccessToken, encryptedRefreshToken: params.encryptedRefreshToken, expiresAt, scope: params.scope, updatedAt: new Date(), }); return user; } public async getUserById(id: string): Promise { return this.users.get(id) || null; } public async getUserByVkId(vkUserId: string): Promise { for (const u of this.users.values()) { if (u.vkUserId === vkUserId) return u; } return null; } public async getUserCredentials(userId: string) { const cred = this.credentials.get(userId); if (!cred) return null; return { encryptedAccessToken: cred.encryptedAccessToken, encryptedRefreshToken: cred.encryptedRefreshToken ?? null, expiresAt: cred.expiresAt ?? null, scope: cred.scope ?? null, updatedAt: cred.updatedAt ?? new Date(0), }; } public async updateCredentialConditionally( userId: string, update: { encryptedAccessToken: string; encryptedRefreshToken: string; expiresAt: Date | null; scope?: string | null; }, expectedUpdatedAt: Date ): Promise { const existing = this.credentials.get(userId); if (!existing) return false; const existingUpdatedAt: Date = existing.updatedAt ?? new Date(0); if (existingUpdatedAt.getTime() !== expectedUpdatedAt.getTime()) return false; this.credentials.set(userId, { ...existing, encryptedAccessToken: update.encryptedAccessToken, encryptedRefreshToken: update.encryptedRefreshToken, expiresAt: update.expiresAt, scope: update.scope ?? existing.scope, updatedAt: new Date(), }); return true; } public clear(): void { this.users.clear(); this.credentials.clear(); } } // Global user repository selector function createUserRepository(): IUserRepository { const driver = process.env.STORAGE_DRIVER || (process.env.NODE_ENV === 'test' ? 'memory' : 'prisma'); if (driver === 'memory') { return new MemoryUserRepository(); } return new PrismaUserRepository(); } export let defaultUserRepository: IUserRepository = createUserRepository(); export function setUserRepository(repo: IUserRepository): void { defaultUserRepository = repo; }