@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.
- package/README.md +75 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/tenant-provider.d.ts +11 -0
- package/dist/client/tenant-provider.d.ts.map +1 -0
- package/dist/client/tenant-provider.js +19 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +16 -0
- package/dist/module.json +14 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/server/create-subdomain-tenancy-server.d.ts +12 -0
- package/dist/server/create-subdomain-tenancy-server.d.ts.map +1 -0
- package/dist/server/create-subdomain-tenancy-server.js +18 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/subdomain-rules.d.ts +13 -0
- package/dist/subdomain-rules.d.ts.map +1 -0
- package/dist/subdomain-rules.js +82 -0
- package/dist/tenant-signup.d.ts +59 -0
- package/dist/tenant-signup.d.ts.map +1 -0
- package/dist/tenant-signup.js +5 -0
- package/package.json +55 -0
- package/scaffold/README.md +70 -0
- package/scaffold/src/app/(public)/signup/page.tsx +1165 -0
- package/scaffold/src/app/api/locations/cities/route.ts +125 -0
- package/scaffold/src/app/api/locations/states/route.ts +120 -0
- package/scaffold/src/modules/subdomain-tenancy/index.ts +20 -0
- package/scaffold/src/modules/subdomain-tenancy/server/create-public-tenant-action.ts +113 -0
- package/scaffold/src/modules/subdomain-tenancy/server/public-api-base-url.ts +28 -0
- package/scaffold/src/modules/subdomain-tenancy/server/resend-public-tenant-activation-email-action.ts +118 -0
- package/scaffold/src/modules/subdomain-tenancy/server/validate-public-tenant-email-action.ts +138 -0
- 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
|
+
[](https://www.npmjs.com/package/@seedgrid/fe-subdomain-tenancy)
|
|
4
|
+
[](https://www.npmjs.com/package/@seedgrid/fe-subdomain-tenancy)
|
|
5
|
+
[](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 @@
|
|
|
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/module.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/module.json
ADDED
package/dist/paths.d.ts
ADDED
|
@@ -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 @@
|
|
|
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,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.
|