@greatapps/common 1.1.785 → 1.1.787

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.
Files changed (113) hide show
  1. package/dist/components/layouts/AppMobileNavBar.mjs +64 -5
  2. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  3. package/dist/components/layouts/AppNavBar.mjs +2 -0
  4. package/dist/components/layouts/AppNavBar.mjs.map +1 -1
  5. package/dist/components/layouts/GreatDomainIcon.mjs +40 -0
  6. package/dist/components/layouts/GreatDomainIcon.mjs.map +1 -0
  7. package/dist/components/layouts/NavBar.mjs +111 -52
  8. package/dist/components/layouts/NavBar.mjs.map +1 -1
  9. package/dist/components/ui/form/PhoneInput.mjs +1 -1
  10. package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
  11. package/dist/handlers.mjs +2 -0
  12. package/dist/handlers.mjs.map +1 -1
  13. package/dist/i18n/messages/en-us.mjs +3 -1
  14. package/dist/i18n/messages/en-us.mjs.map +1 -1
  15. package/dist/i18n/messages/es-es.mjs +3 -1
  16. package/dist/i18n/messages/es-es.mjs.map +1 -1
  17. package/dist/i18n/messages/pt-br.mjs +3 -1
  18. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  19. package/dist/index.mjs +11 -2
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/infra/api/client.mjs +5 -1
  22. package/dist/infra/api/client.mjs.map +1 -1
  23. package/dist/infra/api/types.mjs +1 -1
  24. package/dist/infra/api/types.mjs.map +1 -1
  25. package/dist/infra/route/safe-route-handler.mjs +4 -1
  26. package/dist/infra/route/safe-route-handler.mjs.map +1 -1
  27. package/dist/middlewares/create-auth-middleware.mjs +11 -1
  28. package/dist/middlewares/create-auth-middleware.mjs.map +1 -1
  29. package/dist/modules/auth/utils/assert-account-not-frozen.mjs +6 -5
  30. package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
  31. package/dist/modules/auth/utils/assert-management-subscription.mjs +7 -14
  32. package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
  33. package/dist/modules/auth/utils/assert-paid-subscription.mjs +7 -12
  34. package/dist/modules/auth/utils/assert-paid-subscription.mjs.map +1 -1
  35. package/dist/modules/auth/utils/assert-subscription-addon.mjs +2 -10
  36. package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
  37. package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
  38. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs +10 -0
  39. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs.map +1 -0
  40. package/dist/modules/subscriptions/constants/subscription.constants.mjs +2 -0
  41. package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
  42. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs +17 -0
  43. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs.map +1 -0
  44. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs +2 -13
  45. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
  46. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs +28 -0
  47. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs.map +1 -0
  48. package/dist/modules/subscriptions/services/subscriptions.service.mjs +26 -0
  49. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
  50. package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
  51. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs +16 -0
  52. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs.map +1 -0
  53. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs +1 -1
  54. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs.map +1 -1
  55. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
  56. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
  57. package/dist/modules/subscriptions/utils/read-current-subscription.mjs +21 -0
  58. package/dist/modules/subscriptions/utils/read-current-subscription.mjs.map +1 -0
  59. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +9 -6
  60. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
  61. package/dist/modules/whitelabel/services/whitelabel.service.mjs +28 -2
  62. package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
  63. package/dist/providers/auth.provider.mjs +12 -9
  64. package/dist/providers/auth.provider.mjs.map +1 -1
  65. package/dist/server.mjs +4 -0
  66. package/dist/server.mjs.map +1 -1
  67. package/dist/store/useMdSidebarStore.mjs +17 -4
  68. package/dist/store/useMdSidebarStore.mjs.map +1 -1
  69. package/dist/utils/redirect.mjs +10 -0
  70. package/dist/utils/redirect.mjs.map +1 -1
  71. package/dist/utils/safeServerAction.mjs +4 -1
  72. package/dist/utils/safeServerAction.mjs.map +1 -1
  73. package/dist/utils/withAction.mjs +2 -1
  74. package/dist/utils/withAction.mjs.map +1 -1
  75. package/package.json +1 -1
  76. package/src/components/layouts/AppMobileNavBar.tsx +90 -6
  77. package/src/components/layouts/AppNavBar.tsx +5 -1
  78. package/src/components/layouts/GreatDomainIcon.tsx +43 -0
  79. package/src/components/layouts/NavBar.tsx +160 -66
  80. package/src/components/ui/form/PhoneInput.tsx +1 -1
  81. package/src/handlers.ts +1 -0
  82. package/src/i18n/messages/en-us.ts +2 -0
  83. package/src/i18n/messages/es-es.ts +2 -0
  84. package/src/i18n/messages/pt-br.ts +2 -0
  85. package/src/index.ts +8 -2
  86. package/src/infra/api/client.ts +11 -3
  87. package/src/infra/api/types.ts +7 -5
  88. package/src/infra/route/safe-route-handler.ts +4 -1
  89. package/src/middlewares/create-auth-middleware.ts +12 -1
  90. package/src/modules/auth/utils/assert-account-not-frozen.ts +7 -6
  91. package/src/modules/auth/utils/assert-management-subscription.ts +15 -23
  92. package/src/modules/auth/utils/assert-paid-subscription.ts +7 -15
  93. package/src/modules/auth/utils/assert-subscription-addon.ts +2 -12
  94. package/src/modules/plans/hooks/use-plan-by-id.hook.ts +1 -1
  95. package/src/modules/subscriptions/actions/get-current-subscription.action.ts +8 -0
  96. package/src/modules/subscriptions/constants/subscription.constants.ts +3 -0
  97. package/src/modules/subscriptions/handlers/current-subscription.handler.ts +16 -0
  98. package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +7 -17
  99. package/src/modules/subscriptions/hooks/use-current-subscription.hook.ts +38 -0
  100. package/src/modules/subscriptions/services/subscriptions.service.ts +34 -0
  101. package/src/modules/subscriptions/types/subscription.type.ts +2 -0
  102. package/src/modules/subscriptions/utils/group-subscriptions-by-product.ts +23 -0
  103. package/src/modules/subscriptions/utils/has-active-paid-subscription.ts +1 -2
  104. package/src/modules/subscriptions/utils/has-paid-subscription.ts +1 -2
  105. package/src/modules/subscriptions/utils/read-current-subscription.ts +34 -0
  106. package/src/modules/whitelabel/actions/find-whitelabel.action.ts +13 -9
  107. package/src/modules/whitelabel/services/whitelabel.service.ts +34 -2
  108. package/src/providers/auth.provider.tsx +13 -10
  109. package/src/server.ts +2 -0
  110. package/src/store/useMdSidebarStore.ts +20 -5
  111. package/src/utils/redirect.ts +10 -0
  112. package/src/utils/safeServerAction.ts +4 -6
  113. package/src/utils/withAction.ts +2 -1
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
 
