auth.service.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. // apps/box-app-api/src/feature/auth/auth.service.ts
  2. import { Injectable, Logger } from '@nestjs/common';
  3. import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto';
  4. import { nowSecBigInt } from '@box/common/time/time.util';
  5. import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service';
  6. import { PrismaMongoStatsService } from '../../prisma/prisma-mongo-stats.service';
  7. import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
  8. import { AdService } from '../ads/ad.service';
  9. type LoginParams = {
  10. uid: string;
  11. ip: string;
  12. userAgent?: string;
  13. appVersion?: string;
  14. os?: string;
  15. channelId?: string;
  16. machine?: string;
  17. };
  18. type LoginResult = {
  19. uid: string;
  20. channelId: string;
  21. startupAds: any | null; // keep as any until you wire your Ad payload DTO
  22. };
  23. @Injectable()
  24. export class AuthService {
  25. private readonly logger = new Logger(AuthService.name);
  26. constructor(
  27. private readonly rabbitmqPublisher: RabbitmqPublisherService,
  28. private readonly prismaMongoStatsService: PrismaMongoStatsService,
  29. private readonly prismaMongoService: PrismaMongoService, // box-admin
  30. private readonly adService: AdService,
  31. ) {}
  32. async login(params: LoginParams): Promise<LoginResult> {
  33. const { uid, ip, userAgent, appVersion, os } = params;
  34. const channelIdInput = this.normalizeOptional(params.channelId);
  35. const machine = this.normalizeOptional(params.machine);
  36. const nowSec = nowSecBigInt(); // BigInt epoch seconds
  37. // 1) Find user (in box-admin)
  38. let user = await this.getUserByUid(uid);
  39. // 2) First login: auto-register
  40. if (!user) {
  41. const resolvedChannel =
  42. await this.resolveFirstLoginChannel(channelIdInput);
  43. user = await this.createUser({
  44. uid,
  45. ip,
  46. os: os ?? 'unknown',
  47. channelId: resolvedChannel.channelId,
  48. machine,
  49. nowSec,
  50. });
  51. } else {
  52. // 3) Subsequent login: keep stored user.channelId (ignore incoming channelId)
  53. // Optionally update machine if provided (safe)
  54. if (
  55. machine &&
  56. machine !== this.normalizeOptional((user as any).machine)
  57. ) {
  58. try {
  59. user = await this.prismaMongoService.user.update({
  60. where: { uid },
  61. data: {
  62. machine,
  63. updateAt: nowSec,
  64. lastLoginAt: nowSec,
  65. },
  66. });
  67. } catch (e) {
  68. // non-blocking
  69. this.logger.warn(
  70. `Failed to update machine for uid=${uid}: ${String(e)}`,
  71. );
  72. }
  73. }
  74. }
  75. const finalChannelId = String((user as any).channelId);
  76. // 4) Publish login history event (fire-and-forget)
  77. const loginEvent: UserLoginEventPayload = {
  78. uid,
  79. ip,
  80. userAgent,
  81. appVersion,
  82. os,
  83. channelId: finalChannelId,
  84. machine: machine ?? undefined,
  85. // IMPORTANT: use seconds (BigInt not always serializable across MQ)
  86. loginAt: Number(nowSec), // if consumer expects ms, change consumer; per your rule we use seconds
  87. };
  88. this.publishLoginEvent(loginEvent).catch(() => {
  89. // already logged inside
  90. });
  91. // 5) startupAds (placeholder: you’ll wire channel-specific ads later)
  92. // For now return null to keep behaviour deterministic.
  93. const startupAds = await this.adService.listAdsByType(1, 20);
  94. return {
  95. uid,
  96. channelId: finalChannelId,
  97. startupAds,
  98. };
  99. }
  100. private async publishLoginEvent(
  101. payload: UserLoginEventPayload,
  102. ): Promise<void> {
  103. try {
  104. this.logger.log(`Publishing user login event for uid=${payload.uid}`);
  105. await this.rabbitmqPublisher.publishUserLogin(payload);
  106. } catch (error) {
  107. const errorMessage =
  108. error instanceof Error ? error.message : String(error);
  109. const errorStack = error instanceof Error ? error.stack : undefined;
  110. this.logger.error(
  111. `Failed to publish user login event for uid=${payload.uid}: ${errorMessage}`,
  112. errorStack,
  113. );
  114. }
  115. }
  116. private normalizeOptional(v?: string): string | undefined {
  117. const s = (v ?? '').trim();
  118. return s.length > 0 ? s : undefined;
  119. }
  120. // ---------------------------
  121. // User (box-admin)
  122. // ---------------------------
  123. private async getUserByUid(uid: string): Promise<any | null> {
  124. return this.prismaMongoService.user.findUnique({
  125. where: { uid },
  126. });
  127. }
  128. private async createUser(input: {
  129. uid: string;
  130. ip: string;
  131. os: string;
  132. channelId: string;
  133. machine?: string;
  134. nowSec: bigint;
  135. }): Promise<any> {
  136. const { uid, channelId, machine, nowSec } = input;
  137. // Validate uniqueness explicitly (clear intention; Prisma will also enforce unique if set)
  138. const existed = await this.getUserByUid(uid);
  139. if (existed) return existed;
  140. return this.prismaMongoService.user.create({
  141. data: {
  142. uid,
  143. ip: input.ip,
  144. os: input.os,
  145. channelId,
  146. machine: machine ?? null,
  147. createAt: nowSec,
  148. updateAt: nowSec,
  149. lastLoginAt: nowSec,
  150. },
  151. });
  152. }
  153. // ---------------------------
  154. // Channel resolution (box-admin)
  155. // ---------------------------
  156. private async resolveFirstLoginChannel(
  157. channelIdInput?: string,
  158. ): Promise<any> {
  159. // 1) If client provided channelId, validate it exists
  160. if (channelIdInput) {
  161. const byId = await this.getChannelByChannelId(channelIdInput);
  162. if (byId) return byId;
  163. }
  164. // 2) Use default channel where isDefault=true
  165. const def = await this.getDefaultChannel();
  166. if (def) return def;
  167. // 3) Last resort: first created channel (so system can still run)
  168. const first = await this.getFirstChannel();
  169. if (first) return first;
  170. // If no channel exists at all, this is a deployment/config error
  171. throw new Error(
  172. 'No channel available (missing default channel and no channels exist)',
  173. );
  174. }
  175. private async getChannelByChannelId(channelId: string): Promise<any | null> {
  176. return this.prismaMongoService.channel.findUnique({
  177. where: { channelId },
  178. });
  179. }
  180. private async getDefaultChannel(): Promise<any | null> {
  181. return this.prismaMongoService.channel.findFirst({
  182. where: { isDefault: true },
  183. orderBy: { createAt: 'asc' },
  184. });
  185. }
  186. private async getFirstChannel(): Promise<any | null> {
  187. return this.prismaMongoService.channel.findFirst({
  188. orderBy: { createAt: 'asc' },
  189. });
  190. }
  191. }