| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- // 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<LoginResult> {
- 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<void> {
- 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<any | null> {
- return this.prismaMongoService.user.findUnique({
- where: { uid },
- });
- }
- private async createUser(input: {
- uid: string;
- ip: string;
- os: string;
- channelId: string;
- machine?: string;
- nowSec: bigint;
- }): Promise<any> {
- 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<any> {
- // 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<any | null> {
- return this.prismaMongoService.channel.findUnique({
- where: { channelId },
- });
- }
- private async getDefaultChannel(): Promise<any | null> {
- return this.prismaMongoService.channel.findFirst({
- where: { isDefault: true },
- orderBy: { createAt: 'asc' },
- });
- }
- private async getFirstChannel(): Promise<any | null> {
- return this.prismaMongoService.channel.findFirst({
- orderBy: { createAt: 'asc' },
- });
- }
- }
|