3
3
  import { continueChain, MiddlewareConfig, MiddlewareFunction, MiddlewareResult, stopChain } from './types';
4
4
  import { whitelabelService } from '../modules/whitelabel/services/whitelabel.service';
5
+ import { isSameSiteUrl } from '../utils/redirect';
5
6
 
6
7
  function isRouteEquals(pathname: string, routes: string[]): boolean {
7
8
  return routes.some((route) => pathname === route || pathname.startsWith(`${route}/`));
@@ -27,8 +28,18 @@ export function createAuthMiddleware(options?: CreateAuthMiddlewareOptions): Mid
27
28
  const host = request.headers.get('host') || '';
28
29
  const whitelabel = await whitelabelService.getTokenByDomain(host).catch(() => null);
29
30
  const accountsUrl = whitelabel?.accounts_url;
31
+ const accountsUrlIsSameSite = !!accountsUrl && isSameSiteUrl(accountsUrl, host);
32
+
33
+ if (accountsUrl && !accountsUrlIsSameSite) {
34
+ console.error('[auth-middleware] accounts_url fora do site do host da requisição', {
35
+ host,
36
+ pathname,
37
+ whitelabelId: whitelabel?.id,
38
+ accountsUrl,
39
+ });
40
+ }
30
41
 
31
- const loginUrl = accountsUrl
42
+ const loginUrl = accountsUrlIsSameSite
32
43
  ? new URL('/login', accountsUrl)
33
44
  : new URL('/login', request.url);
34
45
 
@@ -1,22 +1,23 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
5
  import { getAccountFreezeState } from "../../subscriptions/utils/get-account-freeze-state";
6
+ import type { Subscription } from "../../subscriptions/types/subscription.type";
6
7
 
7
8
  export async function assertAccountNotFrozen(): Promise<void> {
8
- const result = await listSubscriptionsAction({ active: true });
9
+ let subscription: Subscription | null;
9
10
 
10
- if (!result.success) {
11
+ try {
12
+ subscription = await readCurrentSubscription();
13
+ } catch (error) {
11
14
  console.error(
12
15
  "[assertAccountNotFrozen] failed to read subscription context:",
13
- result.error,
16
+ error,
14
17
  );
15
18
  return;
16
19
  }
17
20
 
18
- const subscription = result.data?.data?.[0];
19
-
20
21
  if (!subscription) return;
21
22
 
22
23
  if (getAccountFreezeState(subscription) !== "frozen") return;
@@ -1,32 +1,23 @@
1
1
  import "server-only";
2
2
 
3
- import { cache } from "react";
4
-
5
3
  import { ApiError } from "../../../infra/api/types";
6
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
+ import { getSubscriptionCancellationState } from "../../subscriptions/utils/get-subscription-cancellation-state";
7
6
  import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
8
7
 
9
8
  /**
10
- * Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.
9
+ * A leitura por trás (`readCurrentSubscription`) já é deduplicada por request via
10
+ * React.cache, então não precisa de outra camada aqui: em produção (26/08) o mesmo
11
+ * carregamento disparava até quatro cópias da mesma pergunta em paralelo, com 727 ms,
12
+ * 4,2 s, 5,0 s e 7,0 s, contra um p50 de 528 ms; agora é uma leitura só,
13
+ * compartilhada pelos quatro gates.
11
14
  *
12
- * A leitura por trás é `/accounts/{id}/subscriptions?limit=1`, e ela é MAIS cara que o gate de
13
- * perfil: para conta Stripe a listagem consulta a API do gateway ao vivo. Em produção (26/08) o
14
- * mesmo carregamento disparou quatro dessas em paralelo — 727 ms, 4,2 s, 5,0 s e 7,0 s, contra um
15
- * p50 de 528 ms para o endpoint. Elas se atrasam entre si: são cópias da mesma pergunta disputando
16
- * a mesma fila.
15
+ * A checagem de cancelamento é obrigatória além da de vencimento: a corrente pode
16
+ * vir com `active: false` (baixa por falha de pagamento) com `date_due` ainda no
17
+ * futuro, caso que só `getSubscriptionExpiryDate` não cobre.
17
18
  */
18
- export const assertManagementSubscription = cache(async () => {
19
- const result = await listSubscriptionsAction({ active: true });
20
-
21
- if (!result.success) {
22
- throw new ApiError(
23
- result.error || "Não foi possível validar a assinatura atual",
24
- "SUBSCRIPTION_CONTEXT_FAILED",
25
- 403,
26
- );
27
- }
28
-
29
- const subscription = result.data?.data?.[0];
19
+ export async function assertManagementSubscription(): Promise<void> {
20
+ const subscription = await readCurrentSubscription();
30
21
 
31
22
  if (!subscription) {
32
23
  throw new ApiError(
@@ -36,14 +27,15 @@ export const assertManagementSubscription = cache(async () => {
36
27
  );
37
28
  }
38
29
 
30
+ const isCancelled = getSubscriptionCancellationState(subscription) === "cancelled";
39
31
  const expiryDate = getSubscriptionExpiryDate(subscription);
40
32
  const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();
41
33
 
42
- if (isExpired) {
34
+ if (isCancelled || isExpired) {
43
35
  throw new ApiError(
44
36
  "Sua assinatura está vencida. Assine um novo plano para continuar.",
45
37
  "SUBSCRIPTION_EXPIRED",
46
38
  403,
47
39
  );
48
40
  }
49
- });
41
+ }
@@ -1,21 +1,13 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
+ import { getSubscriptionCancellationState } from "../../subscriptions/utils/get-subscription-cancellation-state";
5
6
  import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
7
+ import { FREE_PLAN_ID } from "../../subscriptions/constants/subscription.constants";
6
8
 
7
9
  export async function assertPaidSubscription() {
8
- const result = await listSubscriptionsAction({ active: true });
9
-
10
- if (!result.success) {
11
- throw new ApiError(
12
- result.error || "Não foi possível validar a assinatura atual",
13
- "SUBSCRIPTION_CONTEXT_FAILED",
14
- 403,
15
- );
16
- }
17
-
18
- const subscription = result.data?.data?.[0];
10
+ const subscription = await readCurrentSubscription();
19
11
 
20
12
  if (!subscription) {
21
13
  throw new ApiError(
@@ -25,10 +17,11 @@ export async function assertPaidSubscription() {
25
17
  );
26
18
  }
27
19
 
20
+ const isCancelled = getSubscriptionCancellationState(subscription) === "cancelled";
28
21
  const expiryDate = getSubscriptionExpiryDate(subscription);
29
22
  const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();
30
23
 
31
- if (isExpired) {
24
+ if (isCancelled || isExpired) {
32
25
  throw new ApiError(
33
26
  "Sua assinatura está vencida. Assine um novo plano para continuar.",
34
27
  "SUBSCRIPTION_EXPIRED",
@@ -36,8 +29,7 @@ export async function assertPaidSubscription() {
36
29
  );
37
30
  }
38
31
 
39
-
40
- if (subscription.type === 'free' || subscription.id_plan === 5) {
32
+ if (subscription.type === 'free' || subscription.id_plan === FREE_PLAN_ID) {
41
33
  throw new ApiError(
42
34
  "Este recurso está disponível apenas para planos pagos. Faça upgrade do seu plano para continuar.",
43
35
  "PAID_SUBSCRIPTION_REQUIRED",
@@ -1,24 +1,14 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
5
  import type { AddonKindEnum } from "../../subscriptions/constants/addons.constants";
6
6
 
7
7
  export async function assertSubscriptionAddon(
8
8
  addonId: AddonKindEnum,
9
9
  addonLabel: string,
10
10
  ) {
11
- const result = await listSubscriptionsAction({ active: true });
12
-
13
- if (!result.success) {
14
- throw new ApiError(
15
- result.error || "Não foi possível validar a assinatura atual",
16
- "SUBSCRIPTION_CONTEXT_FAILED",
17
- 403,
18
- );
19
- }
20
-
21
- const subscription = result.data?.data?.[0];
11
+ const subscription = await readCurrentSubscription();
22
12
 
23
13
  if (!subscription) {
24
14
  throw new ApiError(
@@ -11,7 +11,7 @@ export function usePlanById(idPlan?: number | string | null) {
11
11
  const queryClient = useQueryClient();
12
12
  const plansKey = useAuthQueryKey([...PLANS_QUERY_KEY]);
13
13
 
14
- return useQuery({
14
+ return useQuery<SuccessResult<Plan>>({
15
15
  queryKey: [...plansKey, idPlan],
16
16
  queryFn: ({ signal }) =>
17
17
  apiFetch<SuccessResult<Plan>>(`/api/plans/${idPlan}`, { signal }),
@@ -0,0 +1,8 @@
1
+ 'use server';
2
+
3
+ import { safeServerAction } from '../../../utils/safeServerAction';
4
+ import { subscriptionsService } from '../services/subscriptions.service';
5
+
6
+ export async function getCurrentSubscriptionAction(params?: { id_product?: number }) {
7
+ return safeServerAction(() => subscriptionsService.getCurrentSubscription(params));
8
+ }
@@ -6,3 +6,6 @@ export const FREEZE_WARNING_DAYS_BEFORE = 1;
6
6
  export const FREEZE_ELIGIBLE_WHITELABEL_ID = 1;
7
7
 
8
8
  export const MAX_PAYMENT_ATTEMPTS = 8;
9
+
10
+ /** Plano legado "FREE" identificado por `id_plan`, fora do `type: 'free'` moderno. */
11
+ export const FREE_PLAN_ID = 5;
@@ -0,0 +1,16 @@
1
+ import 'server-only';
2
+
3
+ import type { NextRequest } from 'next/server';
4
+ import { safeRouteHandler } from '../../../infra/route/safe-route-handler';
5
+ import { searchParamsToParams } from '../../../infra/route/search-params';
6
+ import { subscriptionsService } from '../services/subscriptions.service';
7
+
8
+ export function createCurrentSubscriptionHandler() {
9
+ return async function GET(req: NextRequest) {
10
+ return safeRouteHandler(() =>
11
+ subscriptionsService.getCurrentSubscription(
12
+ searchParamsToParams<{ id_product?: number }>(req.nextUrl.searchParams),
13
+ ),
14
+ );
15
+ };
16
+ }
@@ -1,22 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { useQuery } from "@tanstack/react-query";
4
- import { apiFetch } from "../../../infra/http/api-fetch";
5
- import type { PaginatedSuccessResult } from "../../../infra/api/types";
6
- import type { Subscription } from "../types/subscription.type";
7
- import { SUBSCRIPTIONS_QUERY_KEY } from "../constants/query-keys.constants";
8
- import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
3
+ import { useCurrentSubscription } from "./use-current-subscription.hook";
9
4
 
5
+ /**
6
+ * @deprecated Use `useCurrentSubscription`, mesma leitura (mesma chave de
7
+ * cache, uma única requisição); só o nome muda para o vocabulário certo
8
+ * ("corrente", não "ativa"). Sai na 1.1.784.
9
+ */
10
10
  export function useActiveSubscription() {
11
- const queryKey = useAuthQueryKey([...SUBSCRIPTIONS_QUERY_KEY, "active"]);
12
-
13
- return useQuery({
14
- queryKey,
15
- queryFn: ({ signal }) =>
16
- apiFetch<PaginatedSuccessResult<Subscription>>('/api/subscriptions', {
17
- params: { limit: 1 },
18
- signal,
19
- }),
20
- staleTime: 10_000,
21
- });
11
+ return useCurrentSubscription();
22
12
  }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { apiFetch } from '../../../infra/http/api-fetch';
5
+ import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
6
+ import type { PaginatedSuccessResult, SuccessResult } from '../../../infra/api/types';
7
+ import type { Subscription } from '../types/subscription.type';
8
+ import { SUBSCRIPTIONS_QUERY_KEY } from '../constants/query-keys.constants';
9
+
10
+ /**
11
+ * A assinatura corrente do plano da conta, pela regra única do backend
12
+ * (contrato de domínio nunca é corrente; encerrada é decidida por `active`).
13
+ *
14
+ * Devolvida na mesma forma paginada de `useSubscriptions`/`useActiveSubscription`
15
+ * (`{ data: [sub] }`) para não exigir mudança nos call sites que já leem
16
+ * `data?.[0]`: só a URL e a regra por trás mudam.
17
+ */
18
+ export function useCurrentSubscription() {
19
+ const queryKey = useAuthQueryKey([...SUBSCRIPTIONS_QUERY_KEY, 'current']);
20
+
21
+ return useQuery({
22
+ queryKey,
23
+ queryFn: async ({ signal }): Promise<PaginatedSuccessResult<Subscription>> => {
24
+ const result = await apiFetch<SuccessResult<Subscription | null>>(
25
+ '/api/subscriptions/current',
26
+ { signal },
27
+ );
28
+ const subscription = result.data ?? null;
29
+
30
+ return {
31
+ success: true,
32
+ data: subscription ? [subscription] : [],
33
+ total: subscription ? 1 : 0,
34
+ };
35
+ },
36
+ staleTime: 10_000,
37
+ });
38
+ }
@@ -86,6 +86,40 @@ class SubscriptionsService {
86
86
  } satisfies PaginatedSuccessResult<Subscription>;
87
87
  }
88
88
 
89
+ /**
90
+ * A assinatura corrente do plano da conta, pela regra única do backend
91
+ * (`GET .../subscriptions/action/current`): contrato de domínio nunca é
92
+ * corrente, encerrada é decidida por `active`. Sem `id_product`, paridade
93
+ * com hoje (melhor rank entre todos os produtos, nunca item/domínio).
94
+ */
95
+ async getCurrentSubscription(
96
+ params?: { id_product?: number },
97
+ ): Promise<SuccessResult<Subscription | null>> {
98
+ const { id_account } = await getUserContext();
99
+
100
+ const query = buildQueryParams(
101
+ params as Record<string, string | number | undefined>,
102
+ );
103
+ const url = `/accounts/${id_account}/subscriptions/action/current${query ? `?${query}` : ""}`;
104
+
105
+ const response = await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);
106
+
107
+ if (response.status === 0) {
108
+ throw new ApiError(
109
+ response.message || "Erro ao consultar assinatura corrente",
110
+ "GET_CURRENT_SUBSCRIPTION_FAILED",
111
+ 400,
112
+ );
113
+ }
114
+
115
+ const raw = response.data[0];
116
+
117
+ return {
118
+ success: true,
119
+ data: raw ? SubscriptionSchema.parse(raw) : null,
120
+ };
121
+ }
122
+
89
123
  async calculateSubscription(
90
124
  data: CalculateSubscriptionRequest,
91
125
  ): Promise<SuccessResult<CalculateSubscriptionResponse>> {
@@ -142,4 +142,6 @@ export interface FindSubscriptionsParams {
142
142
  date_hiring_start?: string;
143
143
  date_hiring_end?: string;
144
144
  limit?: number;
145
+ page?: number;
146
+ id_product?: number;
145
147
  }
@@ -0,0 +1,23 @@
1
+ import type { Subscription } from '../types/subscription.type';
2
+
3
+ /**
4
+ * Agrupa assinaturas por `id_product`, preservando a ordem de chegada dentro
5
+ * de cada grupo. Base da seção "Domínios registrados" do painel e da aba
6
+ * Assinatura agrupada por produto no admin.
7
+ */
8
+ export function groupSubscriptionsByProduct(
9
+ subscriptions: Subscription[],
10
+ ): Map<number, Subscription[]> {
11
+ const groups = new Map<number, Subscription[]>();
12
+
13
+ for (const subscription of subscriptions) {
14
+ const group = groups.get(subscription.id_product);
15
+ if (group) {
16
+ group.push(subscription);
17
+ } else {
18
+ groups.set(subscription.id_product, [subscription]);
19
+ }
20
+ }
21
+
22
+ return groups;
23
+ }
@@ -1,9 +1,8 @@
1
1
  import { toCalendarDate, daysBetween } from "../../../infra/utils/date";
2
2
  import { getSubscriptionCancellationState } from "./get-subscription-cancellation-state";
3
+ import { FREE_PLAN_ID } from "../constants/subscription.constants";
3
4
  import type { Subscription } from "../types/subscription.type";
4
5
 
5
- const FREE_PLAN_ID = 5;
6
-
7
6
  type SubscriptionSnapshot = Pick<
8
7
  Subscription,
9
8
  | "type"
@@ -1,6 +1,5 @@
1
1
  import type { Subscription } from '../types/subscription.type';
2
-
3
- const FREE_PLAN_ID = 5;
2
+ import { FREE_PLAN_ID } from '../constants/subscription.constants';
4
3
 
5
4
  type PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan' | 'charged'>;
6
5
 
@@ -0,0 +1,34 @@
1
+ import "server-only";
2
+
3
+ import { cache } from "react";
4
+
5
+ import { ApiError } from "../../../infra/api/types";
6
+ import { getCurrentSubscriptionAction } from "../actions/get-current-subscription.action";
7
+ import type { Subscription } from "../types/subscription.type";
8
+
9
+ /**
10
+ * Leitura única por request de "qual é a assinatura corrente do plano desta
11
+ * conta", deduplicada via React.cache. Os quatro gates de auth
12
+ * (`assertAccountNotFrozen`, `assertManagementSubscription`,
13
+ * `assertPaidSubscription`, `assertSubscriptionAddon`) chamam esta função em
14
+ * vez de repetir a própria leitura cada um: eram até 4 cópias da mesma
15
+ * pergunta disputando a mesma fila (0,7 a 7s cada, medido em produção).
16
+ *
17
+ * Lança em falha de leitura; `assertAccountNotFrozen` é o único chamador que
18
+ * precisa de fail-open, e faz isso capturando o erro no próprio call site.
19
+ */
20
+ export const readCurrentSubscription = cache(
21
+ async (): Promise<Subscription | null> => {
22
+ const result = await getCurrentSubscriptionAction();
23
+
24
+ if (!result.success) {
25
+ throw new ApiError(
26
+ result.error || "Não foi possível validar a assinatura atual",
27
+ "SUBSCRIPTION_CONTEXT_FAILED",
28
+ 403,
29
+ );
30
+ }
31
+
32
+ return result.data?.data ?? null;
33
+ },
34
+ );
@@ -6,14 +6,7 @@ import { whitelabelService } from "../services/whitelabel.service";
6
6
  import { ApiError } from "../../../infra/api/types";
7
7
  import { cache } from "react";
8
8
 
9
- export const findWhitelabel = cache(async () => {
10
- const headersList = await headers();
11
- const host = headersList.get('host');
12
-
13
- if (!host) {
14
- throw new ApiError('Host header not found', 'HOST_NOT_FOUND', 400);
15
- }
16
-
9
+ const findWhitelabelByHost = cache(async (host: string) => {
17
10
  const whitelabel = await whitelabelService.getTokenByDomain(host);
18
11
 
19
12
  if (!whitelabel) {
@@ -21,4 +14,15 @@ export const findWhitelabel = cache(async () => {
21
14
  }
22
15
 
23
16
  return whitelabel;
24
- });
17
+ });
18
+
19
+ export const findWhitelabel = async () => {
20
+ const headersList = await headers();
21
+ const host = headersList.get('host');
22
+
23
+ if (!host) {
24
+ throw new ApiError('Host header not found', 'HOST_NOT_FOUND', 400);
25
+ }
26
+
27
+ return findWhitelabelByHost(host);
28
+ };
@@ -92,17 +92,49 @@ class WhitelabelService {
92
92
  });
93
93
 
94
94
  if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
95
- console.log("[WhitelabelService] Cache hit for domain", { hostname });
96
- return JSON.parse(cachedData.data) as WhitelabelTokenData;
95
+ const cachedWhitelabel = JSON.parse(cachedData.data) as WhitelabelTokenData;
96
+
97
+ if (this.#matchesHostname(cachedWhitelabel, hostname)) {
98
+ console.log("[WhitelabelService] Cache hit for domain", { hostname });
99
+ return cachedWhitelabel;
100
+ }
101
+
102
+ console.error("[WhitelabelService] Cached whitelabel does not match hostname", {
103
+ hostname,
104
+ cacheKey,
105
+ whitelabelId: cachedWhitelabel.id,
106
+ whitelabelDomain: cachedWhitelabel.domain,
107
+ });
108
+
109
+ await cache.delete(cacheKey);
97
110
  }
98
111
 
99
112
  const data = await this.fetchFromApi(hostname);
100
113
 
114
+ if (!this.#matchesHostname(data, hostname)) {
115
+ console.error("[WhitelabelService] API returned whitelabel of another hostname", {
116
+ hostname,
117
+ whitelabelId: data.id,
118
+ whitelabelDomain: data.domain,
119
+ });
120
+ return data;
121
+ }
122
+
101
123
  await cache.insert(cacheKey, JSON.stringify(data), 604800);
102
124
 
103
125
  return data;
104
126
  }
105
127
 
128
+ #matchesHostname(data: WhitelabelTokenData, hostname: string): boolean {
129
+ if (!data.domain) return true;
130
+
131
+ try {
132
+ return normalizeHostname(new URL(data.domain).hostname) === hostname;
133
+ } catch {
134
+ return true;
135
+ }
136
+ }
137
+
106
138
  async getTokenByWhitelabelId(idWl: number): Promise<string> {
107
139
  const apiUrl = this.getApiUrl();
108
140
  const masterToken = this.getToken();
@@ -46,6 +46,7 @@ import { useWhitelabelUrls, useWhitelabel } from "./whitelabel.provider";
46
46
  import { useWlQueryKey } from "../hooks/useAuthQueryKey";
47
47
  import { normalizeLocale } from "../i18n/normalize";
48
48
  import { setLocaleCookie } from "../utils/intl/locale-cookie";
49
+ import { isSameSiteUrl } from "../utils/redirect";
49
50
 
50
51
  interface AuthContextProps extends AuthState {
51
52
  login: (credentials: LoginRequest) => Promise<LoginResult>;
@@ -200,19 +201,21 @@ export function AuthProvider({ children, publicRoutes = [] }: AuthProviderProps)
200
201
  );
201
202
 
202
203
  const resolveLogoutRedirect = useCallback(() => {
203
- try {
204
- const currentOrigin = window.location.origin;
205
- const pagesOrigin = pagesUrl ? new URL(pagesUrl).origin : "";
204
+ const host = window.location.host;
206
205
 
207
- if (pagesOrigin && currentOrigin === pagesOrigin) {
208
- return `${accountsUrl}/login?redirect=${encodeURIComponent(pagesUrl)}`;
209
- }
210
- } catch {
211
- // fallback below
206
+ if (accountsUrl && !isSameSiteUrl(accountsUrl, host)) {
207
+ console.error("[auth-provider] accounts_url fora do site do host atual", {
208
+ host,
209
+ whitelabelId: whitelabel?.id,
210
+ accountsUrl,
211
+ });
212
+ return "/login";
212
213
  }
213
214
 
214
- return `${accountsUrl}/login?redirect=${encodeURIComponent(pagesUrl || "/")}`;
215
- }, [accountsUrl, pagesUrl]);
215
+ const safePagesUrl = pagesUrl && isSameSiteUrl(pagesUrl, host) ? pagesUrl : "/";
216
+
217
+ return `${accountsUrl}/login?redirect=${encodeURIComponent(safePagesUrl)}`;
218
+ }, [accountsUrl, pagesUrl, whitelabel?.id]);
216
219
 
217
220
  const logout = useCallback(async (): Promise<void> => {
218
221
  try {
package/src/server.ts CHANGED
@@ -28,6 +28,8 @@ export { findUserById } from './modules/users/action/find-user-by-id.action';
28
28
  export { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';
29
29
  export { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';
30
30
  export { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';
31
+ export { getCurrentSubscriptionAction } from './modules/subscriptions/actions/get-current-subscription.action';
32
+ export { readCurrentSubscription } from './modules/subscriptions/utils/read-current-subscription';
31
33
  export { ackFreezeNoticeAction } from './modules/subscriptions/actions/ack-freeze-notice.action';
32
34
  export { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';
33
35
  export { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';
@@ -1,21 +1,21 @@
1
1
  import { create } from "zustand";
2
+ import { useEffect } from "react";
2
3
  import { readStorage, writeStorage } from "../utils/browser/safe-storage";
3
4
 
4
5
  const STORAGE_KEY = "md-sidebar-expanded";
5
6
 
6
- function getInitialState(): boolean {
7
- return readStorage(STORAGE_KEY) === "true";
8
- }
9
-
10
7
  type MdSidebarStore = {
11
8
  isExpanded: boolean;
9
+ _hydrated: boolean;
12
10
  toggle: () => void;
13
11
  expand: () => void;
14
12
  collapse: () => void;
13
+ _hydrate: () => void;
15
14
  };
16
15
 
17
16
  export const useMdSidebarStore = create<MdSidebarStore>((set) => ({
18
- isExpanded: getInitialState(),
17
+ isExpanded: false,
18
+ _hydrated: false,
19
19
  toggle: () => set((state) => {
20
20
  const next = !state.isExpanded;
21
21
  writeStorage(STORAGE_KEY, String(next));
@@ -29,4 +29,19 @@ export const useMdSidebarStore = create<MdSidebarStore>((set) => ({
29
29
  writeStorage(STORAGE_KEY, "false");
30
30
  return set({ isExpanded: false });
31
31
  },
32
+ _hydrate: () => {
33
+ const value = readStorage(STORAGE_KEY) === "true";
34
+ set({ isExpanded: value, _hydrated: true });
35
+ },
32
36
  }));
37
+
38
+ export function useMdSidebarHydration() {
39
+ const hydrate = useMdSidebarStore((s) => s._hydrate);
40
+ const hydrated = useMdSidebarStore((s) => s._hydrated);
41
+
42
+ useEffect(() => {
43
+ if (!hydrated) {
44
+ hydrate();
45
+ }
46
+ }, [hydrate, hydrated]);
47
+ }
@@ -36,6 +36,16 @@ export function resolveSafeRedirect(
36
36
  return url.toString();
37
37
  }
38
38
 
39
+ export function isSameSiteUrl(target: string, currentHost: string): boolean {
40
+ let hostname: string;
41
+ try {
42
+ hostname = new URL(target).hostname;
43
+ } catch {
44
+ return false;
45
+ }
46
+ return isSameSite(hostname, currentHost.split(':')[0]);
47
+ }
48
+
39
49
  function isSameSite(target: string, current: string): boolean {
40
50
  const host = target.toLowerCase();
41
51
  const base = baseDomain(current.toLowerCase());