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:
BIN
src/lib/assets/icono.png
Normal file
BIN
src/lib/assets/icono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/lib/assets/logo.png
Normal file
BIN
src/lib/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -2,36 +2,42 @@
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand-lg md-navbar position-fixed w-100 top-0">
|
||||
<div class="container-fluid px-4">
|
||||
<button class="btn sidebar-toggle d-lg-none me-3" type="button" onclick={() => document.getElementById('sidebar')?.classList.toggle('show')}>
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<div class="md-nav-icon me-3 brand-logo" style="width: 40px; height: 40px; font-size: 1.2rem; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="material-icons-outlined">storage</i>
|
||||
<nav class="fixed inset-x-0 top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
|
||||
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4 md:px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white px-2 py-1 text-slate-700 shadow-sm hover:bg-slate-50 md:hidden"
|
||||
onclick={() => document.getElementById('sidebar')?.classList.toggle('hidden')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">menu</span>
|
||||
</button>
|
||||
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-900 text-slate-50 shadow-sm"
|
||||
>
|
||||
<span class="material-icons-outlined text-base">storage</span>
|
||||
</div>
|
||||
<div class="hidden flex-col md:flex">
|
||||
<span class="text-sm font-semibold text-slate-900">Database Management System</span>
|
||||
<span class="text-[0.7rem] text-slate-500">TransmitirAS</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div class="hidden items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600 shadow-sm md:flex">
|
||||
<span class="material-icons-outlined text-xs">person</span>
|
||||
<span>Admin</span>
|
||||
</div>
|
||||
<span class="brand-text d-none d-sm-block">Database Management System</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav ms-auto d-flex align-items-center">
|
||||
<div class="nav-item dropdown me-3">
|
||||
<a class="nav-link dropdown-toggle px-3" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-2"></i>
|
||||
<span class="d-none d-md-inline">Admin</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end md-card border-0 p-2">
|
||||
<li><a class="dropdown-item rounded" href="#"><i class="bi bi-person me-2"></i>Perfil</a></li>
|
||||
<li><a class="dropdown-item rounded" href="#"><i class="bi bi-gear me-2"></i>Configuración</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger rounded" href="/logout"><i class="bi bi-box-arrow-right me-2"></i>Cerrar Sesión</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="/logout" class="btn btn-primary d-flex align-items-center">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>
|
||||
<span class="d-none d-md-inline">Salir</span>
|
||||
|
||||
<a
|
||||
href="/logout"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-50 shadow-sm hover:bg-slate-800"
|
||||
>
|
||||
<span class="material-icons-outlined text-xs">logout</span>
|
||||
<span class="hidden md:inline">Salir</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,70 +10,106 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="md-sidebar" id="sidebar">
|
||||
<div class="px-3 py-4">
|
||||
<div class="text-center mb-4 d-flex align-items-center px-3">
|
||||
<div style="font-size: 2rem; color: var(--md-primary)" class="me-2">
|
||||
<i class="material-icons-outlined">dashboard</i>
|
||||
<aside
|
||||
id="sidebar"
|
||||
class="hidden md:flex md:flex-col md:w-64 fixed inset-y-0 left-0 bg-white border-r border-slate-200 z-30"
|
||||
>
|
||||
<div class="flex flex-col h-full px-4 py-4">
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center gap-2 mb-6 px-1">
|
||||
<div
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-slate-900 text-slate-100 text-xl shadow-sm"
|
||||
>
|
||||
<span class="material-icons-outlined text-base">dashboard</span>
|
||||
</div>
|
||||
<div class="text-start">
|
||||
<h4 class="brand-text mb-0" style="font-size: 1.1rem; color: var(--md-text-primary)">TransmitirAS</h4>
|
||||
<p class="text-muted small mb-0" style="font-size: 0.75rem">Management System</p>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-semibold text-slate-900">TransmitirAS</span>
|
||||
<span class="text-[0.7rem] text-slate-500">Management System</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav flex-column">
|
||||
<button class="md-nav-link {activeSection === 'summary' ? 'active' : ''}" onclick={() => setActive('summary')}>
|
||||
<i class="bi bi-speedometer2 md-nav-icon"></i>
|
||||
<span>Panel Principal</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'backups' ? 'active' : ''}" onclick={() => setActive('backups')}>
|
||||
<i class="bi bi-hdd-network md-nav-icon"></i>
|
||||
<span>Respaldos Almacenados</span>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 space-y-1 text-sm">
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'summary'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('summary')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">dashboard</span>
|
||||
<span class="text-xs font-medium">Panel Principal</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'clients' ? 'active' : ''}" onclick={() => setActive('clients')}>
|
||||
<i class="bi bi-people md-nav-icon"></i>
|
||||
<span>Catálogo de Clientes</span>
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'backups'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('backups')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">backup</span>
|
||||
<span class="text-xs font-medium">Respaldos Almacenados</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'alerts' ? 'active' : ''}" onclick={() => setActive('alerts')}>
|
||||
<i class="bi bi-exclamation-triangle md-nav-icon"></i>
|
||||
<span>Alertas Críticas</span>
|
||||
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'clients'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('clients')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">people</span>
|
||||
<span class="text-xs font-medium">Catálogo de Clientes</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'azure' ? 'active' : ''}" onclick={() => setActive('azure')}>
|
||||
<i class="bi bi-cloud md-nav-icon"></i>
|
||||
<span>Azure Cloud</span>
|
||||
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'alerts'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('alerts')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">warning</span>
|
||||
<span class="text-xs font-medium">Alertas Críticas</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'databases' ? 'active' : ''}" onclick={() => setActive('databases')}>
|
||||
<i class="bi bi-database md-nav-icon"></i>
|
||||
<span>Gestión de Bases de Datos</span>
|
||||
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'azure'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('azure')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">cloud</span>
|
||||
<span class="text-xs font-medium">Azure Cloud</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition hover:bg-slate-100 ${
|
||||
activeSection === 'databases'
|
||||
? 'bg-slate-900 text-slate-50'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
onclick={() => setActive('databases')}
|
||||
>
|
||||
<span class="material-icons-outlined text-base">dns</span>
|
||||
<span class="text-xs font-medium">Gestión de Bases de Datos</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="mt-5 px-3">
|
||||
<h6 class="text-uppercase text-muted small fw-bold mb-3 ms-2">Configuración</h6>
|
||||
<button class="md-nav-link" onclick={() => console.log('Config')}>
|
||||
<i class="bi bi-gear md-nav-icon"></i>
|
||||
<span>Configuración</span>
|
||||
</button>
|
||||
<a href="/api-docs" class="md-nav-link text-decoration-none">
|
||||
<i class="bi bi-code-square md-nav-icon"></i>
|
||||
<span>API</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto px-4 pt-4 text-center">
|
||||
<div class="d-flex align-items-center justify-content-center text-success mb-2">
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
<small class="fw-bold">Sistema Seguro</small>
|
||||
</div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem;">
|
||||
{new Date().toLocaleDateString()}
|
||||
<!-- Footer -->
|
||||
<div class="mt-6 border-t border-slate-200 pt-3 text-center text-[0.7rem] text-slate-500">
|
||||
<div class="flex items-center justify-center gap-1 mb-1 text-emerald-600">
|
||||
<span class="material-icons-outlined text-xs">shield</span>
|
||||
<span class="font-medium">Sistema Seguro</span>
|
||||
</div>
|
||||
<div>{new Date().toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
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