1e66e918ac
- 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
98 lines
3.5 KiB
TypeScript
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;
|
|
}
|