tenants Cliente 1 e Cliente 2
This commit is contained in:
parent
dd772b0905
commit
3436ae1e80
@ -11,7 +11,7 @@ SaaS de inteligência logística. Cada operação simula frete, compara transpor
|
|||||||
- Chat interno (admin da plataforma ↔ empresa)
|
- Chat interno (admin da plataforma ↔ empresa)
|
||||||
- Arquivos de operação (CSV/XLSX) por empresa
|
- Arquivos de operação (CSV/XLSX) por empresa
|
||||||
|
|
||||||
Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/demo`, `/outra-empresa`).
|
Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/cliente-1`, `/cliente-2`).
|
||||||
|
|
||||||
## Isolamento
|
## Isolamento
|
||||||
|
|
||||||
@ -68,9 +68,10 @@ docker compose --profile full up -d --build
|
|||||||
| Área | URL | E-mail | Senha |
|
| Área | URL | E-mail | Senha |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Plataforma | `/admin/login` | tina.r@example.net | SinkaPlatform!1 |
|
| Plataforma | `/admin/login` | tina.r@example.net | SinkaPlatform!1 |
|
||||||
| Empresa (admin) | `/demo/login` | xena.w@example.org | SinkaAdmin!1 |
|
| Cliente 1 (admin) | `/cliente-1/login` | xena.w@example.org | SinkaAdmin!1 |
|
||||||
| Empresa (manager) | `/demo/login` | james.b@example.com | SinkaManager!1 |
|
| Cliente 1 (manager) | `/cliente-1/login` | james.b@example.com | SinkaManager!1 |
|
||||||
| Empresa (operador) | `/demo/login` | ursula.b@example.com | SinkaOperador!1 |
|
| Cliente 1 (operador) | `/cliente-1/login` | ursula.b@example.com | SinkaOperador!1 |
|
||||||
|
| Cliente 2 (admin) | `/cliente-2/login` | ivan.p@example.net | Cliente2Admin!1 |
|
||||||
|
|
||||||
## Testes
|
## Testes
|
||||||
|
|
||||||
|
|||||||
@ -24,10 +24,70 @@ async function upsertUser(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureTenant(
|
||||||
|
platform: PlatformPrisma,
|
||||||
|
name: string,
|
||||||
|
slug: string,
|
||||||
|
aliases: string[] = [],
|
||||||
|
) {
|
||||||
|
let tenant = await platform.tenant.findUnique({ where: { slug } });
|
||||||
|
if (!tenant) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
const old = await platform.tenant.findUnique({ where: { slug: alias } });
|
||||||
|
if (old) {
|
||||||
|
tenant = await platform.tenant.update({
|
||||||
|
where: { id: old.id },
|
||||||
|
data: { name, slug },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tenant) {
|
||||||
|
const database = databaseForSlug(slug);
|
||||||
|
await createMysqlDatabase(database);
|
||||||
|
await pushTenantSchema(database);
|
||||||
|
tenant = await platform.tenant.create({ data: { name, slug, database } });
|
||||||
|
} else {
|
||||||
|
await pushTenantSchema(tenant.database);
|
||||||
|
if (tenant.name !== name) {
|
||||||
|
tenant = await platform.tenant.update({ where: { id: tenant.id }, data: { name } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedCompany(
|
||||||
|
tenant: { database: string },
|
||||||
|
users: { email: string; name: string; password: string; role: 'ADMIN' | 'MANAGER' | 'OPERATOR' }[],
|
||||||
|
customers: { name: string; document: string; city: string; state: string }[],
|
||||||
|
carriers: { name: string; baseFee: number; pricePerKg: number; pricePerKm: number; adValoremRate: number }[],
|
||||||
|
) {
|
||||||
|
const db = new TenantPrisma({
|
||||||
|
datasources: { db: { url: urlForDatabase(tenant.database) } },
|
||||||
|
});
|
||||||
|
for (const user of users) {
|
||||||
|
await upsertUser(db, user.email, user.name, user.password, user.role);
|
||||||
|
}
|
||||||
|
for (const customer of customers) {
|
||||||
|
const existing = await db.customer.findFirst({ where: { name: customer.name } });
|
||||||
|
if (!existing) {
|
||||||
|
await db.customer.create({ data: customer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const carrier of carriers) {
|
||||||
|
const existing = await db.carrier.findFirst({ where: { name: carrier.name } });
|
||||||
|
if (existing) {
|
||||||
|
await db.carrier.update({ where: { id: existing.id }, data: carrier });
|
||||||
|
} else {
|
||||||
|
await db.carrier.create({ data: carrier });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const platform = new PlatformPrisma();
|
const platform = new PlatformPrisma();
|
||||||
const slug = 'demo';
|
|
||||||
const database = databaseForSlug(slug);
|
|
||||||
|
|
||||||
const platformEmail = process.env.PLATFORM_ADMIN_EMAIL ?? 'tina.r@example.net';
|
const platformEmail = process.env.PLATFORM_ADMIN_EMAIL ?? 'tina.r@example.net';
|
||||||
const platformPassword = process.env.PLATFORM_ADMIN_PASSWORD ?? 'SinkaPlatform!1';
|
const platformPassword = process.env.PLATFORM_ADMIN_PASSWORD ?? 'SinkaPlatform!1';
|
||||||
@ -42,75 +102,68 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let tenant = await platform.tenant.findUnique({ where: { slug } });
|
const cliente1 = await ensureTenant(platform, 'Cliente 1', 'cliente-1', ['demo']);
|
||||||
if (!tenant) {
|
const cliente2 = await ensureTenant(platform, 'Cliente 2', 'cliente-2');
|
||||||
await createMysqlDatabase(database);
|
|
||||||
await pushTenantSchema(database);
|
|
||||||
tenant = await platform.tenant.create({
|
|
||||||
data: { name: 'Sinka Demo', slug, database },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await pushTenantSchema(tenant.database);
|
|
||||||
}
|
|
||||||
|
|
||||||
const others = await platform.tenant.findMany({ where: { slug: { not: slug } } });
|
const others = await platform.tenant.findMany({
|
||||||
|
where: { slug: { notIn: ['cliente-1', 'cliente-2'] } },
|
||||||
|
});
|
||||||
for (const item of others) {
|
for (const item of others) {
|
||||||
await pushTenantSchema(item.database);
|
await pushTenantSchema(item.database);
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = new TenantPrisma({
|
await seedCompany(
|
||||||
datasources: { db: { url: urlForDatabase(tenant.database) } },
|
cliente1,
|
||||||
});
|
[
|
||||||
|
{
|
||||||
await upsertUser(
|
email: process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org',
|
||||||
db,
|
name: 'Ana Admin',
|
||||||
process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org',
|
password: process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1',
|
||||||
'Ana Admin',
|
role: 'ADMIN',
|
||||||
process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1',
|
},
|
||||||
'ADMIN',
|
{
|
||||||
);
|
email: process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com',
|
||||||
await upsertUser(
|
name: 'Marcos Manager',
|
||||||
db,
|
password: process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1',
|
||||||
process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com',
|
role: 'MANAGER',
|
||||||
'Marcos Manager',
|
},
|
||||||
process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1',
|
{
|
||||||
'MANAGER',
|
email: process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com',
|
||||||
);
|
name: 'Olga Operador',
|
||||||
await upsertUser(
|
password: process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1',
|
||||||
db,
|
role: 'OPERATOR',
|
||||||
process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com',
|
},
|
||||||
'Olga Operador',
|
],
|
||||||
process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1',
|
[
|
||||||
'OPERATOR',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const customer of [
|
|
||||||
{ name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' },
|
{ name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' },
|
||||||
{ name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' },
|
{ name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' },
|
||||||
]) {
|
],
|
||||||
const existing = await db.customer.findFirst({ where: { name: customer.name } });
|
[
|
||||||
if (!existing) {
|
|
||||||
await db.customer.create({ data: customer });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const carriers = [
|
|
||||||
{ name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 },
|
{ name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 },
|
||||||
{ name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 },
|
{ name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 },
|
||||||
{ name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 },
|
{ name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 },
|
||||||
];
|
],
|
||||||
for (const carrier of carriers) {
|
);
|
||||||
const existing = await db.carrier.findFirst({ where: { name: carrier.name } });
|
|
||||||
if (existing) {
|
await seedCompany(
|
||||||
await db.carrier.update({ where: { id: existing.id }, data: carrier });
|
cliente2,
|
||||||
} else {
|
[
|
||||||
await db.carrier.create({ data: carrier });
|
{
|
||||||
}
|
email: 'ivan.p@example.net',
|
||||||
}
|
name: 'Bruno Admin',
|
||||||
|
password: 'Cliente2Admin!1',
|
||||||
|
role: 'ADMIN',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ name: 'Padaria Centro', document: '11.222.333/0001-44', city: 'Curitiba', state: 'PR' },
|
||||||
|
{ name: 'Loja Norte', document: '55.666.777/0001-88', city: 'Joinville', state: 'SC' },
|
||||||
|
],
|
||||||
|
[{ name: 'Carga Paraná', baseFee: 38, pricePerKg: 1.5, pricePerKm: 0.7, adValoremRate: 0.0035 }],
|
||||||
|
);
|
||||||
|
|
||||||
await db.$disconnect();
|
|
||||||
await platform.$disconnect();
|
await platform.$disconnect();
|
||||||
console.log('Seed ok: platform + tenant demo (admin, manager, operador).');
|
console.log('Seed ok: platform + Cliente 1 + Cliente 2.');
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(async (error) => {
|
main().catch(async (error) => {
|
||||||
|
|||||||
@ -14,4 +14,4 @@
|
|||||||
|
|
||||||
Document root de produto: o frontend Next.js. A API não serve HTML.
|
Document root de produto: o frontend Next.js. A API não serve HTML.
|
||||||
|
|
||||||
Login de empresa exige o slug **na URL** (`/demo`, `/filial-sul`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database.
|
Login de empresa exige o slug **na URL** (`/cliente-1`, `/cliente-2`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database.
|
||||||
|
|||||||
@ -13,9 +13,9 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{ source: "/login", destination: "/demo/login", permanent: false },
|
{ source: "/login", destination: "/cliente-1/login", permanent: false },
|
||||||
{ source: "/app", destination: "/demo", permanent: false },
|
{ source: "/app", destination: "/cliente-1", permanent: false },
|
||||||
{ source: "/app/:path*", destination: "/demo/:path*", permanent: false },
|
{ source: "/app/:path*", destination: "/cliente-1/:path*", permanent: false },
|
||||||
{ source: "/platform", destination: "/admin", permanent: false },
|
{ source: "/platform", destination: "/admin", permanent: false },
|
||||||
{ source: "/platform/:path*", destination: "/admin/:path*", permanent: false },
|
{ source: "/platform/:path*", destination: "/admin/:path*", permanent: false },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -225,7 +225,7 @@ export default function PlatformPage() {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className={fieldClass}
|
className={fieldClass}
|
||||||
placeholder="Endereço (ex: demo)"
|
placeholder="Endereço (ex: cliente-1)"
|
||||||
value={slug}
|
value={slug}
|
||||||
onChange={(event) => setSlug(event.target.value.toLowerCase())}
|
onChange={(event) => setSlug(event.target.value.toLowerCase())}
|
||||||
required
|
required
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export function SinkaLanding() {
|
|||||||
<a href="#problema" className="btn-ghost">
|
<a href="#problema" className="btn-ghost">
|
||||||
Conhecer a plataforma
|
Conhecer a plataforma
|
||||||
</a>
|
</a>
|
||||||
<a href="/demo/login" className="btn-sinka">
|
<a href="/cliente-1/login" className="btn-sinka">
|
||||||
Começar agora
|
Começar agora
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +175,7 @@ export function SinkaLanding() {
|
|||||||
<a href="#topo" className="btn-ghost">
|
<a href="#topo" className="btn-ghost">
|
||||||
Conhecer a Sinka
|
Conhecer a Sinka
|
||||||
</a>
|
</a>
|
||||||
<a href="/demo/login" className="btn-sinka">
|
<a href="/cliente-1/login" className="btn-sinka">
|
||||||
Começar agora
|
Começar agora
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export function SiteHeader() {
|
|||||||
Conhecer a plataforma
|
Conhecer a plataforma
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/demo/login"
|
href="/cliente-1/login"
|
||||||
className="rounded-full bg-[#82C85C] px-4 py-2 text-xs font-semibold uppercase tracking-wide text-black"
|
className="rounded-full bg-[#82C85C] px-4 py-2 text-xs font-semibold uppercase tracking-wide text-black"
|
||||||
>
|
>
|
||||||
Entrar
|
Entrar
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user