Add ciclo de vida de sesión y helpers de usuario actual
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
mvp/b2c/src/db/auth-queries.ts
Normal file
47
mvp/b2c/src/db/auth-queries.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from './index';
|
||||||
|
import { users, tenants } from './schema';
|
||||||
|
import { sessionExpiry } from '@/lib/auth/tokens';
|
||||||
|
|
||||||
|
export async function getUserByEmail(email: string) {
|
||||||
|
const [row] = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserById(id: string) {
|
||||||
|
const [row] = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTenantWithOwner(input: {
|
||||||
|
nombreEmpresa: string;
|
||||||
|
slug: string;
|
||||||
|
provincia: string | null;
|
||||||
|
email: string;
|
||||||
|
passwordHash: string;
|
||||||
|
nombre: string | null;
|
||||||
|
}) {
|
||||||
|
const [tenant] = await db
|
||||||
|
.insert(tenants)
|
||||||
|
.values({
|
||||||
|
slug: input.slug,
|
||||||
|
nombreEmpresa: input.nombreEmpresa,
|
||||||
|
provincia: input.provincia,
|
||||||
|
subscriptionStatus: 'trial',
|
||||||
|
trialEndsAt: sessionExpiry(new Date()),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const [user] = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
email: input.email,
|
||||||
|
passwordHash: input.passwordHash,
|
||||||
|
nombre: input.nombre,
|
||||||
|
role: 'reformista',
|
||||||
|
tenantId: tenant.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return { tenant, user };
|
||||||
|
}
|
||||||
25
mvp/b2c/src/lib/auth/current-user.ts
Normal file
25
mvp/b2c/src/lib/auth/current-user.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { getSessionUser } from './session';
|
||||||
|
import { resolveTenantId, type AuthUser } from './authz';
|
||||||
|
|
||||||
|
export async function getCurrentUser(): Promise<AuthUser | null> {
|
||||||
|
return getSessionUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireUser(): Promise<AuthUser> {
|
||||||
|
const user = await getSessionUser();
|
||||||
|
if (!user) redirect('/login');
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireAdmin(): Promise<AuthUser> {
|
||||||
|
const user = await getSessionUser();
|
||||||
|
if (!user) redirect('/login');
|
||||||
|
if (user.role !== 'admin') redirect('/panel');
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentTenantId(): Promise<string> {
|
||||||
|
const user = await getSessionUser();
|
||||||
|
return resolveTenantId(user);
|
||||||
|
}
|
||||||
70
mvp/b2c/src/lib/auth/session.ts
Normal file
70
mvp/b2c/src/lib/auth/session.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { sessions } from '@/db/schema';
|
||||||
|
import { getUserById } from '@/db/auth-queries';
|
||||||
|
import {
|
||||||
|
generateSessionToken,
|
||||||
|
hashSessionToken,
|
||||||
|
isSessionExpired,
|
||||||
|
sessionExpiry,
|
||||||
|
} from './tokens';
|
||||||
|
import type { AuthUser } from './authz';
|
||||||
|
|
||||||
|
const COOKIE = 'session';
|
||||||
|
|
||||||
|
export async function createSession(userId: string): Promise<void> {
|
||||||
|
const token = generateSessionToken();
|
||||||
|
await db.insert(sessions).values({
|
||||||
|
userId,
|
||||||
|
tokenHash: hashSessionToken(token),
|
||||||
|
expiresAt: sessionExpiry(),
|
||||||
|
});
|
||||||
|
const store = await cookies();
|
||||||
|
store.set(COOKIE, token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
expires: sessionExpiry(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function destroySession(): Promise<void> {
|
||||||
|
const store = await cookies();
|
||||||
|
const token = store.get(COOKIE)?.value;
|
||||||
|
if (token) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.tokenHash, hashSessionToken(token)));
|
||||||
|
store.delete(COOKIE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionUser(): Promise<AuthUser | null> {
|
||||||
|
const store = await cookies();
|
||||||
|
const token = store.get(COOKIE)?.value;
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const [session] = await db
|
||||||
|
.select()
|
||||||
|
.from(sessions)
|
||||||
|
.where(eq(sessions.tokenHash, hashSessionToken(token)))
|
||||||
|
.limit(1);
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
if (isSessionExpired(session.expiresAt)) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, session.id));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserById(session.userId);
|
||||||
|
if (!user || user.status !== 'activo') return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
nombre: user.nombre,
|
||||||
|
role: user.role,
|
||||||
|
tenantId: user.tenantId,
|
||||||
|
status: user.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user