@greatapps/common 1.1.771 → 1.1.773

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,693 +1,707 @@
1
- import { cookies } from "next/headers";
2
-
3
- import { api, type RequestConfig } from "../../../infra/api/client";
4
- import { ApiError } from "../../../infra/api/types";
5
- import { whitelabelService } from "../../whitelabel/services/whitelabel.service";
6
- import { buildWlOverrideFromJwt } from "../utils/build-wl-override";
7
- import {
8
- ClientInfo,
9
- ForgotPasswordRequest,
10
- ForgotPasswordResponse,
11
- GeoLocation,
12
- LoginApiResponse,
13
- LoginRequest,
14
- LoginResponse,
15
- LogoutApiResponse,
16
- LogoutRequest,
17
- LogoutResponse,
18
- RegisterApiRequest,
19
- RegisterApiResponse,
20
- RegisterRequest,
21
- RegisterResponse,
22
- ResendVerificationRequest,
23
- ResendVerificationResponse,
24
- ResetPasswordRequest,
25
- ResetPasswordResponse,
26
- SearchPlansApiResponse,
27
- SessionKeepRequest,
28
- SessionKeepResponse,
29
- SocialGoogleApiResponse,
30
- SocialGoogleResponse,
31
- SocialOnboardingApiResponse,
32
- SocialOnboardingRequest,
33
- SocialOnboardingResponse,
34
- IsolatedLoginResponse,
35
- TwoFactorApiResponse,
36
- TwoFactorRequest,
37
- TwoFactorResponse,
38
- UnlinkGoogleApiResponse,
39
- UnlinkGoogleResponse,
40
- VerifyEmailRequest,
41
- VerifyEmailResponse,
42
- } from "../schema";
43
- import { findWhitelabel } from "../../../server";
44
-
45
- const AUTH_COOKIE_NAME = "greatapps";
46
- const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
47
-
48
- const COOKIE_OPTIONS = {
49
- httpOnly: true,
50
- secure: process.env.NODE_ENV === "production",
51
- sameSite: "lax" as const,
52
- path: "/",
53
- };
54
-
55
- class AuthService {
56
- async login(
57
- credentials: LoginRequest,
58
- clientInfo: ClientInfo,
59
- ): Promise<LoginResponse>;
60
- async login(
61
- credentials: LoginRequest,
62
- clientInfo: ClientInfo,
63
- options: { setCookie: false },
64
- ): Promise<IsolatedLoginResponse>;
65
- async login(
66
- credentials: LoginRequest,
67
- clientInfo: ClientInfo,
68
- options?: { setCookie?: boolean },
69
- ): Promise<LoginResponse | IsolatedLoginResponse>;
70
- async login(
71
- credentials: LoginRequest,
72
- clientInfo: ClientInfo,
73
- options?: { setCookie?: boolean },
74
- ): Promise<LoginResponse | IsolatedLoginResponse> {
75
- let requestConfig: RequestConfig | undefined;
76
- if (credentials.id_wl) {
77
- const wlToken = await whitelabelService.getTokenByWhitelabelId(
78
- credentials.id_wl,
79
- );
80
- requestConfig = { whiteLabelId: credentials.id_wl, authToken: wlToken };
81
- }
82
-
83
- const response = await api.apps.post<LoginApiResponse>(
84
- "/auth/login",
85
- {
86
- email: credentials.email,
87
- password: credentials.password,
88
- source: credentials?.source,
89
- location: clientInfo.location,
90
- ip: clientInfo.ip,
91
- timezone: clientInfo.timezone,
92
- agent: clientInfo.agent,
93
- },
94
- requestConfig,
95
- );
96
-
97
- if (response.status === 0) {
98
- throw new ApiError(
99
- response.message || "E-mail ou senha incorretos",
100
- response.code || "LOGIN_FAILED",
101
- 401,
102
- );
103
- }
104
-
105
- if (!response.cookie) {
106
- throw new ApiError(
107
- "Resposta de autenticação inválida",
108
- "INVALID_RESPONSE",
109
- 500,
110
- );
111
- }
112
-
113
- if (response.two_factor_required) {
114
- return { result: "two_factor_required", cookie: response.cookie, twoFactorMode: response.two_factor_required };
115
- }
116
-
117
- // Modo isolado: caller só quer o token, sem afetar a sessão do navegador
118
- // nem pagar pelas chamadas de user/account (ex.: fluxo de SSO).
119
- if (options?.setCookie === false) {
120
- return { result: "success", accessToken: response.cookie };
121
- }
122
-
123
- await this.setAuthCookie(response.cookie);
124
-
125
- const [{ userService }, { accountService }] = await Promise.all([
126
- import("../../users/services/user.service"),
127
- import("../../accounts/services/account.service"),
128
- ]);
129
- const [user, account] = await Promise.all([
130
- userService.findById(
131
- requestConfig ? { authToken: requestConfig.authToken } : undefined,
132
- ),
133
- accountService.findCurrentAccount(requestConfig),
134
- ]);
135
-
136
- return {
137
- result: "success",
138
- user,
139
- account,
140
- accessToken: response.cookie,
141
- refreshToken: "",
142
- expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
143
- };
144
- }
145
-
146
- async verifyTwoFactor(
147
- cookie: string,
148
- code: string,
149
- clientInfo: ClientInfo,
150
- options?: { window?: number; setCookie?: boolean },
151
- ): Promise<TwoFactorResponse> {
152
- const payload: TwoFactorRequest = {
153
- location: clientInfo.location,
154
- ip: clientInfo.ip,
155
- timezone: clientInfo.timezone,
156
- agent: clientInfo.agent,
157
- cookie,
158
- code,
159
- };
160
-
161
- const endpoint = options?.window !== undefined ? `/auth/code?window=${options.window}` : '/auth/code';
162
- const response = await api.apps.post<TwoFactorApiResponse>(
163
- endpoint,
164
- payload,
165
- );
166
-
167
- if (response.status === 0) {
168
- throw new ApiError("Código 2FA inválido", "TWO_FACTOR_FAILED", 401);
169
- }
170
-
171
- if (!response?.data?.cookie) {
172
- throw new ApiError(
173
- "Resposta de autenticação inválida após 2FA",
174
- "INVALID_RESPONSE",
175
- 500,
176
- );
177
- }
178
-
179
- // Modo isolado: devolve o token sem gravar o cookie — o caller redireciona
180
- // pra outro lugar (ex.: SSO callback) e não quer mexer na sessão atual.
181
- if (options?.setCookie === false) {
182
- return { status: 1, accessToken: response.data.cookie };
183
- }
184
-
185
- await this.setAuthCookie(response.data.cookie);
186
-
187
- return { status: 1 };
188
- }
189
-
190
- async socialLoginGoogle(
191
- code: string,
192
- clientInfo: ClientInfo,
193
- ): Promise<SocialGoogleResponse> {
194
- const response = await api.apps.post<SocialGoogleApiResponse>(
195
- "/auth/social/google",
196
- {
197
- code,
198
- location: clientInfo.location,
199
- ip: clientInfo.ip,
200
- timezone: clientInfo.timezone,
201
- agent: clientInfo.agent,
202
- verification: true,
203
- },
204
- );
205
-
206
- if (response.status === 0) {
207
- if (response.code === "link_requires_password") {
208
- return {
209
- result: "link_requires_password",
210
- message:
211
- response.message ||
212
- "Uma conta com esse e-mail já existe. Faça login com e-mail e senha para vincular sua conta Google.",
213
- };
214
- }
215
- throw new ApiError(
216
- response.message || "Falha no login com Google",
217
- response.code || "GOOGLE_LOGIN_FAILED",
218
- 401,
219
- );
220
- }
221
-
222
- if (response.needs_onboarding) {
223
- if (!response.onboarding_token || !response.partial?.email) {
224
- throw new ApiError(
225
- "Resposta de onboarding inválida",
226
- "INVALID_RESPONSE",
227
- 500,
228
- );
229
- }
230
- return {
231
- result: "needs_onboarding",
232
- onboardingToken: response.onboarding_token,
233
- partial: response.partial,
234
- };
235
- }
236
-
237
- if (!response.cookie) {
238
- throw new ApiError(
239
- "Resposta de autenticação inválida",
240
- "INVALID_RESPONSE",
241
- 500,
242
- );
243
- }
244
-
245
- if (response.two_factor_required) {
246
- return {
247
- result: "two_factor_required",
248
- cookie: response.cookie,
249
- twoFactorMode: response.two_factor_required,
250
- };
251
- }
252
-
253
- await this.setAuthCookie(response.cookie);
254
-
255
- const [{ userService }, { accountService }] = await Promise.all([
256
- import("../../users/services/user.service"),
257
- import("../../accounts/services/account.service"),
258
- ]);
259
- const [user, account] = await Promise.all([
260
- userService.findById(),
261
- accountService.findCurrentAccount(),
262
- ]);
263
-
264
- return {
265
- result: "success",
266
- user,
267
- account,
268
- accessToken: response.cookie,
269
- expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
270
- };
271
- }
272
-
273
- async completeGoogleOnboarding(
274
- data: SocialOnboardingRequest,
275
- clientInfo: ClientInfo,
276
- ): Promise<SocialOnboardingResponse> {
277
- const today = new Date();
278
- const trialEndDate = new Date(today);
279
- const trialDays = Math.min(Math.max(data.trialDays ?? 7, 1), 90);
280
- trialEndDate.setDate(today.getDate() + trialDays);
281
-
282
- const idPlan = await this.resolvePlanId(data.idPlan);
283
-
284
- const payload = {
285
- onboarding_token: data.onboardingToken,
286
- name: data.user.name,
287
- bussiness_type: data.businessType ?? 0,
288
- language: "pt-br",
289
- timezone: "America/Sao_Paulo",
290
- currency: "BRL",
291
- location: clientInfo.location,
292
- ip: clientInfo.ip,
293
- agent: clientInfo.agent,
294
- id_affiliate: data.affiliateId || 0,
295
- origin: data.origin,
296
- user: {
297
- id_api: "",
298
- name: data.user.name || "",
299
- last_name: data.user.last_name || "",
300
- rg: data.user.rg || "",
301
- cpf: data.user.cpf || "",
302
- gender: data.user.gender ?? 1,
303
- ddi: data.user.ddi || "55",
304
- phone: data.user.phone || "",
305
- profile: "owner",
306
- language: "pt-br",
307
- },
308
- subscription:
309
- idPlan === -1
310
- ? {
311
- type: "free",
312
- id_coupon: data.couponId || 0,
313
- id_plan: 5,
314
- id_product: 1,
315
- }
316
- : {
317
- type: "trial",
318
- id_coupon: data.couponId || 0,
319
- id_plan: idPlan,
320
- date_due: trialEndDate.toISOString().split("T")[0],
321
- trial_days: trialDays,
322
- id_product: 1,
323
- },
324
- };
325
-
326
- const response = await api.apps.post<SocialOnboardingApiResponse>(
327
- "/auth/social/onboarding",
328
- payload,
329
- );
330
-
331
- if (response.status === 0) {
332
- throw new ApiError(
333
- response.message || "Erro ao finalizar cadastro com Google",
334
- "ONBOARDING_FAILED",
335
- 400,
336
- );
337
- }
338
-
339
- if (!response.data?.length || !response.cookie) {
340
- throw new ApiError(
341
- "Resposta de cadastro inválida",
342
- "INVALID_RESPONSE",
343
- 500,
344
- );
345
- }
346
-
347
- await this.setAuthCookie(response.cookie);
348
-
349
- const [{ userService }, { accountService }] = await Promise.all([
350
- import("../../users/services/user.service"),
351
- import("../../accounts/services/account.service"),
352
- ]);
353
- const [user, account] = await Promise.all([
354
- userService.findById(),
355
- accountService.findCurrentAccount(),
356
- ]);
357
-
358
- return {
359
- user,
360
- account,
361
- accessToken: response.cookie,
362
- expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
363
- };
364
- }
365
-
366
- async unlinkGoogle(): Promise<UnlinkGoogleResponse> {
367
- const { id_account, id_user } = await import(
368
- "../utils/get-user-context"
369
- ).then((m) => m.getUserContext());
370
-
371
- const response = await api.apps.delete<UnlinkGoogleApiResponse>(
372
- `/accounts/${id_account}/users/${id_user}/social/google`,
373
- );
374
-
375
- if (response.status === 0) {
376
- throw new ApiError(
377
- response.message || "Erro ao desvincular conta Google",
378
- "UNLINK_FAILED",
379
- 400,
380
- );
381
- }
382
-
383
- return {
384
- success: true,
385
- message: response.message,
386
- };
387
- }
388
-
389
- async register(
390
- data: RegisterRequest,
391
- clientInfo: ClientInfo,
392
- ): Promise<RegisterResponse> {
393
- const today = new Date();
394
- const trialEndDate = new Date(today);
395
- const trialDays = Math.min(Math.max(data.trialDays ?? 7, 1), 90);
396
- trialEndDate.setDate(today.getDate() + trialDays);
397
-
398
- const idPlan = await this.resolvePlanId(data.idPlan);
399
-
400
- const payload: RegisterApiRequest = {
401
- name: data.accountName,
402
- bussiness_type: data.businessType ?? 0,
403
- language: "pt-br",
404
- timezone: "America/Sao_Paulo",
405
- currency: "BRL",
406
- location: clientInfo.location,
407
- ip: clientInfo.ip,
408
- agent: clientInfo.agent,
409
- id_affiliate: data.affiliateId || 0,
410
- origin: data.origin,
411
- user: {
412
- id_api: "",
413
- name: data.name,
414
- last_name: data.lastName || "",
415
- rg: data.rg || "",
416
- cpf: data.cpf || "",
417
- gender: data.gender,
418
- email: data.email,
419
- phone: data.phone,
420
- password: data.password,
421
- profile: "owner",
422
- language: "pt-br",
423
- },
424
- subscription:
425
- idPlan === -1
426
- ? {
427
- type: "free",
428
- id_coupon: data.couponId || 0,
429
- id_plan: 5, // Plano Free (ID fixo)
430
- id_product: 1,
431
- }
432
- : {
433
- type: "trial",
434
- id_coupon: data.couponId || 0,
435
- id_plan: idPlan,
436
- // Fluxo não-Stripe lê `date_due`; fluxo Stripe lê `trial_days`.
437
- // Mandamos os dois pra cobrir ambos os caminhos da API.
438
- date_due: trialEndDate.toISOString().split("T")[0],
439
- trial_days: trialDays,
440
- id_product: 1,
441
- },
442
- };
443
-
444
- const response = await api.apps.post<RegisterApiResponse>(
445
- "/accounts",
446
- payload,
447
- );
448
-
449
- if (response.status === 0) {
450
- throw new ApiError(
451
- response.message || "Erro ao criar conta",
452
- "REGISTER_FAILED",
453
- 400,
454
- );
455
- }
456
-
457
- if (!response.data || response.data.length === 0) {
458
- throw new ApiError(
459
- "Resposta de registro inválida",
460
- "INVALID_RESPONSE",
461
- 500,
462
- );
463
- }
464
-
465
- if (!response.cookie) {
466
- throw new ApiError(
467
- "Resposta de autenticação inválida",
468
- "INVALID_RESPONSE",
469
- 500,
470
- );
471
- }
472
-
473
- const accountData = response.data[0];
474
-
475
- await this.setAuthCookie(response.cookie);
476
-
477
- const [{ userService }, { accountService }] = await Promise.all([
478
- import("../../users/services/user.service"),
479
- import("../../accounts/services/account.service"),
480
- ]);
481
- const [user, account] = await Promise.all([
482
- userService.findById(),
483
- accountService.findCurrentAccount(),
484
- ]);
485
-
486
- return {
487
- user,
488
- account,
489
- accessToken: response.cookie,
490
- refreshToken: "",
491
- expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
492
- requiresEmailVerification: !accountData.verified,
493
- };
494
- }
495
-
496
- async logout(clientInfo: ClientInfo): Promise<LogoutResponse> {
497
- const cookie = await this.getToken();
498
-
499
- if (!cookie) {
500
- throw new ApiError(
501
- "Usuário não autenticado",
502
- "NOT_AUTHENTICATED_LOGOUT",
503
- 401
504
- );
505
- }
506
-
507
- const payload: LogoutRequest = {
508
- location: clientInfo.location,
509
- ip: clientInfo.ip,
510
- timezone: clientInfo.timezone,
511
- agent: clientInfo.agent,
512
- cookie,
513
- };
514
-
515
- try {
516
- const response = await api.apps.post<LogoutApiResponse>(
517
- "/auth/logout",
518
- payload,
519
- );
520
-
521
- if (response.status === 0) {
522
- throw new ApiError(
523
- response.message || "Erro ao realizar logout",
524
- response.code || "LOGOUT_FAILED",
525
- 400,
526
- );
527
- }
528
- } finally {
529
- await this.removeAuthCookie();
530
- }
531
-
532
- return {
533
- success: true,
534
- };
535
- }
536
-
537
- async forgotPassword(
538
- _data: ForgotPasswordRequest,
539
- ): Promise<ForgotPasswordResponse> {
540
- throw new ApiError(
541
- "Recuperação de senha não implementada",
542
- "NOT_IMPLEMENTED",
543
- 501,
544
- );
545
- }
546
-
547
- async resetPassword(
548
- _data: ResetPasswordRequest,
549
- ): Promise<ResetPasswordResponse> {
550
- throw new ApiError(
551
- "Redefinição de senha não implementada",
552
- "NOT_IMPLEMENTED",
553
- 501,
554
- );
555
- }
556
-
557
- async verifyEmail(_data: VerifyEmailRequest): Promise<VerifyEmailResponse> {
558
- throw new ApiError(
559
- "Verificação de email não implementada",
560
- "NOT_IMPLEMENTED",
561
- 501,
562
- );
563
- }
564
-
565
- async resendVerification(
566
- _data: ResendVerificationRequest,
567
- ): Promise<ResendVerificationResponse> {
568
- throw new ApiError(
569
- "Reenvio de verificação não implementado",
570
- "NOT_IMPLEMENTED",
571
- 501,
572
- );
573
- }
574
-
575
- async validateSession(clientInfo: ClientInfo): Promise<boolean> {
576
- const cookie = await this.getToken();
577
-
578
- if (!cookie) {
579
- return false;
580
- }
581
-
582
- try {
583
- const requestConfig = await buildWlOverrideFromJwt(cookie);
584
-
585
- const payload: SessionKeepRequest = {
586
- location: clientInfo.location,
587
- ip: clientInfo.ip,
588
- timezone: clientInfo.timezone,
589
- agent: clientInfo.agent,
590
- cookie,
591
- };
592
-
593
- const response = await api.apps.post<SessionKeepResponse>(
594
- "/auth/keep",
595
- payload,
596
- requestConfig,
597
- );
598
-
599
- return response.status === 1;
600
- } catch (error) {
601
- console.error("[AuthService] validateSession error", error);
602
- return false;
603
- }
604
- }
605
-
606
- async isAuthenticated(): Promise<boolean> {
607
- const token = await this.getToken();
608
- return !!token;
609
- }
610
-
611
- async getToken(): Promise<string | undefined> {
612
- if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;
613
- const cookieStore = await cookies();
614
- return cookieStore.get(AUTH_COOKIE_NAME)?.value;
615
- }
616
-
617
- private async resolvePlanId(idPlan?: number | string | null): Promise<number> {
618
- if (idPlan == null || idPlan === "") return -1;
619
- if (typeof idPlan === "number" || Number(idPlan)) return Number(idPlan);
620
-
621
- if (typeof idPlan === "string") {
622
- const response = await api.apps.get<SearchPlansApiResponse>(
623
- `/plans?search=${encodeURIComponent(idPlan)}`,
624
- );
625
-
626
- if (response.status === 1 && response.data?.length) {
627
- return response.data[0].id;
628
- }
629
-
630
- throw new ApiError(
631
- `Plano "${idPlan}" não encontrado`,
632
- "PLAN_NOT_FOUND",
633
- 404,
634
- );
635
- }
636
- return -1;
637
- }
638
-
639
- private async setAuthCookie(token: string): Promise<void> {
640
- const cookieStore = await cookies();
641
- const whitelabel = await findWhitelabel().catch(() => null);
642
- const cookieDomain =
643
- this.normalizeCookieDomain(whitelabel?.domain);
644
-
645
- cookieStore.set(AUTH_COOKIE_NAME, token, {
646
- ...COOKIE_OPTIONS,
647
- ...(cookieDomain ? { domain: cookieDomain } : {}),
648
- maxAge: COOKIE_MAX_AGE,
649
- });
650
- }
651
-
652
- async removeAuthCookie(): Promise<void> {
653
- const cookieStore = await cookies();
654
- const whitelabel = await findWhitelabel().catch(() => null);
655
- const cookieDomain =
656
- this.normalizeCookieDomain(whitelabel?.domain);
657
-
658
- cookieStore.delete({
659
- name: AUTH_COOKIE_NAME,
660
- ...COOKIE_OPTIONS,
661
- ...(cookieDomain ? { domain: cookieDomain } : {}),
662
- });
663
- }
664
-
665
- private normalizeCookieDomain(domain?: string | null): string | undefined {
666
- if (!domain) return undefined;
667
-
668
- const normalized = domain.trim();
669
- if (!normalized) return undefined;
670
-
671
- const rawHost = (() => {
672
- try {
673
- return new URL(normalized).hostname;
674
- } catch {
675
- return normalized
676
- .replace(/^https?:\/\//i, "")
677
- .replace(/^www\./i, "")
678
- .split("/")[0]
679
- .split(":")[0];
680
- }
681
- })();
682
-
683
- if (!rawHost || rawHost === "localhost") {
684
- return undefined;
685
- }
686
-
687
- return rawHost.startsWith(".") ? rawHost : `.${rawHost}`;
688
- }
689
- }
690
-
691
- export const authService = new AuthService();
692
-
693
- export type { ClientInfo, GeoLocation };
1
+ import { cookies } from "next/headers";
2
+
3
+ import { api, type RequestConfig } from "../../../infra/api/client";
4
+ import { ApiError } from "../../../infra/api/types";
5
+ import { whitelabelService } from "../../whitelabel/services/whitelabel.service";
6
+ import { buildWlOverrideFromJwt } from "../utils/build-wl-override";
7
+ import {
8
+ ClientInfo,
9
+ ForgotPasswordRequest,
10
+ ForgotPasswordResponse,
11
+ GeoLocation,
12
+ LoginApiResponse,
13
+ LoginRequest,
14
+ LoginResponse,
15
+ LogoutApiResponse,
16
+ LogoutRequest,
17
+ LogoutResponse,
18
+ RegisterApiRequest,
19
+ RegisterApiResponse,
20
+ RegisterRequest,
21
+ RegisterResponse,
22
+ ResendVerificationRequest,
23
+ ResendVerificationResponse,
24
+ ResetPasswordRequest,
25
+ ResetPasswordResponse,
26
+ SearchPlansApiResponse,
27
+ SessionKeepRequest,
28
+ SessionKeepResponse,
29
+ SocialGoogleApiResponse,
30
+ SocialGoogleResponse,
31
+ SocialOnboardingApiResponse,
32
+ SocialOnboardingRequest,
33
+ SocialOnboardingResponse,
34
+ IsolatedLoginResponse,
35
+ TwoFactorApiResponse,
36
+ TwoFactorRequest,
37
+ TwoFactorResponse,
38
+ UnlinkGoogleApiResponse,
39
+ UnlinkGoogleResponse,
40
+ VerifyEmailRequest,
41
+ VerifyEmailResponse,
42
+ } from "../schema";
43
+ import { findWhitelabel } from "../../../server";
44
+
45
+ const AUTH_COOKIE_NAME = "greatapps";
46
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
47
+
48
+ /** Plano Free (ID fixo). */
49
+ const FREE_PLAN_ID = 5;
50
+
51
+ /**
52
+ * O cadastro pede o Free de duas formas, e as duas valem: ausência de plano (que o
53
+ * `resolvePlanId` traduz para o sentinela -1) e o id do próprio Free.
54
+ *
55
+ * Olhar só o sentinela fazia `?id_plan=5` cair no ramo `trial` e criar um trial de 7 dias
56
+ * do plano gratuito, com `date_due` e `trial_days` — estado que não existe no produto.
57
+ */
58
+ function isFreePlan(idPlan: number): boolean {
59
+ return idPlan === -1 || idPlan === FREE_PLAN_ID;
60
+ }
61
+
62
+ const COOKIE_OPTIONS = {
63
+ httpOnly: true,
64
+ secure: process.env.NODE_ENV === "production",
65
+ sameSite: "lax" as const,
66
+ path: "/",
67
+ };
68
+
69
+ class AuthService {
70
+ async login(
71
+ credentials: LoginRequest,
72
+ clientInfo: ClientInfo,
73
+ ): Promise<LoginResponse>;
74
+ async login(
75
+ credentials: LoginRequest,
76
+ clientInfo: ClientInfo,
77
+ options: { setCookie: false },
78
+ ): Promise<IsolatedLoginResponse>;
79
+ async login(
80
+ credentials: LoginRequest,
81
+ clientInfo: ClientInfo,
82
+ options?: { setCookie?: boolean },
83
+ ): Promise<LoginResponse | IsolatedLoginResponse>;
84
+ async login(
85
+ credentials: LoginRequest,
86
+ clientInfo: ClientInfo,
87
+ options?: { setCookie?: boolean },
88
+ ): Promise<LoginResponse | IsolatedLoginResponse> {
89
+ let requestConfig: RequestConfig | undefined;
90
+ if (credentials.id_wl) {
91
+ const wlToken = await whitelabelService.getTokenByWhitelabelId(
92
+ credentials.id_wl,
93
+ );
94
+ requestConfig = { whiteLabelId: credentials.id_wl, authToken: wlToken };
95
+ }
96
+
97
+ const response = await api.apps.post<LoginApiResponse>(
98
+ "/auth/login",
99
+ {
100
+ email: credentials.email,
101
+ password: credentials.password,
102
+ source: credentials?.source,
103
+ location: clientInfo.location,
104
+ ip: clientInfo.ip,
105
+ timezone: clientInfo.timezone,
106
+ agent: clientInfo.agent,
107
+ },
108
+ requestConfig,
109
+ );
110
+
111
+ if (response.status === 0) {
112
+ throw new ApiError(
113
+ response.message || "E-mail ou senha incorretos",
114
+ response.code || "LOGIN_FAILED",
115
+ 401,
116
+ );
117
+ }
118
+
119
+ if (!response.cookie) {
120
+ throw new ApiError(
121
+ "Resposta de autenticação inválida",
122
+ "INVALID_RESPONSE",
123
+ 500,
124
+ );
125
+ }
126
+
127
+ if (response.two_factor_required) {
128
+ return { result: "two_factor_required", cookie: response.cookie, twoFactorMode: response.two_factor_required };
129
+ }
130
+
131
+ // Modo isolado: caller só quer o token, sem afetar a sessão do navegador
132
+ // nem pagar pelas chamadas de user/account (ex.: fluxo de SSO).
133
+ if (options?.setCookie === false) {
134
+ return { result: "success", accessToken: response.cookie };
135
+ }
136
+
137
+ await this.setAuthCookie(response.cookie);
138
+
139
+ const [{ userService }, { accountService }] = await Promise.all([
140
+ import("../../users/services/user.service"),
141
+ import("../../accounts/services/account.service"),
142
+ ]);
143
+ const [user, account] = await Promise.all([
144
+ userService.findById(
145
+ requestConfig ? { authToken: requestConfig.authToken } : undefined,
146
+ ),
147
+ accountService.findCurrentAccount(requestConfig),
148
+ ]);
149
+
150
+ return {
151
+ result: "success",
152
+ user,
153
+ account,
154
+ accessToken: response.cookie,
155
+ refreshToken: "",
156
+ expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
157
+ };
158
+ }
159
+
160
+ async verifyTwoFactor(
161
+ cookie: string,
162
+ code: string,
163
+ clientInfo: ClientInfo,
164
+ options?: { window?: number; setCookie?: boolean },
165
+ ): Promise<TwoFactorResponse> {
166
+ const payload: TwoFactorRequest = {
167
+ location: clientInfo.location,
168
+ ip: clientInfo.ip,
169
+ timezone: clientInfo.timezone,
170
+ agent: clientInfo.agent,
171
+ cookie,
172
+ code,
173
+ };
174
+
175
+ const endpoint = options?.window !== undefined ? `/auth/code?window=${options.window}` : '/auth/code';
176
+ const response = await api.apps.post<TwoFactorApiResponse>(
177
+ endpoint,
178
+ payload,
179
+ );
180
+
181
+ if (response.status === 0) {
182
+ throw new ApiError("Código 2FA inválido", "TWO_FACTOR_FAILED", 401);
183
+ }
184
+
185
+ if (!response?.data?.cookie) {
186
+ throw new ApiError(
187
+ "Resposta de autenticação inválida após 2FA",
188
+ "INVALID_RESPONSE",
189
+ 500,
190
+ );
191
+ }
192
+
193
+ // Modo isolado: devolve o token sem gravar o cookie — o caller redireciona
194
+ // pra outro lugar (ex.: SSO callback) e não quer mexer na sessão atual.
195
+ if (options?.setCookie === false) {
196
+ return { status: 1, accessToken: response.data.cookie };
197
+ }
198
+
199
+ await this.setAuthCookie(response.data.cookie);
200
+
201
+ return { status: 1 };
202
+ }
203
+
204
+ async socialLoginGoogle(
205
+ code: string,
206
+ clientInfo: ClientInfo,
207
+ ): Promise<SocialGoogleResponse> {
208
+ const response = await api.apps.post<SocialGoogleApiResponse>(
209
+ "/auth/social/google",
210
+ {
211
+ code,
212
+ location: clientInfo.location,
213
+ ip: clientInfo.ip,
214
+ timezone: clientInfo.timezone,
215
+ agent: clientInfo.agent,
216
+ verification: true,
217
+ },
218
+ );
219
+
220
+ if (response.status === 0) {
221
+ if (response.code === "link_requires_password") {
222
+ return {
223
+ result: "link_requires_password",
224
+ message:
225
+ response.message ||
226
+ "Uma conta com esse e-mail já existe. Faça login com e-mail e senha para vincular sua conta Google.",
227
+ };
228
+ }
229
+ throw new ApiError(
230
+ response.message || "Falha no login com Google",
231
+ response.code || "GOOGLE_LOGIN_FAILED",
232
+ 401,
233
+ );
234
+ }
235
+
236
+ if (response.needs_onboarding) {
237
+ if (!response.onboarding_token || !response.partial?.email) {
238
+ throw new ApiError(
239
+ "Resposta de onboarding inválida",
240
+ "INVALID_RESPONSE",
241
+ 500,
242
+ );
243
+ }
244
+ return {
245
+ result: "needs_onboarding",
246
+ onboardingToken: response.onboarding_token,
247
+ partial: response.partial,
248
+ };
249
+ }
250
+
251
+ if (!response.cookie) {
252
+ throw new ApiError(
253
+ "Resposta de autenticação inválida",
254
+ "INVALID_RESPONSE",
255
+ 500,
256
+ );
257
+ }
258
+
259
+ if (response.two_factor_required) {
260
+ return {
261
+ result: "two_factor_required",
262
+ cookie: response.cookie,
263
+ twoFactorMode: response.two_factor_required,
264
+ };
265
+ }
266
+
267
+ await this.setAuthCookie(response.cookie);
268
+
269
+ const [{ userService }, { accountService }] = await Promise.all([
270
+ import("../../users/services/user.service"),
271
+ import("../../accounts/services/account.service"),
272
+ ]);
273
+ const [user, account] = await Promise.all([
274
+ userService.findById(),
275
+ accountService.findCurrentAccount(),
276
+ ]);
277
+
278
+ return {
279
+ result: "success",
280
+ user,
281
+ account,
282
+ accessToken: response.cookie,
283
+ expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
284
+ };
285
+ }
286
+
287
+ async completeGoogleOnboarding(
288
+ data: SocialOnboardingRequest,
289
+ clientInfo: ClientInfo,
290
+ ): Promise<SocialOnboardingResponse> {
291
+ const today = new Date();
292
+ const trialEndDate = new Date(today);
293
+ const trialDays = Math.min(Math.max(data.trialDays ?? 7, 1), 90);
294
+ trialEndDate.setDate(today.getDate() + trialDays);
295
+
296
+ const idPlan = await this.resolvePlanId(data.idPlan);
297
+
298
+ const payload = {
299
+ onboarding_token: data.onboardingToken,
300
+ name: data.user.name,
301
+ bussiness_type: data.businessType ?? 0,
302
+ language: "pt-br",
303
+ timezone: "America/Sao_Paulo",
304
+ currency: "BRL",
305
+ location: clientInfo.location,
306
+ ip: clientInfo.ip,
307
+ agent: clientInfo.agent,
308
+ id_affiliate: data.affiliateId || 0,
309
+ origin: data.origin,
310
+ user: {
311
+ id_api: "",
312
+ name: data.user.name || "",
313
+ last_name: data.user.last_name || "",
314
+ rg: data.user.rg || "",
315
+ cpf: data.user.cpf || "",
316
+ gender: data.user.gender ?? 1,
317
+ ddi: data.user.ddi || "55",
318
+ phone: data.user.phone || "",
319
+ profile: "owner",
320
+ language: "pt-br",
321
+ },
322
+ subscription:
323
+ isFreePlan(idPlan)
324
+ ? {
325
+ type: "free",
326
+ id_coupon: data.couponId || 0,
327
+ id_plan: FREE_PLAN_ID,
328
+ id_product: 1,
329
+ }
330
+ : {
331
+ type: "trial",
332
+ id_coupon: data.couponId || 0,
333
+ id_plan: idPlan,
334
+ date_due: trialEndDate.toISOString().split("T")[0],
335
+ trial_days: trialDays,
336
+ id_product: 1,
337
+ },
338
+ };
339
+
340
+ const response = await api.apps.post<SocialOnboardingApiResponse>(
341
+ "/auth/social/onboarding",
342
+ payload,
343
+ );
344
+
345
+ if (response.status === 0) {
346
+ throw new ApiError(
347
+ response.message || "Erro ao finalizar cadastro com Google",
348
+ "ONBOARDING_FAILED",
349
+ 400,
350
+ );
351
+ }
352
+
353
+ if (!response.data?.length || !response.cookie) {
354
+ throw new ApiError(
355
+ "Resposta de cadastro inválida",
356
+ "INVALID_RESPONSE",
357
+ 500,
358
+ );
359
+ }
360
+
361
+ await this.setAuthCookie(response.cookie);
362
+
363
+ const [{ userService }, { accountService }] = await Promise.all([
364
+ import("../../users/services/user.service"),
365
+ import("../../accounts/services/account.service"),
366
+ ]);
367
+ const [user, account] = await Promise.all([
368
+ userService.findById(),
369
+ accountService.findCurrentAccount(),
370
+ ]);
371
+
372
+ return {
373
+ user,
374
+ account,
375
+ accessToken: response.cookie,
376
+ expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
377
+ };
378
+ }
379
+
380
+ async unlinkGoogle(): Promise<UnlinkGoogleResponse> {
381
+ const { id_account, id_user } = await import(
382
+ "../utils/get-user-context"
383
+ ).then((m) => m.getUserContext());
384
+
385
+ const response = await api.apps.delete<UnlinkGoogleApiResponse>(
386
+ `/accounts/${id_account}/users/${id_user}/social/google`,
387
+ );
388
+
389
+ if (response.status === 0) {
390
+ throw new ApiError(
391
+ response.message || "Erro ao desvincular conta Google",
392
+ "UNLINK_FAILED",
393
+ 400,
394
+ );
395
+ }
396
+
397
+ return {
398
+ success: true,
399
+ message: response.message,
400
+ };
401
+ }
402
+
403
+ async register(
404
+ data: RegisterRequest,
405
+ clientInfo: ClientInfo,
406
+ ): Promise<RegisterResponse> {
407
+ const today = new Date();
408
+ const trialEndDate = new Date(today);
409
+ const trialDays = Math.min(Math.max(data.trialDays ?? 7, 1), 90);
410
+ trialEndDate.setDate(today.getDate() + trialDays);
411
+
412
+ const idPlan = await this.resolvePlanId(data.idPlan);
413
+
414
+ const payload: RegisterApiRequest = {
415
+ name: data.accountName,
416
+ bussiness_type: data.businessType ?? 0,
417
+ language: "pt-br",
418
+ timezone: "America/Sao_Paulo",
419
+ currency: "BRL",
420
+ location: clientInfo.location,
421
+ ip: clientInfo.ip,
422
+ agent: clientInfo.agent,
423
+ id_affiliate: data.affiliateId || 0,
424
+ origin: data.origin,
425
+ user: {
426
+ id_api: "",
427
+ name: data.name,
428
+ last_name: data.lastName || "",
429
+ rg: data.rg || "",
430
+ cpf: data.cpf || "",
431
+ gender: data.gender,
432
+ email: data.email,
433
+ phone: data.phone,
434
+ password: data.password,
435
+ profile: "owner",
436
+ language: "pt-br",
437
+ },
438
+ subscription:
439
+ isFreePlan(idPlan)
440
+ ? {
441
+ type: "free",
442
+ id_coupon: data.couponId || 0,
443
+ id_plan: FREE_PLAN_ID,
444
+ id_product: 1,
445
+ }
446
+ : {
447
+ type: "trial",
448
+ id_coupon: data.couponId || 0,
449
+ id_plan: idPlan,
450
+ // Fluxo não-Stripe lê `date_due`; fluxo Stripe lê `trial_days`.
451
+ // Mandamos os dois pra cobrir ambos os caminhos da API.
452
+ date_due: trialEndDate.toISOString().split("T")[0],
453
+ trial_days: trialDays,
454
+ id_product: 1,
455
+ },
456
+ };
457
+
458
+ const response = await api.apps.post<RegisterApiResponse>(
459
+ "/accounts",
460
+ payload,
461
+ );
462
+
463
+ if (response.status === 0) {
464
+ throw new ApiError(
465
+ response.message || "Erro ao criar conta",
466
+ "REGISTER_FAILED",
467
+ 400,
468
+ );
469
+ }
470
+
471
+ if (!response.data || response.data.length === 0) {
472
+ throw new ApiError(
473
+ "Resposta de registro inválida",
474
+ "INVALID_RESPONSE",
475
+ 500,
476
+ );
477
+ }
478
+
479
+ if (!response.cookie) {
480
+ throw new ApiError(
481
+ "Resposta de autenticação inválida",
482
+ "INVALID_RESPONSE",
483
+ 500,
484
+ );
485
+ }
486
+
487
+ const accountData = response.data[0];
488
+
489
+ await this.setAuthCookie(response.cookie);
490
+
491
+ const [{ userService }, { accountService }] = await Promise.all([
492
+ import("../../users/services/user.service"),
493
+ import("../../accounts/services/account.service"),
494
+ ]);
495
+ const [user, account] = await Promise.all([
496
+ userService.findById(),
497
+ accountService.findCurrentAccount(),
498
+ ]);
499
+
500
+ return {
501
+ user,
502
+ account,
503
+ accessToken: response.cookie,
504
+ refreshToken: "",
505
+ expiresAt: new Date(Date.now() + COOKIE_MAX_AGE * 1000).toISOString(),
506
+ requiresEmailVerification: !accountData.verified,
507
+ };
508
+ }
509
+
510
+ async logout(clientInfo: ClientInfo): Promise<LogoutResponse> {
511
+ const cookie = await this.getToken();
512
+
513
+ if (!cookie) {
514
+ throw new ApiError(
515
+ "Usuário não autenticado",
516
+ "NOT_AUTHENTICATED_LOGOUT",
517
+ 401
518
+ );
519
+ }
520
+
521
+ const payload: LogoutRequest = {
522
+ location: clientInfo.location,
523
+ ip: clientInfo.ip,
524
+ timezone: clientInfo.timezone,
525
+ agent: clientInfo.agent,
526
+ cookie,
527
+ };
528
+
529
+ try {
530
+ const response = await api.apps.post<LogoutApiResponse>(
531
+ "/auth/logout",
532
+ payload,
533
+ );
534
+
535
+ if (response.status === 0) {
536
+ throw new ApiError(
537
+ response.message || "Erro ao realizar logout",
538
+ response.code || "LOGOUT_FAILED",
539
+ 400,
540
+ );
541
+ }
542
+ } finally {
543
+ await this.removeAuthCookie();
544
+ }
545
+
546
+ return {
547
+ success: true,
548
+ };
549
+ }
550
+
551
+ async forgotPassword(
552
+ _data: ForgotPasswordRequest,
553
+ ): Promise<ForgotPasswordResponse> {
554
+ throw new ApiError(
555
+ "Recuperação de senha não implementada",
556
+ "NOT_IMPLEMENTED",
557
+ 501,
558
+ );
559
+ }
560
+
561
+ async resetPassword(
562
+ _data: ResetPasswordRequest,
563
+ ): Promise<ResetPasswordResponse> {
564
+ throw new ApiError(
565
+ "Redefinição de senha não implementada",
566
+ "NOT_IMPLEMENTED",
567
+ 501,
568
+ );
569
+ }
570
+
571
+ async verifyEmail(_data: VerifyEmailRequest): Promise<VerifyEmailResponse> {
572
+ throw new ApiError(
573
+ "Verificação de email não implementada",
574
+ "NOT_IMPLEMENTED",
575
+ 501,
576
+ );
577
+ }
578
+
579
+ async resendVerification(
580
+ _data: ResendVerificationRequest,
581
+ ): Promise<ResendVerificationResponse> {
582
+ throw new ApiError(
583
+ "Reenvio de verificação não implementado",
584
+ "NOT_IMPLEMENTED",
585
+ 501,
586
+ );
587
+ }
588
+
589
+ async validateSession(clientInfo: ClientInfo): Promise<boolean> {
590
+ const cookie = await this.getToken();
591
+
592
+ if (!cookie) {
593
+ return false;
594
+ }
595
+
596
+ try {
597
+ const requestConfig = await buildWlOverrideFromJwt(cookie);
598
+
599
+ const payload: SessionKeepRequest = {
600
+ location: clientInfo.location,
601
+ ip: clientInfo.ip,
602
+ timezone: clientInfo.timezone,
603
+ agent: clientInfo.agent,
604
+ cookie,
605
+ };
606
+
607
+ const response = await api.apps.post<SessionKeepResponse>(
608
+ "/auth/keep",
609
+ payload,
610
+ requestConfig,
611
+ );
612
+
613
+ return response.status === 1;
614
+ } catch (error) {
615
+ console.error("[AuthService] validateSession error", error);
616
+ return false;
617
+ }
618
+ }
619
+
620
+ async isAuthenticated(): Promise<boolean> {
621
+ const token = await this.getToken();
622
+ return !!token;
623
+ }
624
+
625
+ async getToken(): Promise<string | undefined> {
626
+ if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;
627
+ const cookieStore = await cookies();
628
+ return cookieStore.get(AUTH_COOKIE_NAME)?.value;
629
+ }
630
+
631
+ private async resolvePlanId(idPlan?: number | string | null): Promise<number> {
632
+ if (idPlan == null || idPlan === "") return -1;
633
+ if (typeof idPlan === "number" || Number(idPlan)) return Number(idPlan);
634
+
635
+ if (typeof idPlan === "string") {
636
+ const response = await api.apps.get<SearchPlansApiResponse>(
637
+ `/plans?search=${encodeURIComponent(idPlan)}`,
638
+ );
639
+
640
+ if (response.status === 1 && response.data?.length) {
641
+ return response.data[0].id;
642
+ }
643
+
644
+ throw new ApiError(
645
+ `Plano "${idPlan}" não encontrado`,
646
+ "PLAN_NOT_FOUND",
647
+ 404,
648
+ );
649
+ }
650
+ return -1;
651
+ }
652
+
653
+ private async setAuthCookie(token: string): Promise<void> {
654
+ const cookieStore = await cookies();
655
+ const whitelabel = await findWhitelabel().catch(() => null);
656
+ const cookieDomain =
657
+ this.normalizeCookieDomain(whitelabel?.domain);
658
+
659
+ cookieStore.set(AUTH_COOKIE_NAME, token, {
660
+ ...COOKIE_OPTIONS,
661
+ ...(cookieDomain ? { domain: cookieDomain } : {}),
662
+ maxAge: COOKIE_MAX_AGE,
663
+ });
664
+ }
665
+
666
+ async removeAuthCookie(): Promise<void> {
667
+ const cookieStore = await cookies();
668
+ const whitelabel = await findWhitelabel().catch(() => null);
669
+ const cookieDomain =
670
+ this.normalizeCookieDomain(whitelabel?.domain);
671
+
672
+ cookieStore.delete({
673
+ name: AUTH_COOKIE_NAME,
674
+ ...COOKIE_OPTIONS,
675
+ ...(cookieDomain ? { domain: cookieDomain } : {}),
676
+ });
677
+ }
678
+
679
+ private normalizeCookieDomain(domain?: string | null): string | undefined {
680
+ if (!domain) return undefined;
681
+
682
+ const normalized = domain.trim();
683
+ if (!normalized) return undefined;
684
+
685
+ const rawHost = (() => {
686
+ try {
687
+ return new URL(normalized).hostname;
688
+ } catch {
689
+ return normalized
690
+ .replace(/^https?:\/\//i, "")
691
+ .replace(/^www\./i, "")
692
+ .split("/")[0]
693
+ .split(":")[0];
694
+ }
695
+ })();
696
+
697
+ if (!rawHost || rawHost === "localhost") {
698
+ return undefined;
699
+ }
700
+
701
+ return rawHost.startsWith(".") ? rawHost : `.${rawHost}`;
702
+ }
703
+ }
704
+
705
+ export const authService = new AuthService();
706
+
707
+ export type { ClientInfo, GeoLocation };