@greatapps/common 1.1.773 → 1.1.775
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/account/sections/SecuritySection.mjs +4 -2
- package/dist/components/account/sections/SecuritySection.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-permission.mjs +3 -2
- package/dist/modules/auth/utils/assert-management-permission.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-subscription.mjs +3 -2
- package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
- package/package.json +8 -7
- package/src/components/account/sections/SecuritySection.tsx +5 -3
- package/src/modules/auth/utils/assert-management-permission.ts +14 -2
- package/src/modules/auth/utils/assert-management-subscription.ts +13 -2
|
@@ -7,6 +7,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
7
7
|
import { useAuth } from "../../../providers/auth.provider";
|
|
8
8
|
import { useUnlinkGoogle } from "../../../modules/auth/hooks/unlink-google.hook";
|
|
9
9
|
import { USER_QUERY_KEY } from "../../../modules/auth/hooks/useUserQuery";
|
|
10
|
+
import { Button } from "../../ui/buttons/Button";
|
|
10
11
|
import { Toast } from "../../ui/feedback/Toast";
|
|
11
12
|
import { GoogleIcon } from "../../ui/data-display/GoogleIcon";
|
|
12
13
|
function SecuritySection({
|
|
@@ -36,11 +37,12 @@ function SecuritySection({
|
|
|
36
37
|
/* @__PURE__ */ jsx("span", { className: "paragraph-small-regular text-gray-600", children: translate("common.account.security.googleLinkedDescription") })
|
|
37
38
|
] }),
|
|
38
39
|
/* @__PURE__ */ jsx(
|
|
39
|
-
|
|
40
|
+
Button,
|
|
40
41
|
{
|
|
42
|
+
variant: "secondary",
|
|
43
|
+
className: "h-10! w-fit",
|
|
41
44
|
type: "button",
|
|
42
45
|
disabled: isUnlinking,
|
|
43
|
-
className: "paragraph-small-semibold text-zinc-700 bg-zinc-100 rounded-lg px-3 py-2 cursor-pointer hover:bg-zinc-200 transition-colors w-fit disabled:opacity-50 disabled:cursor-not-allowed",
|
|
44
46
|
onClick: handleUnlinkGoogle,
|
|
45
47
|
children: isUnlinking ? translate("common.account.security.unlinking") : translate("common.account.security.unlinkGoogle")
|
|
46
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/account/sections/SecuritySection.tsx"],"sourcesContent":["'use client';\n\nimport { IconAlertTriangle } from '@tabler/icons-react';\nimport { useTranslations } from 'next-intl';\nimport { toast } from 'sonner';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useAuth } from '../../../providers/auth.provider';\nimport { AccountSectionType } from '../../../enums/AccountSectionType';\nimport { useUnlinkGoogle } from '../../../modules/auth/hooks/unlink-google.hook';\nimport { USER_QUERY_KEY } from '../../../modules/auth/hooks/useUserQuery';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { GoogleIcon } from '../../ui/data-display/GoogleIcon';\n\ninterface SecuritySectionProps {\n setActiveSection: (section: AccountSectionType) => void;\n onClose: () => void;\n onDeleteAccount?: () => void;\n}\n\nexport function SecuritySection({\n onDeleteAccount,\n}: SecuritySectionProps) {\n const translate = useTranslations();\n const { user } = useAuth();\n const queryClient = useQueryClient();\n const { mutateAsync: unlinkGoogle, isPending: isUnlinking } = useUnlinkGoogle();\n const hasGoogleLinked = !!user?.google_sub;\n\n const handleUnlinkGoogle = async () => {\n try {\n const result = await unlinkGoogle();\n toast.custom((t) => (\n <Toast variant=\"success\" message={result.message} toastId={t} />\n ));\n queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : translate('common.account.security.unlinkError');\n toast.custom((toastId) => <Toast variant=\"error\" message={message} toastId={toastId} />);\n }\n };\n\n return (\n <div className=\"h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto\">\n <span className=\"paragraph-medium-semibold text-gray-950 hidden lg:block\">\n {translate('common.account.security.title')}\n </span>\n\n {hasGoogleLinked && (\n <div className=\"flex flex-col items-center lg:items-start gap-4\">\n <div className=\"size-10 rounded-lg bg-gray-50 flex items-center justify-center\">\n <GoogleIcon size={20} />\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <span className=\"paragraph-medium-semibold text-gray-950\">\n {translate('common.account.security.googleLinkedTitle')}\n </span>\n <span className=\"paragraph-small-regular text-gray-600\">\n {translate('common.account.security.googleLinkedDescription')}\n </span>\n </div>\n\n <
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/account/sections/SecuritySection.tsx"],"sourcesContent":["'use client';\n\nimport { IconAlertTriangle } from '@tabler/icons-react';\nimport { useTranslations } from 'next-intl';\nimport { toast } from 'sonner';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useAuth } from '../../../providers/auth.provider';\nimport { AccountSectionType } from '../../../enums/AccountSectionType';\nimport { useUnlinkGoogle } from '../../../modules/auth/hooks/unlink-google.hook';\nimport { USER_QUERY_KEY } from '../../../modules/auth/hooks/useUserQuery';\nimport { Button } from '../../ui/buttons/Button';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { GoogleIcon } from '../../ui/data-display/GoogleIcon';\n\ninterface SecuritySectionProps {\n setActiveSection: (section: AccountSectionType) => void;\n onClose: () => void;\n onDeleteAccount?: () => void;\n}\n\nexport function SecuritySection({\n onDeleteAccount,\n}: SecuritySectionProps) {\n const translate = useTranslations();\n const { user } = useAuth();\n const queryClient = useQueryClient();\n const { mutateAsync: unlinkGoogle, isPending: isUnlinking } = useUnlinkGoogle();\n const hasGoogleLinked = !!user?.google_sub;\n\n const handleUnlinkGoogle = async () => {\n try {\n const result = await unlinkGoogle();\n toast.custom((t) => (\n <Toast variant=\"success\" message={result.message} toastId={t} />\n ));\n queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : translate('common.account.security.unlinkError');\n toast.custom((toastId) => <Toast variant=\"error\" message={message} toastId={toastId} />);\n }\n };\n\n return (\n <div className=\"h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto\">\n <span className=\"paragraph-medium-semibold text-gray-950 hidden lg:block\">\n {translate('common.account.security.title')}\n </span>\n\n {hasGoogleLinked && (\n <div className=\"flex flex-col items-center lg:items-start gap-4\">\n <div className=\"size-10 rounded-lg bg-gray-50 flex items-center justify-center\">\n <GoogleIcon size={20} />\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <span className=\"paragraph-medium-semibold text-gray-950\">\n {translate('common.account.security.googleLinkedTitle')}\n </span>\n <span className=\"paragraph-small-regular text-gray-600\">\n {translate('common.account.security.googleLinkedDescription')}\n </span>\n </div>\n\n <Button\n variant=\"secondary\"\n className=\"h-10! w-fit\"\n type=\"button\"\n disabled={isUnlinking}\n onClick={handleUnlinkGoogle}\n >\n {isUnlinking\n ? translate('common.account.security.unlinking')\n : translate('common.account.security.unlinkGoogle')}\n </Button>\n </div>\n )}\n\n <div className=\"flex flex-col items-center lg:items-start gap-4\">\n <div className=\"size-10 rounded-lg bg-red-50 flex items-center justify-center\">\n <IconAlertTriangle size={20} className=\"text-red-500\" />\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <span className=\"paragraph-medium-semibold text-gray-950\">\n {translate('common.account.deleteAccount.title')}\n </span>\n <span className=\"paragraph-small-regular text-gray-600\">\n {translate('common.account.security.deleteAccountDescription')}\n </span>\n </div>\n\n {onDeleteAccount && (\n <button\n type=\"button\"\n className=\"paragraph-small-semibold text-red-600 bg-red-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-red-100 transition-colors w-fit\"\n onClick={onDeleteAccount}\n >\n {translate('common.account.security.deleteMyAccount')}\n </button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAiCQ,cAsBE,YAtBF;AA/BR,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,aAAa;AACtB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAExB,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAQpB,SAAS,gBAAgB;AAAA,EAC9B;AACF,GAAyB;AACvB,QAAM,YAAY,gBAAgB;AAClC,QAAM,EAAE,KAAK,IAAI,QAAQ;AACzB,QAAM,cAAc,eAAe;AACnC,QAAM,EAAE,aAAa,cAAc,WAAW,YAAY,IAAI,gBAAgB;AAC9E,QAAM,kBAAkB,CAAC,CAAC,MAAM;AAEhC,QAAM,qBAAqB,YAAY;AACrC,QAAI;AACF,YAAM,SAAS,MAAM,aAAa;AAClC,YAAM,OAAO,CAAC,MACZ,oBAAC,SAAM,SAAQ,WAAU,SAAS,OAAO,SAAS,SAAS,GAAG,CAC/D;AACD,kBAAY,kBAAkB,EAAE,UAAU,eAAe,CAAC;AAAA,IAC5D,SAAS,OAAO;AACd,YAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU,UAAU,qCAAqC;AAC1F,YAAM,OAAO,CAAC,YAAY,oBAAC,SAAM,SAAQ,SAAQ,SAAkB,SAAkB,CAAE;AAAA,IACzF;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,gIACb;AAAA,wBAAC,UAAK,WAAU,2DACb,oBAAU,+BAA+B,GAC5C;AAAA,IAEC,mBACC,qBAAC,SAAI,WAAU,mDACb;AAAA,0BAAC,SAAI,WAAU,kEACb,8BAAC,cAAW,MAAM,IAAI,GACxB;AAAA,MAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,UAAK,WAAU,2CACb,oBAAU,2CAA2C,GACxD;AAAA,QACA,oBAAC,UAAK,WAAU,yCACb,oBAAU,iDAAiD,GAC9D;AAAA,SACF;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,WAAU;AAAA,UACV,MAAK;AAAA,UACL,UAAU;AAAA,UACV,SAAS;AAAA,UAER,wBACG,UAAU,mCAAmC,IAC7C,UAAU,sCAAsC;AAAA;AAAA,MACtD;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,mDACb;AAAA,0BAAC,SAAI,WAAU,iEACb,8BAAC,qBAAkB,MAAM,IAAI,WAAU,gBAAe,GACxD;AAAA,MAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,UAAK,WAAU,2CACb,oBAAU,oCAAoC,GACjD;AAAA,QACA,oBAAC,UAAK,WAAU,yCACb,oBAAU,kDAAkD,GAC/D;AAAA,SACF;AAAA,MAEC,mBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS;AAAA,UAER,oBAAU,yCAAyC;AAAA;AAAA,MACtD;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
2
3
|
import { UserProfile } from "../../users/schema";
|
|
3
4
|
import { findUserById } from "../../users/action/find-user-by-id.action";
|
|
4
5
|
import { ApiError } from "../../../infra/api/types";
|
|
5
6
|
import { assertManagementSubscription } from "./assert-management-subscription";
|
|
6
|
-
|
|
7
|
+
const getCurrentUserProfile = cache(async () => {
|
|
7
8
|
const result = await findUserById();
|
|
8
9
|
if (!result.success || !result.data) {
|
|
9
10
|
throw new ApiError(
|
|
@@ -13,7 +14,7 @@ async function getCurrentUserProfile() {
|
|
|
13
14
|
);
|
|
14
15
|
}
|
|
15
16
|
return result.data.profile;
|
|
16
|
-
}
|
|
17
|
+
});
|
|
17
18
|
async function assertManagementPermission(permission) {
|
|
18
19
|
const profile = await getCurrentUserProfile();
|
|
19
20
|
switch (permission) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-permission.ts"],"sourcesContent":["import 'server-only';\n\nimport { UserProfile } from '../../users/schema';\nimport { findUserById } from '../../users/action/find-user-by-id.action';\nimport { ApiError } from '../../../infra/api/types';\nimport { assertManagementSubscription } from './assert-management-subscription';\nimport type { ManagementPermission } from '../types/management-permission.type';\n\
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-permission.ts"],"sourcesContent":["import 'server-only';\n\nimport { cache } from 'react';\n\nimport { UserProfile } from '../../users/schema';\nimport { findUserById } from '../../users/action/find-user-by-id.action';\nimport { ApiError } from '../../../infra/api/types';\nimport { assertManagementSubscription } from './assert-management-subscription';\nimport type { ManagementPermission } from '../types/management-permission.type';\n\n/**\n * Perfil do usuário autenticado, deduplicado por request via React.cache — mesma razão\n * do `requireValidSession`.\n *\n * Este gate abre TODA leitura e escrita gateada, e o perfil não depende da permissão que\n * está sendo checada: uma request que toca N services pagava N round-trips ao backend para\n * reler a mesma linha. Medido em produção (26/08, carregamento da aba de assinatura de uma\n * conta): ~30 GETs de `/accounts/{id}/users/{id}` em 20 s, 200–880 ms cada, competindo pelo\n * mesmo worker de banco que as chamadas de verdade.\n */\nconst getCurrentUserProfile = cache(async () => {\n const result = await findUserById();\n\n if (!result.success || !result.data) {\n throw new ApiError(\n ('error' in result && result.error) || 'Não foi possível identificar o usuário autenticado',\n 'USER_CONTEXT_FAILED',\n 403\n );\n }\n\n return result.data.profile;\n});\n\nexport async function assertManagementPermission(permission: ManagementPermission) {\n const profile = await getCurrentUserProfile();\n\n switch (permission) {\n case 'manage_pages':\n case 'manage_redirects':\n case 'manage_domains':\n case 'manage_ab_tests':\n case 'restore_pages':\n case 'create_pages':\n case 'update_pages':\n case 'delete_pages':\n if (profile === UserProfile.viewer) {\n throw new ApiError('Você não tem permissão para executar esta ação', 'FORBIDDEN', 403);\n }\n break;\n case 'manage_projects':\n case 'delete_leads':\n case 'view_cards':\n case 'manage_cards':\n case 'manage_subscriptions':\n if (profile !== UserProfile.owner && profile !== UserProfile.admin) {\n throw new ApiError('Você não tem permissão para executar esta ação', 'FORBIDDEN', 403);\n }\n break;\n default:\n throw new ApiError('Permissão inválida', 'INVALID_PERMISSION', 500);\n }\n\n const skipSubscriptionCheck: ManagementPermission[] = ['manage_subscriptions', 'manage_cards', 'view_cards'];\n if (!skipSubscriptionCheck.includes(permission)) {\n await assertManagementSubscription();\n }\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,aAAa;AAEtB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,SAAS,oCAAoC;AAa7C,MAAM,wBAAwB,MAAM,YAAY;AAC9C,QAAM,SAAS,MAAM,aAAa;AAElC,MAAI,CAAC,OAAO,WAAW,CAAC,OAAO,MAAM;AACnC,UAAM,IAAI;AAAA,MACP,WAAW,UAAU,OAAO,SAAU;AAAA,MACvC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,OAAO,KAAK;AACrB,CAAC;AAED,eAAsB,2BAA2B,YAAkC;AACjF,QAAM,UAAU,MAAM,sBAAsB;AAE5C,UAAQ,YAAY;AAAA,IAClB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,YAAY,YAAY,QAAQ;AAClC,cAAM,IAAI,SAAS,iEAAkD,aAAa,GAAG;AAAA,MACvF;AACA;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,YAAY,YAAY,SAAS,YAAY,YAAY,OAAO;AAClE,cAAM,IAAI,SAAS,iEAAkD,aAAa,GAAG;AAAA,MACvF;AACA;AAAA,IACF;AACE,YAAM,IAAI,SAAS,4BAAsB,sBAAsB,GAAG;AAAA,EACtE;AAEA,QAAM,wBAAgD,CAAC,wBAAwB,gBAAgB,YAAY;AAC3G,MAAI,CAAC,sBAAsB,SAAS,UAAU,GAAG;AAC/C,UAAM,6BAA6B;AAAA,EACrC;AACF;","names":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
2
3
|
import { ApiError } from "../../../infra/api/types";
|
|
3
4
|
import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
|
|
4
5
|
import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
|
|
5
|
-
|
|
6
|
+
const assertManagementSubscription = cache(async () => {
|
|
6
7
|
const result = await listSubscriptionsAction({ active: true });
|
|
7
8
|
if (!result.success) {
|
|
8
9
|
throw new ApiError(
|
|
@@ -28,7 +29,7 @@ async function assertManagementSubscription() {
|
|
|
28
29
|
403
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
|
-
}
|
|
32
|
+
});
|
|
32
33
|
export {
|
|
33
34
|
assertManagementSubscription
|
|
34
35
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-subscription.ts"],"sourcesContent":["import \"server-only\";\n\nimport { ApiError } from \"../../../infra/api/types\";\nimport { listSubscriptionsAction } from \"../../subscriptions/actions/list-subscriptions.action\";\nimport { getSubscriptionExpiryDate } from \"../../subscriptions/utils/get-subscription-expiry-date\";\n\nexport
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-subscription.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\n\nimport { ApiError } from \"../../../infra/api/types\";\nimport { listSubscriptionsAction } from \"../../subscriptions/actions/list-subscriptions.action\";\nimport { getSubscriptionExpiryDate } from \"../../subscriptions/utils/get-subscription-expiry-date\";\n\n/**\n * Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.\n *\n * A leitura por trás é `/accounts/{id}/subscriptions?limit=1`, e ela é MAIS cara que o gate de\n * perfil: para conta Stripe a listagem consulta a API do gateway ao vivo. Em produção (26/08) o\n * mesmo carregamento disparou quatro dessas em paralelo — 727 ms, 4,2 s, 5,0 s e 7,0 s, contra um\n * p50 de 528 ms para o endpoint. Elas se atrasam entre si: são cópias da mesma pergunta disputando\n * a mesma fila.\n */\nexport const assertManagementSubscription = cache(async () => {\n const result = await listSubscriptionsAction({ active: true });\n\n if (!result.success) {\n throw new ApiError(\n result.error || \"Não foi possível validar a assinatura atual\",\n \"SUBSCRIPTION_CONTEXT_FAILED\",\n 403,\n );\n }\n\n const subscription = result.data?.data?.[0];\n\n if (!subscription) {\n throw new ApiError(\n \"Nenhuma assinatura ativa encontrada. Assine um plano para continuar.\",\n \"SUBSCRIPTION_NOT_FOUND\",\n 403,\n );\n }\n\n const expiryDate = getSubscriptionExpiryDate(subscription);\n const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();\n\n if (isExpired) {\n throw new ApiError(\n \"Sua assinatura está vencida. Assine um novo plano para continuar.\",\n \"SUBSCRIPTION_EXPIRED\",\n 403,\n );\n }\n});\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,aAAa;AAEtB,SAAS,gBAAgB;AACzB,SAAS,+BAA+B;AACxC,SAAS,iCAAiC;AAWnC,MAAM,+BAA+B,MAAM,YAAY;AAC5D,QAAM,SAAS,MAAM,wBAAwB,EAAE,QAAQ,KAAK,CAAC;AAE7D,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI;AAAA,MACR,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,OAAO,MAAM,OAAO,CAAC;AAE1C,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,0BAA0B,YAAY;AACzD,QAAM,YAAY,CAAC,CAAC,cAAc,KAAK,IAAI,KAAK,WAAW,QAAQ;AAEnE,MAAI,WAAW;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greatapps/common",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.775",
|
|
4
4
|
"description": "Shared library for GreatApps frontend applications",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
"dist",
|
|
47
47
|
"src"
|
|
48
48
|
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"prepublishOnly": "npm run build"
|
|
54
|
+
},
|
|
49
55
|
"keywords": [],
|
|
50
56
|
"author": "",
|
|
51
57
|
"license": "ISC",
|
|
@@ -131,10 +137,5 @@
|
|
|
131
137
|
"@faker-js/faker": {
|
|
132
138
|
"optional": true
|
|
133
139
|
}
|
|
134
|
-
},
|
|
135
|
-
"scripts": {
|
|
136
|
-
"build": "tsup",
|
|
137
|
-
"dev": "tsup --watch",
|
|
138
|
-
"typecheck": "tsc --noEmit"
|
|
139
140
|
}
|
|
140
|
-
}
|
|
141
|
+
}
|
|
@@ -8,6 +8,7 @@ import { useAuth } from '../../../providers/auth.provider';
|
|
|
8
8
|
import { AccountSectionType } from '../../../enums/AccountSectionType';
|
|
9
9
|
import { useUnlinkGoogle } from '../../../modules/auth/hooks/unlink-google.hook';
|
|
10
10
|
import { USER_QUERY_KEY } from '../../../modules/auth/hooks/useUserQuery';
|
|
11
|
+
import { Button } from '../../ui/buttons/Button';
|
|
11
12
|
import { Toast } from '../../ui/feedback/Toast';
|
|
12
13
|
import { GoogleIcon } from '../../ui/data-display/GoogleIcon';
|
|
13
14
|
|
|
@@ -61,16 +62,17 @@ export function SecuritySection({
|
|
|
61
62
|
</span>
|
|
62
63
|
</div>
|
|
63
64
|
|
|
64
|
-
<
|
|
65
|
+
<Button
|
|
66
|
+
variant="secondary"
|
|
67
|
+
className="h-10! w-fit"
|
|
65
68
|
type="button"
|
|
66
69
|
disabled={isUnlinking}
|
|
67
|
-
className="paragraph-small-semibold text-zinc-700 bg-zinc-100 rounded-lg px-3 py-2 cursor-pointer hover:bg-zinc-200 transition-colors w-fit disabled:opacity-50 disabled:cursor-not-allowed"
|
|
68
70
|
onClick={handleUnlinkGoogle}
|
|
69
71
|
>
|
|
70
72
|
{isUnlinking
|
|
71
73
|
? translate('common.account.security.unlinking')
|
|
72
74
|
: translate('common.account.security.unlinkGoogle')}
|
|
73
|
-
</
|
|
75
|
+
</Button>
|
|
74
76
|
</div>
|
|
75
77
|
)}
|
|
76
78
|
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
+
import { cache } from 'react';
|
|
4
|
+
|
|
3
5
|
import { UserProfile } from '../../users/schema';
|
|
4
6
|
import { findUserById } from '../../users/action/find-user-by-id.action';
|
|
5
7
|
import { ApiError } from '../../../infra/api/types';
|
|
6
8
|
import { assertManagementSubscription } from './assert-management-subscription';
|
|
7
9
|
import type { ManagementPermission } from '../types/management-permission.type';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Perfil do usuário autenticado, deduplicado por request via React.cache — mesma razão
|
|
13
|
+
* do `requireValidSession`.
|
|
14
|
+
*
|
|
15
|
+
* Este gate abre TODA leitura e escrita gateada, e o perfil não depende da permissão que
|
|
16
|
+
* está sendo checada: uma request que toca N services pagava N round-trips ao backend para
|
|
17
|
+
* reler a mesma linha. Medido em produção (26/08, carregamento da aba de assinatura de uma
|
|
18
|
+
* conta): ~30 GETs de `/accounts/{id}/users/{id}` em 20 s, 200–880 ms cada, competindo pelo
|
|
19
|
+
* mesmo worker de banco que as chamadas de verdade.
|
|
20
|
+
*/
|
|
21
|
+
const getCurrentUserProfile = cache(async () => {
|
|
10
22
|
const result = await findUserById();
|
|
11
23
|
|
|
12
24
|
if (!result.success || !result.data) {
|
|
@@ -18,7 +30,7 @@ async function getCurrentUserProfile() {
|
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
return result.data.profile;
|
|
21
|
-
}
|
|
33
|
+
});
|
|
22
34
|
|
|
23
35
|
export async function assertManagementPermission(permission: ManagementPermission) {
|
|
24
36
|
const profile = await getCurrentUserProfile();
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
|
|
3
|
+
import { cache } from "react";
|
|
4
|
+
|
|
3
5
|
import { ApiError } from "../../../infra/api/types";
|
|
4
6
|
import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
|
|
5
7
|
import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.
|
|
11
|
+
*
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
export const assertManagementSubscription = cache(async () => {
|
|
8
19
|
const result = await listSubscriptionsAction({ active: true });
|
|
9
20
|
|
|
10
21
|
if (!result.success) {
|
|
@@ -35,4 +46,4 @@ export async function assertManagementSubscription() {
|
|
|
35
46
|
403,
|
|
36
47
|
);
|
|
37
48
|
}
|
|
38
|
-
}
|
|
49
|
+
});
|