Files
la-huasca-ts/src/lib/auth.ts
T
muken 1e66e918ac feat: add messaging system with admin bulk messages and user conversations
- Add conversations and direct_messages tables to schema
- User messaging page at /messages (users can start conversations, see admin responses)
- Admin messaging page at /admin/messages (view all conversations, reply to users)
- Admin bulk messaging: send to all customers, specific users, or admins
- Mail icon in navbar for logged-in users (links to appropriate messaging page)
- Show first name of admin who responded in conversation view
- Clean build cache after middleware changes
2026-07-03 00:06:07 -05:00

98 lines
3.5 KiB
TypeScript

import { cookies } from 'next/headers';
import { SignJWT, jwtVerify } from 'jose';
import bcrypt from 'bcryptjs';
import { db } from './db';
function getSecret() {
const s = process.env.JWT_SECRET;
if (!s) throw new Error('JWT_SECRET is not configured');
return new TextEncoder().encode(s);
}
export async function hashPassword(password: string) {
return bcrypt.hashSync(password, 10);
}
export async function verifyPassword(password: string, hash: string) {
return bcrypt.compareSync(password, hash);
}
export async function createUser(
username: string,
password: string,
role: 'admin' | 'user',
profile?: { first_name?: string; last_name?: string; email?: string; phone?: string; country?: string; preferred_language?: string }
) {
const hash = await hashPassword(password);
const { rows } = await db.query(
`INSERT INTO users (username, password_hash, role, first_name, last_name, email, phone, country, preferred_language)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash, role = EXCLUDED.role
RETURNING id, username, role, first_name, last_name, email, phone, country, preferred_language`,
[username, hash, role, profile?.first_name || null, profile?.last_name || null, profile?.email || null, profile?.phone || null, profile?.country || null, profile?.preferred_language || 'es']
);
return rows[0];
}
export async function authenticateUser(username: string, password: string) {
const { rows } = await db.query('SELECT id, username, password_hash, role, preferred_language, terms_accepted_at, first_name, last_name FROM users WHERE username = $1', [username]);
if (rows.length === 0) return null;
const user = rows[0];
const valid = await verifyPassword(password, user.password_hash);
if (!valid) return null;
return {
id: user.id,
username: user.username,
role: user.role,
preferred_language: user.preferred_language || 'es',
terms_accepted_at: user.terms_accepted_at,
first_name: user.first_name,
last_name: user.last_name
};
}
export async function createSession(user: { id: number; username: string; role: string }) {
const token = await new SignJWT({ id: user.id, username: user.username, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('4h') // 4 hours for security (reduced from 7 days)
.sign(getSecret());
cookies().set('session', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 4 }); // 4 hours
return token;
}
export async function getSession() {
const token = cookies().get('session')?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, getSecret());
return payload as { id: number; username: string; role: string };
} catch {
return null;
}
}
export async function clearSession() {
cookies().delete('session');
}
export async function requireRole(role: 'admin' | 'user') {
const session = await getSession();
if (!session || session.role !== role) {
throw new Error('Unauthorized');
}
return session;
}
export async function requireAuth() {
const session = await getSession();
if (!session) {
throw new Error('Unauthorized');
}
return session;
}
export async function canComment(userId: number) {
const { rows } = await db.query('SELECT comments_disabled FROM users WHERE id = $1', [userId]);
return rows.length === 0 || !rows[0].comments_disabled;
}