// apps/box-app-api/src/feature/auth/auth.service.ts import { Injectable, Logger } from '@nestjs/common'; import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto'; import { nowSecBigInt } from '@box/common/time/time.util'; import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service'; import { PrismaMongoStatsService } from '../../prisma/prisma-mongo-stats.service'; import { PrismaMongoService } from '../../prisma/prisma-mongo.service'; import { AdService } from '../ads/ad.service'; type LoginParams = { uid: string; ip: string; userAgent?: string; appVersion?: string; os?: string; channelId?: string; machine?: string; }; type LoginResult = { uid: string; channelId: string; startupAds: any | null; // keep as any until you wire your Ad payload DTO }; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( private readonly rabbitmqPublisher: RabbitmqPublisherService, private readonly prismaMongoStatsService: PrismaMongoStatsService, private readonly prismaMongoService: PrismaMongoService, // box-admin private readonly adService: AdService, ) {} async login(params: LoginParams): Promise { const { uid, ip, userAgent, appVersion, os } = params; const channelIdInput = this.normalizeOptional(params.channelId); const machine = this.normalizeOptional(params.machine); const nowSec = nowSecBigInt(); // BigInt epoch seconds // 1) Find user (in box-admin) let user = await this.getUserByUid(uid); // 2) First login: auto-register if (!user) { const resolvedChannel = await this.resolveFirstLoginChannel(channelIdInput); user = await this.createUser({ uid, ip, os: os ?? 'unknown', channelId: resolvedChannel.channelId, machine, nowSec, }); } else { // 3) Subsequent login: keep stored user.channelId (ignore incoming channelId) // Optionally update machine if provided (safe) if ( machine && machine !== this.normalizeOptional((user as any).machine) ) { try { user = await this.prismaMongoService.user.update({ where: { uid }, data: { machine, updateAt: nowSec, lastLoginAt: nowSec, }, }); } catch (e) { // non-blocking this.logger.warn( `Failed to update machine for uid=${uid}: ${String(e)}`, ); } } } const finalChannelId = String((user as any).channelId); // 4) Publish login history event (fire-and-forget) const loginEvent: UserLoginEventPayload = { uid, ip, userAgent, appVersion, os, channelId: finalChannelId, machine: machine ?? undefined, // IMPORTANT: use seconds (BigInt not always serializable across MQ) loginAt: Number(nowSec), // if consumer expects ms, change consumer; per your rule we use seconds }; this.publishLoginEvent(loginEvent).catch(() => { // already logged inside }); // 5) startupAds (placeholder: you’ll wire channel-specific ads later) // For now return null to keep behaviour deterministic. const startupAds = await this.adService.listAdsByType(1, 20); return { uid, channelId: finalChannelId, startupAds, }; } private async publishLoginEvent( payload: UserLoginEventPayload, ): Promise { try { this.logger.log(`Publishing user login event for uid=${payload.uid}`); await this.rabbitmqPublisher.publishUserLogin(payload); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; this.logger.error( `Failed to publish user login event for uid=${payload.uid}: ${errorMessage}`, errorStack, ); } } private normalizeOptional(v?: string): string | undefined { const s = (v ?? '').trim(); return s.length > 0 ? s : undefined; } // --------------------------- // User (box-admin) // --------------------------- private async getUserByUid(uid: string): Promise { return this.prismaMongoService.user.findUnique({ where: { uid }, }); } private async createUser(input: { uid: string; ip: string; os: string; channelId: string; machine?: string; nowSec: bigint; }): Promise { const { uid, channelId, machine, nowSec } = input; // Validate uniqueness explicitly (clear intention; Prisma will also enforce unique if set) const existed = await this.getUserByUid(uid); if (existed) return existed; return this.prismaMongoService.user.create({ data: { uid, ip: input.ip, os: input.os, channelId, machine: machine ?? null, createAt: nowSec, updateAt: nowSec, lastLoginAt: nowSec, }, }); } // --------------------------- // Channel resolution (box-admin) // --------------------------- private async resolveFirstLoginChannel( channelIdInput?: string, ): Promise { // 1) If client provided channelId, validate it exists if (channelIdInput) { const byId = await this.getChannelByChannelId(channelIdInput); if (byId) return byId; } // 2) Use default channel where isDefault=true const def = await this.getDefaultChannel(); if (def) return def; // 3) Last resort: first created channel (so system can still run) const first = await this.getFirstChannel(); if (first) return first; // If no channel exists at all, this is a deployment/config error throw new Error( 'No channel available (missing default channel and no channels exist)', ); } private async getChannelByChannelId(channelId: string): Promise { return this.prismaMongoService.channel.findUnique({ where: { channelId }, }); } private async getDefaultChannel(): Promise { return this.prismaMongoService.channel.findFirst({ where: { isDefault: true }, orderBy: { createAt: 'asc' }, }); } private async getFirstChannel(): Promise { return this.prismaMongoService.channel.findFirst({ orderBy: { createAt: 'asc' }, }); } }