auth.service.ts 6.5 KB

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