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:
2026-02-19 12:08:56 -06:00
parent 7e7773578d
commit 5fa97105c9
33 changed files with 6160 additions and 912 deletions

87
src/lib/server/auth.ts Normal file
View 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 };
}

View File

@@ -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
View 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();
}
}