enderecos cliente-1 cliente-2 e aviso se o cliente nao existe
This commit is contained in:
parent
ae39ca786b
commit
0fd708762c
@ -3,6 +3,7 @@ import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
@ -159,7 +160,7 @@ export class AuthService {
|
||||
private async loginTenant(slug: string, email: string, password: string, ip?: string) {
|
||||
const tenant = await this.platform.tenant.findUnique({ where: { slug } });
|
||||
if (!tenant) {
|
||||
throw new UnauthorizedException('Credenciais inválidas.');
|
||||
throw new NotFoundException('Este cliente não existe.');
|
||||
}
|
||||
if (tenant.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('Empresa suspensa.');
|
||||
@ -205,8 +206,11 @@ export class AuthService {
|
||||
|
||||
private async loginTenantGoogle(slug: string, identity: GoogleIdentity, ip?: string) {
|
||||
const tenant = await this.platform.tenant.findUnique({ where: { slug } });
|
||||
if (!tenant || tenant.status !== 'ACTIVE') {
|
||||
throw new UnauthorizedException('Credenciais inválidas.');
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('Este cliente não existe.');
|
||||
}
|
||||
if (tenant.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('Empresa suspensa.');
|
||||
}
|
||||
const db = this.tenants.getByDatabase(tenant.database);
|
||||
let user =
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlatformController } from './platform.controller';
|
||||
import { PlatformService } from './platform.service';
|
||||
import { PublicTenantsController } from './public-tenants.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [PlatformController],
|
||||
controllers: [PlatformController, PublicTenantsController],
|
||||
providers: [PlatformService],
|
||||
})
|
||||
export class PlatformModule {}
|
||||
|
||||
@ -3,6 +3,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { PlatformPrismaService } from '../prisma/platform-prisma.service';
|
||||
import { createMysqlDatabase, databaseForSlug, pushTenantSchema } from '../tenancy/provision-database';
|
||||
import { publicTenantView } from '../tenancy/public-tenant';
|
||||
import { TenantConnectionService } from '../tenancy/tenant-connection.service';
|
||||
import { normalizeSlug } from '../tenancy/tenant-url';
|
||||
import type { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
@ -21,6 +22,20 @@ export class PlatformService {
|
||||
});
|
||||
}
|
||||
|
||||
async publicBySlug(raw: string) {
|
||||
let slug: string;
|
||||
try {
|
||||
slug = normalizeSlug(raw);
|
||||
} catch {
|
||||
throw new NotFoundException('Este cliente não existe.');
|
||||
}
|
||||
const tenant = await this.platform.tenant.findUnique({
|
||||
where: { slug },
|
||||
select: { name: true, slug: true, status: true },
|
||||
});
|
||||
return publicTenantView(tenant);
|
||||
}
|
||||
|
||||
async create(actorId: string, dto: CreateTenantDto, ip?: string) {
|
||||
const slug = normalizeSlug(dto.slug);
|
||||
const exists = await this.platform.tenant.findUnique({ where: { slug } });
|
||||
|
||||
12
api/src/platform/public-tenants.controller.ts
Normal file
12
api/src/platform/public-tenants.controller.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { PlatformService } from './platform.service';
|
||||
|
||||
@Controller('tenants')
|
||||
export class PublicTenantsController {
|
||||
constructor(private readonly platform: PlatformService) {}
|
||||
|
||||
@Get(':slug')
|
||||
bySlug(@Param('slug') slug: string) {
|
||||
return this.platform.publicBySlug(slug);
|
||||
}
|
||||
}
|
||||
19
api/src/tenancy/public-tenant.spec.ts
Normal file
19
api/src/tenancy/public-tenant.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { publicTenantView } from './public-tenant';
|
||||
|
||||
describe('publicTenantView', () => {
|
||||
it('hides internal fields and returns the public company', () => {
|
||||
expect(
|
||||
publicTenantView({
|
||||
name: 'Cliente 1',
|
||||
slug: 'cliente-1',
|
||||
status: 'ACTIVE',
|
||||
}),
|
||||
).toEqual({ name: 'Cliente 1', slug: 'cliente-1', status: 'ACTIVE' });
|
||||
});
|
||||
|
||||
it('tells the visitor when the company is missing', () => {
|
||||
expect(() => publicTenantView(null)).toThrow(NotFoundException);
|
||||
expect(() => publicTenantView(null)).toThrow('Este cliente não existe.');
|
||||
});
|
||||
});
|
||||
16
api/src/tenancy/public-tenant.ts
Normal file
16
api/src/tenancy/public-tenant.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
|
||||
export type PublicTenant = {
|
||||
name: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export function publicTenantView(
|
||||
tenant: { name: string; slug: string; status: string } | null,
|
||||
): PublicTenant {
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('Este cliente não existe.');
|
||||
}
|
||||
return { name: tenant.name, slug: tenant.slug, status: tenant.status };
|
||||
}
|
||||
@ -87,6 +87,7 @@ services:
|
||||
environment:
|
||||
PORT: 3000
|
||||
HOSTNAME: 0.0.0.0
|
||||
API_INTERNAL_URL: http://api:3001/api
|
||||
ports:
|
||||
- "${SINKA_WEB_PORT:-3000}:3000"
|
||||
healthcheck:
|
||||
|
||||
@ -21,6 +21,8 @@ ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ARG API_INTERNAL_URL=http://api:3001/api
|
||||
ENV API_INTERNAL_URL=$API_INTERNAL_URL
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { RestrictedShell } from "@/components/restricted-shell";
|
||||
import { isTenantSlug } from "@/lib/tenants";
|
||||
import { TenantMissing } from "@/components/tenant-missing";
|
||||
import { fetchPublicTenant, isTenantSlug } from "@/lib/tenants";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function TenantPanelLayout({
|
||||
children,
|
||||
params,
|
||||
@ -15,6 +18,11 @@ export default async function TenantPanelLayout({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const company = await fetchPublicTenant(tenant);
|
||||
if (!company) {
|
||||
return <TenantMissing slug={tenant} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RestrictedShell kind="tenant" basePath={`/${tenant}`}>
|
||||
{children}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { LoginForm } from "@/components/login-form";
|
||||
import { isTenantSlug } from "@/lib/tenants";
|
||||
import { TenantMissing } from "@/components/tenant-missing";
|
||||
import { fetchPublicTenant, isTenantSlug } from "@/lib/tenants";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function TenantLoginPage({ params }: { params: Promise<{ tenant: string }> }) {
|
||||
const { tenant } = await params;
|
||||
@ -9,6 +12,11 @@ export default async function TenantLoginPage({ params }: { params: Promise<{ te
|
||||
notFound();
|
||||
}
|
||||
|
||||
const company = await fetchPublicTenant(tenant);
|
||||
if (!company) {
|
||||
return <TenantMissing slug={tenant} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<main className="flex min-h-dvh items-center justify-center bg-black text-[#E8F4E5]/60">Carregando…</main>}>
|
||||
<LoginForm variant="tenant" tenantSlug={tenant} />
|
||||
|
||||
29
web/src/components/tenant-missing.tsx
Normal file
29
web/src/components/tenant-missing.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export function TenantMissing({ slug }: { slug: string }) {
|
||||
return (
|
||||
<main className="flex min-h-dvh flex-col bg-black text-[#E8F4E5]">
|
||||
<div className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-6 py-16">
|
||||
<a href="/" className="mb-10 block w-fit">
|
||||
<Image
|
||||
src="/brand/sinka-logo.webp"
|
||||
alt="Sinka"
|
||||
width={180}
|
||||
height={58}
|
||||
className="h-8 w-auto"
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-[#82C85C]">Cliente</p>
|
||||
<h1 className="font-display mt-2 text-4xl font-extrabold uppercase leading-none">Este cliente não existe</h1>
|
||||
<p className="mt-4 text-[#E8F4E5]/60">
|
||||
Não há empresa cadastrada em <span className="text-[#E8F4E5]">/{slug}</span>. Confira o endereço com quem
|
||||
contratou a Sinka.
|
||||
</p>
|
||||
<a href="/" className="btn-sinka mt-8 w-fit">
|
||||
Voltar
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -20,3 +20,25 @@ export function isReservedSlug(slug: string): boolean {
|
||||
export function isTenantSlug(slug: string): boolean {
|
||||
return /^[a-z][a-z0-9-]{1,30}$/.test(slug) && !isReservedSlug(slug);
|
||||
}
|
||||
|
||||
export type PublicTenant = {
|
||||
name: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export async function fetchPublicTenant(slug: string): Promise<PublicTenant | null> {
|
||||
const origin = (
|
||||
process.env.API_INTERNAL_URL ??
|
||||
process.env.NEXT_PUBLIC_API_URL ??
|
||||
"http://localhost:3001/api"
|
||||
).replace(/\/$/, "");
|
||||
const response = await fetch(`${origin}/tenants/${encodeURIComponent(slug)}`, { cache: "no-store" });
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error("Não foi possível verificar o cliente.");
|
||||
}
|
||||
return response.json() as Promise<PublicTenant>;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user