Add signup trial que crea tenant y owner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-05-30 19:50:50 +02:00
parent aecfb2c7e3
commit 795d6a7a19
3 changed files with 76 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
'use server';
import { redirect } from 'next/navigation';
import { eq } from 'drizzle-orm';
import { db } from '@/db';
import { tenants } from '@/db/schema';
import { signupSchema, slugify } from '@/lib/validation/signup';
import { getUserByEmail, createTenantWithOwner, slugDisponible } from '@/db/auth-queries';
import { hashPassword } from '@/lib/auth/password';
import { createSession } from '@/lib/auth/session';
const TRIAL_MS = 14 * 24 * 60 * 60 * 1000;
export async function signup(_prev: string | null, formData: FormData): Promise<string | null> {
const parsed = signupSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return parsed.error.issues[0]?.message ?? 'Datos no válidos.';
const data = parsed.data;
if (await getUserByEmail(data.email)) return 'Ya existe una cuenta con ese email.';
let slug = slugify(data.empresa);
let n = 1;
while (!(await slugDisponible(slug))) slug = `${slugify(data.empresa)}-${++n}`;
const passwordHash = await hashPassword(data.password);
const { tenant, user } = await createTenantWithOwner({
nombreEmpresa: data.empresa,
slug,
provincia: data.provincia,
email: data.email,
passwordHash,
nombre: data.nombre,
});
await db
.update(tenants)
.set({ trialEndsAt: new Date(Date.now() + TRIAL_MS) })
.where(eq(tenants.id, tenant.id));
await createSession(user.id);
redirect('/panel');
}

View File

@@ -0,0 +1,29 @@
'use client';
import { useActionState } from 'react';
import { signup } from './actions';
export default function SignupPage() {
const [error, formAction, pending] = useActionState(signup, null);
return (
<main className="min-h-screen flex items-center justify-center bg-gray-50 px-6 py-12">
<form action={formAction} className="w-full max-w-md bg-white border border-gray-200 rounded-xl p-8 flex flex-col gap-4">
<h1 className="text-xl font-black tracking-tight text-black">Empieza gratis 14 días</h1>
<p className="text-sm text-gray-500">Sin tarjeta. Configura tu catálogo y recibe leads.</p>
<input name="nombre" placeholder="Tu nombre" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="empresa" placeholder="Nombre de tu empresa" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="email" type="email" placeholder="Email" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="provincia" placeholder="Provincia" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="password" type="password" placeholder="Contraseña (mín. 8)" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<label className="flex items-center gap-2 text-xs text-gray-500">
<input name="optInMarketing" type="checkbox" /> Quiero recibir novedades de Reformix
</label>
{error && <p className="text-sm text-red-600">{error}</p>}
<button type="submit" disabled={pending} className="bg-black text-white rounded-md py-2 font-semibold disabled:opacity-60">
{pending ? 'Creando cuenta…' : 'Crear cuenta'}
</button>
<a href="/login" className="text-xs text-gray-400 text-center hover:text-black">Ya tengo cuenta</a>
</form>
</main>
);
}

View File

@@ -45,3 +45,8 @@ export async function createTenantWithOwner(input: {
return { tenant, user }; return { tenant, user };
} }
export async function slugDisponible(slug: string): Promise<boolean> {
const [row] = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.slug, slug)).limit(1);
return !row;
}