Add signup trial que crea tenant y owner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
42
mvp/b2c/src/app/signup/actions.ts
Normal file
42
mvp/b2c/src/app/signup/actions.ts
Normal 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');
|
||||||
|
}
|
||||||
29
mvp/b2c/src/app/signup/page.tsx
Normal file
29
mvp/b2c/src/app/signup/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user