feat: improved sidebar UI with larger icons and user info in footer
- Increased all sidebar navigation icons from text-sm to text-xl for better visibility - Moved user information and logout button from header to sidebar footer - Added user avatar with name and email in expanded sidebar state - Collapsed sidebar shows avatar icon and logout icon button stacked vertically - Simplified header by removing user badge and logout button - Added version info (v1.0.0) in sidebar footer - Improved responsive design for both collapsed and expanded states
This commit is contained in:
87
src/lib/server/auth.ts
Normal file
87
src/lib/server/auth.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const JWT_SECRET = env.JWT_SECRET || 'change-this-secret-in-production-please';
|
||||
const JWT_EXPIRES_IN = '7d'; // 7 días
|
||||
|
||||
export interface Usuario {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
nombre_completo: string;
|
||||
activo: boolean;
|
||||
es_admin: boolean;
|
||||
}
|
||||
|
||||
export interface SessionPayload {
|
||||
userId: number;
|
||||
username: string;
|
||||
es_admin: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash de contraseña usando bcrypt
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 10;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar contraseña
|
||||
*/
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
try {
|
||||
return await bcrypt.compare(password, hash);
|
||||
} catch (error) {
|
||||
console.error('Error verificando contraseña:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear token JWT
|
||||
*/
|
||||
export function createToken(payload: SessionPayload): string {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar y decodificar token JWT
|
||||
*/
|
||||
export function verifyToken(token: string): SessionPayload | null {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET) as SessionPayload;
|
||||
} catch (error) {
|
||||
console.error('Error verificando token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar fuerza de contraseña
|
||||
*/
|
||||
export function validatePassword(password: string): { valid: boolean; message?: string } {
|
||||
if (password.length < 8) {
|
||||
return { valid: false, message: 'La contraseña debe tener al menos 8 caracteres' };
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return { valid: false, message: 'La contraseña debe contener al menos una mayúscula' };
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return { valid: false, message: 'La contraseña debe contener al menos una minúscula' };
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return { valid: false, message: 'La contraseña debe contener al menos un número' };
|
||||
}
|
||||
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
return { valid: false, message: 'La contraseña debe contener al menos un carácter especial' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
import sql from 'mssql';
|
||||
import pkg from 'pg';
|
||||
const { Pool } = pkg;
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
// Pool de PostgreSQL para usuarios y permisos
|
||||
const pgPool = new Pool({
|
||||
host: env.DB_POSTGRES_HOST || '10.0.20.152',
|
||||
port: parseInt(env.DB_POSTGRES_PORT || '5432'),
|
||||
database: env.DB_POSTGRES_DB || 'CONTROLDESK',
|
||||
user: env.DB_POSTGRES_USER || 'postgres',
|
||||
password: env.DB_POSTGRES_PASS || 'Control.',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000
|
||||
});
|
||||
|
||||
const primaryConfig: sql.config = {
|
||||
user: env.DB_PRIMARY_USER,
|
||||
password: env.DB_PRIMARY_PASS,
|
||||
server: env.DB_PRIMARY_HOST,
|
||||
database: env.DB_PRIMARY_DB,
|
||||
options: {
|
||||
encrypt: true, // For Azure/Remote
|
||||
trustServerCertificate: true // Self-signed certs
|
||||
encrypt: true,
|
||||
trustServerCertificate: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +70,11 @@ class Database {
|
||||
this.azurePool = await new sql.ConnectionPool(azureConfig).connect();
|
||||
return this.azurePool;
|
||||
}
|
||||
|
||||
async getPostgres() {
|
||||
return pgPool.connect();
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new Database();
|
||||
export { sql };
|
||||
export { sql, pgPool };
|
||||
|
||||
312
src/lib/server/users.ts
Normal file
312
src/lib/server/users.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { pgPool } from './db';
|
||||
import { hashPassword, verifyPassword, type Usuario } from './auth';
|
||||
import type { PoolClient } from 'pg';
|
||||
|
||||
/**
|
||||
* Autenticar usuario por username y password
|
||||
*/
|
||||
export async function authenticateUser(username: string, password: string): Promise<Usuario | null> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
'SELECT id, username, email, password_hash, nombre_completo, activo, es_admin FROM usuarios WHERE username = $1 AND activo = true',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const isValid = await verifyPassword(password, user.password_hash);
|
||||
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Actualizar último acceso
|
||||
await client.query(
|
||||
'UPDATE usuarios SET ultimo_acceso = CURRENT_TIMESTAMP WHERE id = $1',
|
||||
[user.id]
|
||||
);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
nombre_completo: user.nombre_completo,
|
||||
activo: user.activo,
|
||||
es_admin: user.es_admin
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener usuario por ID
|
||||
*/
|
||||
export async function getUserById(userId: number): Promise<Usuario | null> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
'SELECT id, username, email, nombre_completo, activo, es_admin FROM usuarios WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.rows[0];
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo usuario
|
||||
*/
|
||||
export async function createUser(data: {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
nombre_completo?: string;
|
||||
es_admin?: boolean;
|
||||
}): Promise<Usuario> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const passwordHash = await hashPassword(data.password);
|
||||
|
||||
const result = await client.query(
|
||||
`INSERT INTO usuarios (username, email, password_hash, nombre_completo, es_admin)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, username, email, nombre_completo, activo, es_admin`,
|
||||
[data.username, data.email, passwordHash, data.nombre_completo || '', data.es_admin || false]
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar usuario
|
||||
*/
|
||||
export async function updateUser(
|
||||
userId: number,
|
||||
data: {
|
||||
email?: string;
|
||||
nombre_completo?: string;
|
||||
activo?: boolean;
|
||||
es_admin?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
): Promise<Usuario | null> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (data.email !== undefined) {
|
||||
updates.push(`email = $${paramIndex++}`);
|
||||
values.push(data.email);
|
||||
}
|
||||
|
||||
if (data.nombre_completo !== undefined) {
|
||||
updates.push(`nombre_completo = $${paramIndex++}`);
|
||||
values.push(data.nombre_completo);
|
||||
}
|
||||
|
||||
if (data.activo !== undefined) {
|
||||
updates.push(`activo = $${paramIndex++}`);
|
||||
values.push(data.activo);
|
||||
}
|
||||
|
||||
if (data.es_admin !== undefined) {
|
||||
updates.push(`es_admin = $${paramIndex++}`);
|
||||
values.push(data.es_admin);
|
||||
}
|
||||
|
||||
if (data.password) {
|
||||
const passwordHash = await hashPassword(data.password);
|
||||
updates.push(`password_hash = $${paramIndex++}`);
|
||||
values.push(passwordHash);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return getUserById(userId);
|
||||
}
|
||||
|
||||
values.push(userId);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE usuarios
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING id, username, email, nombre_completo, activo, es_admin`,
|
||||
values
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar usuario
|
||||
*/
|
||||
export async function deleteUser(userId: number): Promise<boolean> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const result = await client.query('DELETE FROM usuarios WHERE id = $1', [userId]);
|
||||
return result.rowCount ? result.rowCount > 0 : false;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todos los usuarios
|
||||
*/
|
||||
export async function listUsers(): Promise<Usuario[]> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
'SELECT id, username, email, nombre_completo, activo, es_admin FROM usuarios ORDER BY id DESC'
|
||||
);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener permisos de bases de datos de un usuario
|
||||
*/
|
||||
export async function getUserDatabasePermissions(userId: number): Promise<string[]> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
'SELECT base_datos_nombre FROM usuario_base_datos WHERE usuario_id = $1 AND puede_ver = true',
|
||||
[userId]
|
||||
);
|
||||
return result.rows.map(row => row.base_datos_nombre);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asignar permiso de base de datos a usuario
|
||||
*/
|
||||
export async function assignDatabaseToUser(
|
||||
userId: number,
|
||||
baseDatosNombre: string,
|
||||
permisos: {
|
||||
puede_ver?: boolean;
|
||||
puede_descargar_backup?: boolean;
|
||||
puede_restaurar?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
`INSERT INTO usuario_base_datos (usuario_id, base_datos_nombre, puede_ver, puede_descargar_backup, puede_restaurar)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (usuario_id, base_datos_nombre)
|
||||
DO UPDATE SET
|
||||
puede_ver = $3,
|
||||
puede_descargar_backup = $4,
|
||||
puede_restaurar = $5`,
|
||||
[
|
||||
userId,
|
||||
baseDatosNombre,
|
||||
permisos.puede_ver !== false,
|
||||
permisos.puede_descargar_backup || false,
|
||||
permisos.puede_restaurar || false
|
||||
]
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remover permiso de base de datos a usuario
|
||||
*/
|
||||
export async function removeDatabaseFromUser(userId: number, baseDatosNombre: string): Promise<void> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
'DELETE FROM usuario_base_datos WHERE usuario_id = $1 AND base_datos_nombre = $2',
|
||||
[userId, baseDatosNombre]
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si un usuario tiene permiso para ver una base de datos
|
||||
*/
|
||||
export async function userCanViewDatabase(userId: number, baseDatosNombre: string): Promise<boolean> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
// Los admins pueden ver todo
|
||||
const userResult = await client.query(
|
||||
'SELECT es_admin FROM usuarios WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (userResult.rows.length > 0 && userResult.rows[0].es_admin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar permiso específico
|
||||
const permResult = await client.query(
|
||||
'SELECT puede_ver FROM usuario_base_datos WHERE usuario_id = $1 AND base_datos_nombre = $2',
|
||||
[userId, baseDatosNombre]
|
||||
);
|
||||
|
||||
return permResult.rows.length > 0 && permResult.rows[0].puede_ver;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtrar bases de datos según permisos del usuario
|
||||
*/
|
||||
export async function filterDatabasesByUserPermissions<T extends { visible_name: string }>(
|
||||
userId: number,
|
||||
databases: T[]
|
||||
): Promise<T[]> {
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
// Los admins ven todo
|
||||
const userResult = await client.query(
|
||||
'SELECT es_admin FROM usuarios WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (userResult.rows.length > 0 && userResult.rows[0].es_admin) {
|
||||
return databases;
|
||||
}
|
||||
|
||||
// Obtener bases de datos permitidas
|
||||
const permResult = await client.query(
|
||||
'SELECT base_datos_nombre FROM usuario_base_datos WHERE usuario_id = $1 AND puede_ver = true',
|
||||
[userId]
|
||||
);
|
||||
|
||||
const allowedDatabases = new Set(permResult.rows.map(row => row.base_datos_nombre.toLowerCase()));
|
||||
|
||||
return databases.filter(db => allowedDatabases.has(db.visible_name.toLowerCase()));
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user