// apps/box-app-api/src/feature/auth/auth.service.ts import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto'; import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service'; import { AppJwtPayload } from './interfaces/app-jwt-payload'; import { PrismaMongoStatsService } from '../../prisma/prisma-mongo-stats.service'; import { PrismaMongoService } from '../../prisma/prisma-mongo.service'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( private readonly jwtService: JwtService, private readonly rabbitmqPublisher: RabbitmqPublisherService, private readonly prismaMongoStatsService: PrismaMongoStatsService, private readonly prismaMongoService: PrismaMongoService, ) {} async login(params: { uid: string; ip: string; userAgent?: string; appVersion?: string; os?: string; uChannelId?: string; machine?: string; // plus whatever you need like account, password, etc. }): Promise { const { uid, ip, userAgent, appVersion, os, uChannelId, machine } = params; // 1) Your existing auth logic (validate user, etc.) // const user = await this.validateUser(...); // 2) Generate tokenId (jti) and JWT const tokenId = crypto.randomUUID(); // Node 18+; or use uuid library const now = Date.now(); // number (ms since epoch) const user = await this.getUserByUid(uid); const firstChannel = await this.getChannelById(uChannelId || ''); // if user does not exist, uChannelId = firstChannel.channelId let finalUChannelId = uChannelId; if (!user && firstChannel) { finalUChannelId = firstChannel.channelId; } // if user exists, take firstChannel.channelId else if (user && firstChannel) { finalUChannelId = firstChannel.channelId; } // Build JWT payload with required and optional tracking fields. // Tracking fields (uChannelId, machine, ip, userAgent, appVersion, os) are optional // to preserve backward compatibility with older tokens and minimize JWT size for stability. const payload: AppJwtPayload = { sub: uid, uid, jti: tokenId, uChannelId: finalUChannelId, machine, ip, userAgent, appVersion, os, iat: Math.floor(now / 1000), // issued at (in seconds, per JWT spec) }; const accessToken = await this.jwtService.signAsync(payload); // 3) Build login event payload const loginEvent: UserLoginEventPayload = { uid, ip, userAgent, appVersion, os, uChannelId: finalUChannelId, machine, tokenId, loginAt: now, }; // 4) Fire-and-forget publish (but wait for broker confirm) try { this.logger.log(`Publishing user login event for uid=${uid}`); await this.rabbitmqPublisher.publishUserLogin(loginEvent); } catch (error) { // Decide your policy: // - Either just log and continue (login succeeds even if stats fail), // - Or throw to fail login if stats are critical. // For now, let's log and continue. // If you want stricter behaviour, re-throw. 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=${uid}: ${errorMessage}`, errorStack, ); } // let startupAd: AdPayload | null = null; // try { // startupAd = await this.adPoolService.getRandomAdByType(AdType.STARTUP); // } catch (err) { // this.logger.error('Failed to get startup ad for login', err); // } // let startupAdWithoutUrl: Omit | null = null; // if (startupAd) { // const { adsUrl, ...rest } = startupAd; // startupAdWithoutUrl = rest; // } return { accessToken }; } // add a function to retrieve user record from mongo by uid async getUserByUid(uid: string): Promise { // Implement your MongoDB query here to find the user by uid // For example, if you have a UserModel injected, you might do: // return this.userModel.findOne({ uid }).exec(); // Placeholder implementation: const user = await this.prismaMongoStatsService.user.findUnique({ where: { uid }, }); if (user) { return user; } return null; } // retrive the first channel from mongo collection, order by createAt ascending async getFirstChannel(): Promise { const channel = await this.prismaMongoService.channel.findFirst({ orderBy: { createAt: 'asc' }, }); if (channel) { return channel; } return null; } // find by channelId async getChannelById(channelId: string): Promise { const channel = await this.prismaMongoService.channel.findUnique({ where: { channelId }, }); if (channel) { return this.getFirstChannel(); } return null; } }