@seedgrid/fe-subdomain-tenancy 2026.7.0

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 (40) hide show
  1. package/README.md +75 -0
  2. package/dist/client/index.d.ts +2 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +3 -0
  5. package/dist/client/tenant-provider.d.ts +11 -0
  6. package/dist/client/tenant-provider.d.ts.map +1 -0
  7. package/dist/client/tenant-provider.js +19 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +5 -0
  11. package/dist/module.d.ts +3 -0
  12. package/dist/module.d.ts.map +1 -0
  13. package/dist/module.js +16 -0
  14. package/dist/module.json +14 -0
  15. package/dist/paths.d.ts +9 -0
  16. package/dist/paths.d.ts.map +1 -0
  17. package/dist/paths.js +10 -0
  18. package/dist/server/create-subdomain-tenancy-server.d.ts +12 -0
  19. package/dist/server/create-subdomain-tenancy-server.d.ts.map +1 -0
  20. package/dist/server/create-subdomain-tenancy-server.js +18 -0
  21. package/dist/server/index.d.ts +2 -0
  22. package/dist/server/index.d.ts.map +1 -0
  23. package/dist/server/index.js +2 -0
  24. package/dist/subdomain-rules.d.ts +13 -0
  25. package/dist/subdomain-rules.d.ts.map +1 -0
  26. package/dist/subdomain-rules.js +82 -0
  27. package/dist/tenant-signup.d.ts +59 -0
  28. package/dist/tenant-signup.d.ts.map +1 -0
  29. package/dist/tenant-signup.js +5 -0
  30. package/package.json +55 -0
  31. package/scaffold/README.md +70 -0
  32. package/scaffold/src/app/(public)/signup/page.tsx +1165 -0
  33. package/scaffold/src/app/api/locations/cities/route.ts +125 -0
  34. package/scaffold/src/app/api/locations/states/route.ts +120 -0
  35. package/scaffold/src/modules/subdomain-tenancy/index.ts +20 -0
  36. package/scaffold/src/modules/subdomain-tenancy/server/create-public-tenant-action.ts +113 -0
  37. package/scaffold/src/modules/subdomain-tenancy/server/public-api-base-url.ts +28 -0
  38. package/scaffold/src/modules/subdomain-tenancy/server/resend-public-tenant-activation-email-action.ts +118 -0
  39. package/scaffold/src/modules/subdomain-tenancy/server/validate-public-tenant-email-action.ts +138 -0
  40. package/scaffold/src/modules/subdomain-tenancy/signup/types.ts +129 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @seedgrid/fe-subdomain-tenancy
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40seedgrid%2Ffe-subdomain-tenancy)](https://www.npmjs.com/package/@seedgrid/fe-subdomain-tenancy)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40seedgrid%2Ffe-subdomain-tenancy)](https://www.npmjs.com/package/@seedgrid/fe-subdomain-tenancy)
5
+ [![TypeScript](https://img.shields.io/badge/types-TypeScript-blue)](https://www.typescriptlang.org/)
6
+
7
+ Módulo front de **multitenancy por subdomínio** do SeedGrid: cada tenant vive no seu subdomínio (`empresa.seudominio.com`), com signup público que cria o tenant + usuário root. É a variante de tenancy por subdomínio — a variante por seleção de empresa no login fica no [`@seedgrid/fe-login-tenancy`](https://www.npmjs.com/package/@seedgrid/fe-login-tenancy) (`variantGroup: "tenancy"`, use uma ou outra). Compõe sobre o motor de sessão do [`@seedgrid/fe-security`](https://www.npmjs.com/package/@seedgrid/fe-security).
8
+
9
+ ## Sumário
10
+
11
+ - [Funcionalidades](#funcionalidades)
12
+ - [Telas entregues pelo scaffold](#telas-entregues-pelo-scaffold)
13
+ - [Instalação](#instalação)
14
+ - [Uso rápido](#uso-rápido)
15
+ - [Backend correspondente](#backend-correspondente)
16
+ - [Suporte](#suporte)
17
+
18
+ ## Funcionalidades
19
+
20
+ - **Signup de tenant** — cria o tenant + usuário root num só fluxo, com validação de e-mail por código e reenvio.
21
+ - **Regras de subdomínio** — helpers puros: `normalizeSubdomain`, `validateSubdomain`, `isReservedSubdomain`, `getSubdomainValidationKey` e a lista `RESERVED_SUBDOMAINS` — a mesma validação do backend, disponível pro feedback em tempo real no formulário.
22
+ - **Contexto de tenant no client** — `TenantProvider` / `useTenantContext` (em `@seedgrid/fe-subdomain-tenancy/client`), registrado automaticamente como provider do app pelo CLI.
23
+
24
+ O pacote exporta:
25
+
26
+ - `SubdomainTenancyPaths` — contrato dos endpoints públicos (`/public/tenants`, `validate-email`, `resend-email`).
27
+ - DTOs client-safe do signup de tenant (`tenant-signup`) — conferidos contra o `TenantSignupRequest.java` real.
28
+ - `subdomain-rules` (validação), `TenantProvider` (client) e `createSubdomainTenancyServer` (em `/server`, SERVER-ONLY, pra chamadas autenticadas pós-login).
29
+
30
+ ## Telas entregues pelo scaffold
31
+
32
+ | Tela | Rota | O que faz |
33
+ | --- | --- | --- |
34
+ | Signup de tenant | `/signup` (público) | Wizard completo de cadastro BR (CNPJ, endereço via ViaCEP, UF/cidade), escolha de subdomínio com validação em tempo real e confirmação de e-mail |
35
+
36
+ O scaffold também traz as rotas de apoio de localização (`/api/locations/states`, `/api/locations/cities`) e as server actions públicas do fluxo de signup (criação do tenant, validação de e-mail, reenvio de ativação) — tudo copiado pelo SeedGrid CLI via `module.json` (`id: "subdomain-tenancy"`, `requires: ["security"]`, `variantGroup: "tenancy"`, provider `TenantProvider` registrado automaticamente).
37
+
38
+ ## Instalação
39
+
40
+ ```bash
41
+ pnpm add @seedgrid/fe-subdomain-tenancy @seedgrid/fe-security @seedgrid/fe-core
42
+ ```
43
+
44
+ ## Uso rápido
45
+
46
+ ```tsx
47
+ // Client: validação de subdomínio em tempo real
48
+ import { normalizeSubdomain, validateSubdomain } from "@seedgrid/fe-subdomain-tenancy";
49
+
50
+ const sub = normalizeSubdomain(input); // "Minha Empresa!" -> "minha-empresa"
51
+ const erro = validateSubdomain(sub); // null se ok; chave de erro se inválido/reservado
52
+ ```
53
+
54
+ ```tsx
55
+ // Client: contexto do tenant corrente
56
+ import { TenantProvider, useTenantContext } from "@seedgrid/fe-subdomain-tenancy/client";
57
+
58
+ function Header() {
59
+ const { tenant } = useTenantContext();
60
+ return <span>{tenant?.name}</span>;
61
+ }
62
+ ```
63
+
64
+ ## Backend correspondente
65
+
66
+ Extensão Quarkus `seedgrid-quarkus-ext-subdomain-tenancy`. `SubdomainTenancyPaths` é espelho do `RestControllerPath.java` da extensão; os DTOs de signup foram conferidos contra o `TenantSignupRequest.java` real.
67
+
68
+ ## Suporte
69
+
70
+ - Issues: https://github.com/SeedGrid/seedgrid-fe-components/issues
71
+ - Repositório: https://github.com/SeedGrid/seedgrid-fe-components
72
+
73
+ ## Licença
74
+
75
+ MIT
@@ -0,0 +1,2 @@
1
+ export { TenantProvider, useTenantContext } from "./tenant-provider";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Barrel CLIENT (@seedgrid/fe-subdomain-tenancy/client). React (hooks client).
2
+ // Não importe daqui em código server-only.
3
+ export { TenantProvider, useTenantContext } from "./tenant-provider";
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from "react";
2
+ type TenantContextValue = {
3
+ currentTenant: string;
4
+ setTenant: (tenant: string) => void;
5
+ };
6
+ export declare function TenantProvider({ children }: {
7
+ children: ReactNode;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function useTenantContext(): TenantContextValue;
10
+ export {};
11
+ //# sourceMappingURL=tenant-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-provider.d.ts","sourceRoot":"","sources":["../../src/client/tenant-provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5E,KAAK,kBAAkB,GAAG;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAIF,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAWnE;AAED,wBAAgB,gBAAgB,uBAQ/B"}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useState } from "react";
4
+ const TenantContext = createContext(null);
5
+ export function TenantProvider({ children }) {
6
+ const [currentTenant, setCurrentTenant] = useState("default");
7
+ const value = {
8
+ currentTenant,
9
+ setTenant: setCurrentTenant,
10
+ };
11
+ return (_jsx(TenantContext.Provider, { value: value, children: children }));
12
+ }
13
+ export function useTenantContext() {
14
+ const context = useContext(TenantContext);
15
+ if (!context) {
16
+ throw new Error("useTenantContext must be used inside TenantProvider");
17
+ }
18
+ return context;
19
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./tenant-signup";
2
+ export * from "./paths";
3
+ export * from "./subdomain-rules";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Barrel client-safe (@seedgrid/fe-subdomain-tenancy). Tipos + paths. Server em /server.
2
+ // A tela de signup do tenant é BR-coupled (CNPJ/ViaCEP/UF) → SCAFFOLD (app owns).
3
+ export * from "./tenant-signup";
4
+ export * from "./paths";
5
+ export * from "./subdomain-rules";
@@ -0,0 +1,3 @@
1
+ import type { SeedGridCliModuleManifest } from "@seedgrid/fe-core";
2
+ export declare const module: SeedGridCliModuleManifest;
3
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAWnE,eAAO,MAAM,MAAM,EAAE,yBAMpB,CAAC"}
package/dist/module.js ADDED
@@ -0,0 +1,16 @@
1
+ // Reference-adjacent implementation of the manifest read by the SeedGrid CLI
2
+ // (Rust). See seedgrid-cli/docs/adr/0004-module-manifest-contract.md.
3
+ //
4
+ // `variantGroup: "tenancy"` — mutually exclusive with `login-tenancy`
5
+ // (chosen once at `seedgrid init`).
6
+ //
7
+ // `TenantProvider` has zero app coupling (pure React context, no `@/...`
8
+ // imports) so it was promoted to lib — unlike fe-security's `AuthProvider`,
9
+ // which hardcodes scaffold routes and had to stay in scaffold.
10
+ export const module = {
11
+ id: "subdomain-tenancy",
12
+ requires: ["security"],
13
+ variantGroup: "tenancy",
14
+ configKey: "subdomain-tenancy",
15
+ provider: [{ import: "TenantProvider", from: "@seedgrid/fe-subdomain-tenancy/client" }],
16
+ };
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "subdomain-tenancy",
3
+ "requires": [
4
+ "security"
5
+ ],
6
+ "variantGroup": "tenancy",
7
+ "configKey": "subdomain-tenancy",
8
+ "provider": [
9
+ {
10
+ "import": "TenantProvider",
11
+ "from": "@seedgrid/fe-subdomain-tenancy/client"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,9 @@
1
+ export declare const SubdomainTenancyPaths: {
2
+ /** POST — cria tenant + usuário root. */
3
+ readonly tenants: "/public/tenants";
4
+ /** POST — valida o e-mail do signup com o código. */
5
+ readonly validateEmail: "/public/tenants/validate-email";
6
+ /** POST — reenvia o e-mail de confirmação (query ?subdomain=). */
7
+ readonly resendEmail: (subdomain: string) => string;
8
+ };
9
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB;IAChC,yCAAyC;;IAEzC,qDAAqD;;IAErD,kEAAkE;sCACzC,MAAM;CAEvB,CAAC"}
package/dist/paths.js ADDED
@@ -0,0 +1,10 @@
1
+ // Paths de subdomain-tenancy (espelho de RestControllerPath.java em
2
+ // seedgrid-quarkus-ext-subdomain-tenancy). Endpoints /public/* (pré-auth).
3
+ export const SubdomainTenancyPaths = {
4
+ /** POST — cria tenant + usuário root. */
5
+ tenants: "/public/tenants",
6
+ /** POST — valida o e-mail do signup com o código. */
7
+ validateEmail: "/public/tenants/validate-email",
8
+ /** POST — reenvia o e-mail de confirmação (query ?subdomain=). */
9
+ resendEmail: (subdomain) => `/public/tenants/resend-email?subdomain=${encodeURIComponent(subdomain)}`,
10
+ };
@@ -0,0 +1,12 @@
1
+ import type { SecurityServer } from "@seedgrid/fe-security";
2
+ import type { TenantSignupRequest, TenantSignupResult, TenantValidateEmailRequest } from "../tenant-signup";
3
+ export interface SubdomainTenancyServer {
4
+ /** Cria o tenant + usuário root (dispara confirmação de e-mail). */
5
+ signupTenant(req: TenantSignupRequest): Promise<TenantSignupResult>;
6
+ /** Confirma o e-mail do signup com o código. */
7
+ validateEmail(req: TenantValidateEmailRequest): Promise<void>;
8
+ /** Reenvia o e-mail de confirmação. */
9
+ resendEmail(subdomain: string): Promise<void>;
10
+ }
11
+ export declare function createSubdomainTenancyServer(session: SecurityServer): SubdomainTenancyServer;
12
+ //# sourceMappingURL=create-subdomain-tenancy-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-subdomain-tenancy-server.d.ts","sourceRoot":"","sources":["../../src/server/create-subdomain-tenancy-server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,kBAAkB,CAAC;AAO1B,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,YAAY,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,gDAAgD;IAChD,aAAa,CAAC,GAAG,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,uCAAuC;IACvC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAUxB"}
@@ -0,0 +1,18 @@
1
+ // Helpers server-side do signup de tenant (subdomain-tenancy). SERVER-ONLY.
2
+ // Endpoints públicos (pré-auth) — o session server manda sem Bearer (ou serviceToken).
3
+ //
4
+ // const session = createSecurityServer({ ... });
5
+ // export const tenants = createSubdomainTenancyServer(session);
6
+ import { SubdomainTenancyPaths } from "../paths";
7
+ const post = (body) => ({
8
+ method: "POST",
9
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
10
+ });
11
+ export function createSubdomainTenancyServer(session) {
12
+ const P = SubdomainTenancyPaths;
13
+ return {
14
+ signupTenant: (req) => session.apiFetchSecurity(P.tenants, post(req)),
15
+ validateEmail: (req) => session.apiFetchSecurity(P.validateEmail, post(req)),
16
+ resendEmail: (subdomain) => session.apiFetchSecurity(P.resendEmail(subdomain), post()),
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ export { createSubdomainTenancyServer, type SubdomainTenancyServer, } from "./create-subdomain-tenancy-server";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,4BAA4B,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1,2 @@
1
+ // Barrel SERVER (@seedgrid/fe-subdomain-tenancy/server). Roda em action/route.
2
+ export { createSubdomainTenancyServer, } from "./create-subdomain-tenancy-server";
@@ -0,0 +1,13 @@
1
+ export declare const RESERVED_SUBDOMAINS: readonly ["panel", "admin", "adm", "api", "app", "auth", "backoffice", "billing", "dashboard", "dev", "docs", "email", "ftp", "help", "homolog", "internal", "mail", "manager", "monitor", "portal", "root", "seedgrid", "signin", "signup", "site", "smtp", "staging", "status", "suporte", "support", "system", "tenant", "test", "web", "www"];
2
+ export type SubdomainValidationKey = "multitenancy.signup.fields.subdomain.validation.required" | "multitenancy.signup.fields.subdomain.validation.too_short" | "multitenancy.signup.fields.subdomain.validation.too_long" | "multitenancy.signup.fields.subdomain.validation.invalid_pattern" | "multitenancy.signup.fields.subdomain.validation.reserved";
3
+ export declare function normalizeSubdomain(value: string): string;
4
+ export declare function isReservedSubdomain(value: string): boolean;
5
+ export declare function getSubdomainValidationKey(value: string): SubdomainValidationKey | null;
6
+ /**
7
+ * `translate` is any function that resolves an i18n key to a display string
8
+ * (e.g. the app's own `t` from `@/i18n`). Kept as a plain function type
9
+ * instead of importing the app's `Translator` type, so this stays lib-safe
10
+ * (no `@/...` import).
11
+ */
12
+ export declare function validateSubdomain(value: string, translate?: (key: SubdomainValidationKey) => string): string | null;
13
+ //# sourceMappingURL=subdomain-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subdomain-rules.d.ts","sourceRoot":"","sources":["../src/subdomain-rules.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,mVAoCtB,CAAC;AAKX,MAAM,MAAM,sBAAsB,GAC9B,0DAA0D,GAC1D,2DAA2D,GAC3D,0DAA0D,GAC1D,iEAAiE,GACjE,0DAA0D,CAAC;AAE/D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,UAO/C;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,WAEhD;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,GACZ,sBAAsB,GAAG,IAAI,CAwB/B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,MAAM,iBASpD"}
@@ -0,0 +1,82 @@
1
+ export const RESERVED_SUBDOMAINS = [
2
+ "panel",
3
+ "admin",
4
+ "adm",
5
+ "api",
6
+ "app",
7
+ "auth",
8
+ "backoffice",
9
+ "billing",
10
+ "dashboard",
11
+ "dev",
12
+ "docs",
13
+ "email",
14
+ "ftp",
15
+ "help",
16
+ "homolog",
17
+ "internal",
18
+ "mail",
19
+ "manager",
20
+ "monitor",
21
+ "portal",
22
+ "root",
23
+ "seedgrid",
24
+ "signin",
25
+ "signup",
26
+ "site",
27
+ "smtp",
28
+ "staging",
29
+ "status",
30
+ "suporte",
31
+ "support",
32
+ "system",
33
+ "tenant",
34
+ "test",
35
+ "web",
36
+ "www",
37
+ ];
38
+ const RESERVED_SUBDOMAIN_SET = new Set(RESERVED_SUBDOMAINS);
39
+ const SUBDOMAIN_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
40
+ export function normalizeSubdomain(value) {
41
+ return value
42
+ .trim()
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9-]/g, "-")
45
+ .replace(/-{2,}/g, "-")
46
+ .replace(/^-+|-+$/g, "");
47
+ }
48
+ export function isReservedSubdomain(value) {
49
+ return RESERVED_SUBDOMAIN_SET.has(normalizeSubdomain(value));
50
+ }
51
+ export function getSubdomainValidationKey(value) {
52
+ const normalized = normalizeSubdomain(value);
53
+ if (!normalized) {
54
+ return "multitenancy.signup.fields.subdomain.validation.required";
55
+ }
56
+ if (normalized.length < 3) {
57
+ return "multitenancy.signup.fields.subdomain.validation.too_short";
58
+ }
59
+ if (normalized.length > 32) {
60
+ return "multitenancy.signup.fields.subdomain.validation.too_long";
61
+ }
62
+ if (!SUBDOMAIN_PATTERN.test(normalized)) {
63
+ return "multitenancy.signup.fields.subdomain.validation.invalid_pattern";
64
+ }
65
+ if (isReservedSubdomain(normalized)) {
66
+ return "multitenancy.signup.fields.subdomain.validation.reserved";
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * `translate` is any function that resolves an i18n key to a display string
72
+ * (e.g. the app's own `t` from `@/i18n`). Kept as a plain function type
73
+ * instead of importing the app's `Translator` type, so this stays lib-safe
74
+ * (no `@/...` import).
75
+ */
76
+ export function validateSubdomain(value, translate) {
77
+ const key = getSubdomainValidationKey(value);
78
+ if (!key) {
79
+ return null;
80
+ }
81
+ return translate ? translate(key) : key;
82
+ }
@@ -0,0 +1,59 @@
1
+ /** UF brasileira (StateEnum) — ex.: "SP", "RJ". */
2
+ export type BrazilianState = string;
3
+ /** Endereço (AddressDTO aninhado). */
4
+ export interface TenantAddress {
5
+ street: string;
6
+ number: string;
7
+ complement?: string | null;
8
+ postalCode: string;
9
+ city: string;
10
+ state: BrazilianState;
11
+ /** Bairro. */
12
+ district: string;
13
+ phone?: string | null;
14
+ }
15
+ /**
16
+ * Representante legal (LegalRepresentativeDTO aninhado). Espelha
17
+ * TenantSignupRequest.LegalRepresentativeDTO (seedgrid-quarkus-ext-subdomain-tenancy):
18
+ * firstName/lastName/phone são `@NotBlank` no backend (obrigatórios); cpf e
19
+ * birthDate são validados por formato mas não por `@NotBlank` explícito;
20
+ * whatsapp não tem nenhuma validação (opcional de verdade).
21
+ */
22
+ export interface TenantLegalRepresentative {
23
+ firstName: string;
24
+ lastName: string;
25
+ cpf: string;
26
+ /** LocalDate "yyyy-MM-dd". */
27
+ birthDate: string;
28
+ phone: string;
29
+ whatsapp?: string | null;
30
+ }
31
+ /** POST /public/tenants — cria o tenant + usuário root. */
32
+ export interface TenantSignupRequest {
33
+ subdomain: string;
34
+ cnpj: string;
35
+ /** Razão Social. */
36
+ corporateName: string;
37
+ /** Nome Fantasia. */
38
+ tradeName: string;
39
+ address: TenantAddress;
40
+ legalRepresentative: TenantLegalRepresentative;
41
+ rootEmail: string;
42
+ plainRootPassword: string;
43
+ }
44
+ /** POST /public/tenants/validate-email — confirma o e-mail com o código. */
45
+ export interface TenantValidateEmailRequest {
46
+ subdomain: string;
47
+ code: string;
48
+ }
49
+ /**
50
+ * Resultado do signup (shape do backend não confirmado — flexível). O fluxo tem
51
+ * confirmação de e-mail (validate/resend), então normalmente volta um status pendente.
52
+ */
53
+ export interface TenantSignupResult {
54
+ subdomain?: string;
55
+ status?: string;
56
+ message?: string;
57
+ [key: string]: unknown;
58
+ }
59
+ //# sourceMappingURL=tenant-signup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-signup.d.ts","sourceRoot":"","sources":["../src/tenant-signup.ts"],"names":[],"mappings":"AAKA,mDAAmD;AACnD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,CAAC;IACtB,cAAc;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,mBAAmB,EAAE,yBAAyB,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,4EAA4E;AAC5E,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
@@ -0,0 +1,5 @@
1
+ // DTOs de cadastro de TENANT (subdomain-tenancy: cada tenant tem seu subdomínio/DB).
2
+ // Espelham TenantSignupRequest (ext-subdomain-tenancy). Público (pré-auth). BR-coupled.
3
+ // Distinto do login-tenancy (lá é 1 backend, N empresas, escolhe no login) e do
4
+ // self-enter (lá é usuário name/email/password). Aqui cria um TENANT novo.
5
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@seedgrid/fe-subdomain-tenancy",
3
+ "version": "2026.7.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/SeedGrid/seedgrid-fe-components.git"
10
+ },
11
+ "homepage": "https://github.com/SeedGrid/seedgrid-fe-components",
12
+ "bugs": {
13
+ "url": "https://github.com/SeedGrid/seedgrid-fe-components/issues"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server/index.d.ts",
22
+ "default": "./dist/server/index.js"
23
+ },
24
+ "./client": {
25
+ "types": "./dist/client/index.d.ts",
26
+ "default": "./dist/client/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "scaffold"
32
+ ],
33
+ "peerDependencies": {
34
+ "@seedgrid/fe-core": "*",
35
+ "@seedgrid/fe-security": "*",
36
+ "react": "^18.2.0 || ^19.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "react": "19.0.0",
45
+ "@types/react": "^19.0.0",
46
+ "@seedgrid/fe-core": "2026.7.1",
47
+ "@seedgrid/fe-security": "2026.7.2"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "postbuild": "node scripts/emit-module-manifest.mjs",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "pnpm run build && node --test tests/module-manifest.test.mjs tests/subdomain-rules.test.mjs"
54
+ }
55
+ }
@@ -0,0 +1,70 @@
1
+ # `fe-subdomain-tenancy` scaffold
2
+
3
+ Conteúdo copiado para dentro do app gerado pelo CLI (git do app, editável à
4
+ vontade — ver `seedgrid-cli/docs/adr/0004-module-manifest-contract.md`).
5
+ Extraído de `seedgrid-cli/templates/modules/multitenancy/` (nome pré-rename
6
+ — ver `[[seedgrid-tenancy-rename]]`; a pasta local aqui já nasce renomeada
7
+ pra `@/modules/subdomain-tenancy`, mas as CHAVES de i18n (`t("multitenancy.signup...")`)
8
+ não foram renomeadas: elas são dado compartilhado com os arquivos de locale
9
+ do base-app, que ficam fora do escopo desta extração).
10
+
11
+ Mesmo critério lib vs scaffold do `fe-security` (ver o README dele): import
12
+ `@/...` real = scaffold; puro ou so-um-default-trivial = lib.
13
+
14
+ ## Promovido pra `@seedgrid/fe-subdomain-tenancy` (lib)
15
+
16
+ - `TenantProvider`/`useTenantContext` (`client/tenant-provider.tsx`, export em
17
+ `/client`) — contexto React puro, zero import `@/...`.
18
+ - `subdomain-rules.ts` (`normalizeSubdomain`, `isReservedSubdomain`,
19
+ `getSubdomainValidationKey`, `validateSubdomain`, `RESERVED_SUBDOMAINS`) —
20
+ so tinha 1 import `@/...` (`type Translator` do `@/i18n`), e so como tipo
21
+ do parametro OPCIONAL de traducao. Trocado por um tipo de funcao generico
22
+ (`(key) => string`) — decopla sem mudar comportamento.
23
+
24
+ ## Ficou scaffold
25
+
26
+ - `app/(public)/signup/page.tsx` — 21 marcadores handlebars reais (mantido
27
+ `.hbs`); tela inteira de cadastro de tenant (CNPJ/ViaCEP/UF), BR-coupled.
28
+ - `app/api/locations/{cities,states}/route.ts` — route handlers: por
29
+ natureza SEMPRE scaffold (Next.js exige o arquivo num path fisico
30
+ especifico; nao e algo que se "importa" de um pacote).
31
+ - `modules/subdomain-tenancy/server/{create-public-tenant-action,
32
+ resend-public-tenant-activation-email-action,
33
+ validate-public-tenant-email-action,public-api-base-url}.ts` — usam
34
+ `@/i18n` (mensagem de erro) e `@/config/environment`
35
+ (`resolveApiBaseUrlForHostname`, host-coupled).
36
+ - `modules/subdomain-tenancy/signup/types.ts` — shapes de FORM (`PublicTenantSignupFormValues`,
37
+ `CreatePublicTenantPayload`) especificos da tela; mantidos scaffold-local
38
+ pra nao colidir com os tipos de contrato de backend que ja existem em
39
+ `tenant-signup.ts` (lib) — ver "Achado" abaixo.
40
+
41
+ ## Achado: `createSubdomainTenancyServer` nao serve pras 3 actions de signup
42
+
43
+ O pacote ja tem um engine mais novo (`src/server/create-subdomain-tenancy-server.ts`,
44
+ `createSubdomainTenancyServer(session: SecurityServer)`) que compoe sobre o
45
+ `createSecurityServer` do `fe-security`. Mas as 3 actions de signup
46
+ (`create-public-tenant-action` etc.) sao **pre-auth** — chamam
47
+ `/public/tenants/**` com `requireAuth: false`, sem sessao nenhuma — enquanto
48
+ `createSubdomainTenancyServer` usa `session.apiFetchSecurity`, que so faz
49
+ sentido com uma sessao JA autenticada. Os dois nao se encaixam hoje: o
50
+ engine novo foi desenhado pra chamadas autenticadas pos-login, nao pro
51
+ fluxo de signup em si. Por isso as actions foram portadas verbatim (com
52
+ `@/i18n`/`@/config/environment`/`@/modules/core`, comportamento preservado),
53
+ em vez de reescritas em cima do engine novo — mesma decisao tomada no
54
+ `fe-security` pros arquivos de `server/session-routes.ts` etc. Registrado
55
+ como achado, nao corrigido aqui.
56
+
57
+ ## Corrigido: `TenantLegalRepresentative` (lib) estava incompleto
58
+
59
+ Conferido contra o DTO real do backend
60
+ (`seedgrid-quarkus-ext-subdomain-tenancy/.../dtos/TenantSignupRequest.java`,
61
+ `LegalRepresentativeDTO`): `firstName`/`lastName`/`phone` sao `@NotBlank`
62
+ (obrigatorios) e existe um campo `whatsapp` sem nenhuma validacao (opcional
63
+ de verdade). `TenantLegalRepresentative` em `tenant-signup.ts` tinha `phone`
64
+ como opcional (errado) e nao tinha `whatsapp` (faltando) — corrigido pra
65
+ `phone: string` (obrigatorio) + `whatsapp?: string | null`. `name` (nome
66
+ completo) NAO faz parte do DTO — e um getter computado
67
+ (`getName() = firstName + " " + lastName`) so no lado Java, entao
68
+ `CreatePublicTenantPayload.legalRepresentative.name` (scaffold,
69
+ `signup/types.ts`) e so um extra enviado e ignorado pelo backend; nao e
70
+ bug, so nao precisa existir no tipo da lib.