@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.
- package/dist/components/layouts/AppMobileNavBar.mjs +64 -5
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/layouts/AppNavBar.mjs +2 -0
- package/dist/components/layouts/AppNavBar.mjs.map +1 -1
- package/dist/components/layouts/GreatDomainIcon.mjs +40 -0
- package/dist/components/layouts/GreatDomainIcon.mjs.map +1 -0
- package/dist/components/layouts/NavBar.mjs +111 -52
- package/dist/components/layouts/NavBar.mjs.map +1 -1
- package/dist/components/ui/form/PhoneInput.mjs +1 -1
- package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
- package/dist/handlers.mjs +2 -0
- package/dist/handlers.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +3 -1
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +3 -1
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +3 -1
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +11 -2
- package/dist/index.mjs.map +1 -1
- package/dist/infra/api/client.mjs +5 -1
- package/dist/infra/api/client.mjs.map +1 -1
- package/dist/infra/api/types.mjs +1 -1
- package/dist/infra/api/types.mjs.map +1 -1
- package/dist/infra/route/safe-route-handler.mjs +4 -1
- package/dist/infra/route/safe-route-handler.mjs.map +1 -1
- package/dist/middlewares/create-auth-middleware.mjs +11 -1
- package/dist/middlewares/create-auth-middleware.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs +6 -5
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-subscription.mjs +7 -14
- package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-paid-subscription.mjs +7 -12
- package/dist/modules/auth/utils/assert-paid-subscription.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-subscription-addon.mjs +2 -10
- package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
- package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs +10 -0
- package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs.map +1 -0
- package/dist/modules/subscriptions/constants/subscription.constants.mjs +2 -0
- package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
- package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs +17 -0
- package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs.map +1 -0
- package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs +2 -13
- package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs +28 -0
- package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs.map +1 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs +26 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
- package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs +16 -0
- package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs.map +1 -0
- package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs +1 -1
- package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/read-current-subscription.mjs +21 -0
- package/dist/modules/subscriptions/utils/read-current-subscription.mjs.map +1 -0
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +9 -6
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +28 -2
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/providers/auth.provider.mjs +12 -9
- package/dist/providers/auth.provider.mjs.map +1 -1
- package/dist/server.mjs +4 -0
- package/dist/server.mjs.map +1 -1
- package/dist/store/useMdSidebarStore.mjs +17 -4
- package/dist/store/useMdSidebarStore.mjs.map +1 -1
- package/dist/utils/redirect.mjs +10 -0
- package/dist/utils/redirect.mjs.map +1 -1
- package/dist/utils/safeServerAction.mjs +4 -1
- package/dist/utils/safeServerAction.mjs.map +1 -1
- package/dist/utils/withAction.mjs +2 -1
- package/dist/utils/withAction.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/layouts/AppMobileNavBar.tsx +90 -6
- package/src/components/layouts/AppNavBar.tsx +5 -1
- package/src/components/layouts/GreatDomainIcon.tsx +43 -0
- package/src/components/layouts/NavBar.tsx +160 -66
- package/src/components/ui/form/PhoneInput.tsx +1 -1
- package/src/handlers.ts +1 -0
- package/src/i18n/messages/en-us.ts +2 -0
- package/src/i18n/messages/es-es.ts +2 -0
- package/src/i18n/messages/pt-br.ts +2 -0
- package/src/index.ts +8 -2
- package/src/infra/api/client.ts +11 -3
- package/src/infra/api/types.ts +7 -5
- package/src/infra/route/safe-route-handler.ts +4 -1
- package/src/middlewares/create-auth-middleware.ts +12 -1
- package/src/modules/auth/utils/assert-account-not-frozen.ts +7 -6
- package/src/modules/auth/utils/assert-management-subscription.ts +15 -23
- package/src/modules/auth/utils/assert-paid-subscription.ts +7 -15
- package/src/modules/auth/utils/assert-subscription-addon.ts +2 -12
- package/src/modules/plans/hooks/use-plan-by-id.hook.ts +1 -1
- package/src/modules/subscriptions/actions/get-current-subscription.action.ts +8 -0
- package/src/modules/subscriptions/constants/subscription.constants.ts +3 -0
- package/src/modules/subscriptions/handlers/current-subscription.handler.ts +16 -0
- package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +7 -17
- package/src/modules/subscriptions/hooks/use-current-subscription.hook.ts +38 -0
- package/src/modules/subscriptions/services/subscriptions.service.ts +34 -0
- package/src/modules/subscriptions/types/subscription.type.ts +2 -0
- package/src/modules/subscriptions/utils/group-subscriptions-by-product.ts +23 -0
- package/src/modules/subscriptions/utils/has-active-paid-subscription.ts +1 -2
- package/src/modules/subscriptions/utils/has-paid-subscription.ts +1 -2
- package/src/modules/subscriptions/utils/read-current-subscription.ts +34 -0
- package/src/modules/whitelabel/actions/find-whitelabel.action.ts +13 -9
- package/src/modules/whitelabel/services/whitelabel.service.ts +34 -2
- package/src/providers/auth.provider.tsx +13 -10
- package/src/server.ts +2 -0
- package/src/store/useMdSidebarStore.ts +20 -5
- package/src/utils/redirect.ts +10 -0
- package/src/utils/safeServerAction.ts +4 -6
- 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 =
|
|
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 {
|
|
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
|
-
|
|
9
|
+
let subscription: Subscription | null;
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
subscription = await readCurrentSubscription();
|
|
13
|
+
} catch (error) {
|
|
11
14
|
console.error(
|
|
12
15
|
"[assertAccountNotFrozen] failed to read subscription context:",
|
|
13
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
19
|
-
const
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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
|
-
|
|
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>> {
|
|
@@ -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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
204
|
-
const currentOrigin = window.location.origin;
|
|
205
|
-
const pagesOrigin = pagesUrl ? new URL(pagesUrl).origin : "";
|
|
204
|
+
const host = window.location.host;
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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:
|
|
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
|
+
}
|
package/src/utils/redirect.ts
CHANGED
|
@@ -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());
|