Skip to main content

Overview

OdontologyApp implements multiple layers of security to protect sensitive medical data and ensure proper access control. The security architecture includes authentication, role-based access control (RBAC), session management, and protection against common web vulnerabilities.

Authentication

Password Hashing

Passwords are hashed using bcryptjs with salt rounds before storage:
import bcrypt from "bcryptjs";

// During user registration
const hashedPassword = await bcrypt.hash(plainPassword, 10);

// During login
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
Default user credentials:
  • Admin: admin / admin123
  • Doctor: doctor / doctor123
  • Secretary: secretaria / secretaria123
Hashed passwords in database:
INSERT INTO users (name, username, password, role) VALUES 
('Andrea Rojas', 'admin', 
 '$2b$10$mo1jWoA/zRY1wN6x6hsfgeAWNAOE7cLTXseMBiO3M4JSvhjvhImtC', 'admin');

Session Management

Sessions are stored in HTTP-only cookies using SvelteKit’s built-in cookie handling:
// Setting session on login
event.cookies.set("session", JSON.stringify(userData), {
  path: "/",
  httpOnly: true,
  sameSite: "strict",
  secure: process.env.NODE_ENV === "production",
  maxAge: 60 * 60 * 24 * 7, // 7 days
});

// Reading session
const session = event.cookies.get("session");
if (session) {
  const user = JSON.parse(session);
  event.locals.user = user;
}

// Clearing session on logout
event.cookies.delete("session", { path: "/" });
Session data structure:
{
  id: 1,
  name: "Andrea Rojas",
  username: "admin",
  role: "admin",
  branch_id: 1,
  initials: "AR"
}

Global Authentication Guard

The hooks.server.js (src/hooks.server.js:1-68) file implements global authentication:
export async function handle({ event, resolve }) {
  const session = event.cookies.get("session");
  const isLoginPath = event.url.pathname === "/login";
  const isApiPath = event.url.pathname.startsWith("/api");

  if (session) {
    try {
      const user = JSON.parse(session);
      event.locals.user = user;

      // Redirect authenticated users from /login
      if (isLoginPath) {
        throw redirect(303, "/dashboard");
      }

      // Role-based route protection
      const restrictedToAdmin = [
        "/admin/settings",
        "/admin/treatments",
        "/admin/reports",
        "/users",
        "/branches",
        "/logs",
      ];

      const currentPath = event.url.pathname;
      const isAdminRoute = restrictedToAdmin.some(
        (route) => currentPath === route || currentPath.startsWith(route + "/")
      );

      if (isAdminRoute && user.role !== "admin") {
        throw redirect(303, "/dashboard?error=unauthorized");
      }
    } catch (e) {
      if (e?.status === 303) throw e;
      event.locals.user = null;
    }
  } else {
    event.locals.user = null;
    // Require authentication for all routes except /login and /api
    if (!isLoginPath && !isApiPath && event.url.pathname !== "/") {
      throw redirect(303, "/login");
    }
  }

  const response = await resolve(event);
  return response;
}
Authentication flow:
  1. Check for session cookie
  2. Parse user data from session
  3. Store user in event.locals.user
  4. Validate route access based on role
  5. Redirect if unauthorized

Authorization (RBAC)

Role-Based Access Control

OdontologyApp implements a granular permission system with three primary roles: 1. Admin - Full system access 2. Doctor - Clinical operations and patient care 3. Secretary - Administrative and scheduling tasks

Permission System

Permissions are defined in src/lib/permissions.js (src/lib/permissions.js:1-109):
export const PERMISSIONS = {
  // Patient Management
  VIEW_PATIENTS: "VIEW_PATIENTS",
  CREATE_PATIENTS: "CREATE_PATIENTS",
  EDIT_PATIENTS: "EDIT_PATIENTS",
  DELETE_PATIENTS: "DELETE_PATIENTS",
  PRINT_PATIENTS: "PRINT_PATIENTS",

  // Appointment Management
  VIEW_APPOINTMENTS: "VIEW_APPOINTMENTS",
  CREATE_APPOINTMENTS: "CREATE_APPOINTMENTS",
  EDIT_APPOINTMENTS: "EDIT_APPOINTMENTS",
  CANCEL_APPOINTMENTS: "CANCEL_APPOINTMENTS",

  // Clinical Operations
  VIEW_MEDICAL_RECORDS: "VIEW_MEDICAL_RECORDS",
  CREATE_MEDICAL_RECORDS: "CREATE_MEDICAL_RECORDS",
  EDIT_MEDICAL_RECORDS: "EDIT_MEDICAL_RECORDS",
  VIEW_ODONTOGRAM: "VIEW_ODONTOGRAM",
  EDIT_ODONTOGRAM: "EDIT_ODONTOGRAM",
  VIEW_ANAMNESIS: "VIEW_ANAMNESIS",
  EDIT_ANAMNESIS: "EDIT_ANAMNESIS",
  VIEW_INDICATIONS: "VIEW_INDICATIONS",
  CREATE_INDICATIONS: "CREATE_INDICATIONS",
  VIEW_ATTACHMENTS: "VIEW_ATTACHMENTS",
  UPLOAD_ATTACHMENTS: "UPLOAD_ATTACHMENTS",

  // Administration
  VIEW_DOCTORS: "VIEW_DOCTORS",
  MANAGE_DOCTORS: "MANAGE_DOCTORS",
  MANAGE_BRANCHES: "MANAGE_BRANCHES",
  MANAGE_USERS: "MANAGE_USERS",
  VIEW_LOGS: "VIEW_LOGS",
  MANAGE_SECURITY: "MANAGE_SECURITY",
  VIEW_TREATMENTS: "VIEW_TREATMENTS",
  MANAGE_TREATMENTS: "MANAGE_TREATMENTS",
};

Role Permissions Mapping

export const ROLE_PERMISSIONS = {
  admin: Object.values(PERMISSIONS), // All permissions
  
  doctor: [
    PERMISSIONS.VIEW_PATIENTS,
    PERMISSIONS.EDIT_PATIENTS,
    PERMISSIONS.PRINT_PATIENTS,
    PERMISSIONS.VIEW_APPOINTMENTS,
    PERMISSIONS.VIEW_MEDICAL_RECORDS,
    PERMISSIONS.CREATE_MEDICAL_RECORDS,
    PERMISSIONS.EDIT_MEDICAL_RECORDS,
    PERMISSIONS.VIEW_ODONTOGRAM,
    PERMISSIONS.EDIT_ODONTOGRAM,
    PERMISSIONS.VIEW_ANAMNESIS,
    PERMISSIONS.EDIT_ANAMNESIS,
    PERMISSIONS.VIEW_INDICATIONS,
    PERMISSIONS.CREATE_INDICATIONS,
    PERMISSIONS.VIEW_ATTACHMENTS,
    PERMISSIONS.UPLOAD_ATTACHMENTS,
  ],
  
  secretary: [
    PERMISSIONS.VIEW_PATIENTS,
    PERMISSIONS.CREATE_PATIENTS,
    PERMISSIONS.EDIT_PATIENTS,
    PERMISSIONS.PRINT_PATIENTS,
    PERMISSIONS.VIEW_APPOINTMENTS,
    PERMISSIONS.CREATE_APPOINTMENTS,
    PERMISSIONS.EDIT_APPOINTMENTS,
    PERMISSIONS.CANCEL_APPOINTMENTS,
    PERMISSIONS.VIEW_MEDICAL_RECORDS,
    PERMISSIONS.VIEW_ODONTOGRAM,
    PERMISSIONS.VIEW_ANAMNESIS,
  ],
};

Permission Checking

The checkPermission function (src/lib/server/checkPermission.js:17-41) validates user permissions:
export async function checkPermission(locals, permissionCode) {
  const user = locals.user;
  if (!user) return false;

  // Admin has full access
  if (user.role === "admin") return true;

  try {
    // Check database for individual permissions
    const [rows] = await pool.query(
      "CALL sp_check_single_permission(?, ?)",
      [user.id, permissionCode]
    );

    const resultRows = rows[0];
    if (resultRows.length > 0) {
      return resultRows[0].granted === 1;
    }
  } catch (err) {
    console.warn("checkPermission: SP failed, using static fallback.", err);
  }

  // Fallback to role-based permissions
  return (ROLE_PERMISSIONS[user.role] || []).includes(permissionCode);
}
Permission hierarchy:
  1. Admin check: Admins always return true
  2. Database lookup: Check user_permissions table for individual grants
  3. Role fallback: Use ROLE_PERMISSIONS mapping if no DB entry

Using Permissions in API Endpoints

Example: Patient API (src/routes/api/patients/+server.js:10-17):
export async function GET({ url, locals }) {
  // Authentication check
  if (!locals.user) {
    return json({ message: "No autorizado" }, { status: 401 });
  }

  // Permission check
  if (!(await checkPermission(locals, "VIEW_PATIENTS"))) {
    return forbiddenResponse();
  }

  // Proceed with operation
  const [rows] = await pool.query("CALL sp_list_patients(?)", [query]);
  return json({ success: true, patients: rows[0] });
}
Standard response for forbidden access:
export function forbiddenResponse() {
  return json(
    {
      message: "No tienes permiso para realizar esta acción.",
      code: "FORBIDDEN",
    },
    { status: 403 }
  );
}

Frontend Permission Checks

Components can check permissions to show/hide UI elements:
import { hasPermission } from "$lib/permissions";

// In component
let { data } = $props();
const user = data.user;

const canEditPatients = hasPermission(user.role, "EDIT_PATIENTS");
{#if canEditPatients}
  <Button onclick={editPatient}>Edit Patient</Button>
{/if}

Database-Backed Permissions

Admins can grant/revoke individual permissions via the user_permissions table:
-- Grant permission
CALL sp_upsert_user_permission(3, 'DELETE_PATIENTS', 1, 1);
-- user_id=3, permission='DELETE_PATIENTS', granted=1, granted_by=1

-- Revoke permission
CALL sp_upsert_user_permission(3, 'DELETE_PATIENTS', 0, 1);
-- granted=0 revokes the permission
This allows fine-grained control beyond role defaults.

CSRF Protection

SvelteKit CSRF Configuration

CSRF protection is configured in svelte.config.js (src/routes/svelte.config.js:10-12):
const config = {
  kit: {
    csrf: {
      checkOrigin: false, // Disabled for development
    },
  },
};
Production recommendation: Enable checkOrigin: true to validate request origins. Session cookies use sameSite: "strict" to prevent CSRF:
event.cookies.set("session", data, {
  sameSite: "strict", // Only send cookie for same-site requests
  httpOnly: true,     // Prevent JavaScript access
  secure: true,       // HTTPS only in production
});

SQL Injection Prevention

Prepared Statements

All database queries use parameterized statements via mysql2/promise:
// ✅ SAFE - Parameters are escaped automatically
await pool.query(
  "SELECT * FROM patients WHERE id = ? AND branch_id = ?",
  [patientId, branchId]
);

// ✅ SAFE - Stored procedures with parameters
await pool.query(
  "CALL sp_create_patient(?, ?, ?, ?)",
  [firstName, lastName, cedula, phone]
);

// ❌ NEVER DO THIS - String interpolation is vulnerable
await pool.query(
  `SELECT * FROM patients WHERE id = ${patientId}` // SQL INJECTION RISK!
);

Input Validation

All inputs are validated using Zod schemas before database operations:
import { z } from "zod";

export const createPatientSchema = z.object({
  first_name: z.string().min(1, "First name required"),
  last_name: z.string().min(1, "Last name required"),
  cedula: z.string().optional(),
  email: z.string().email().optional(),
  phone: z.string().optional(),
  birth_date: z.string().optional(),
});

// In API handler
const valid = await validateRequest(request, createPatientSchema);
if (!valid.success) {
  return json({ errors: valid.errors }, { status: 400 });
}

Audit Logging

All sensitive operations are logged to the logs table:
CREATE TABLE logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    action VARCHAR(50) NOT NULL,
    module VARCHAR(50) NOT NULL,
    description TEXT,
    ip_address VARCHAR(45),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
Example logging:
await pool.query(
  "INSERT INTO logs (user_id, action, module, description, ip_address) VALUES (?, ?, ?, ?, ?)",
  [user.id, "DELETE_PATIENT", "patients", `Deleted patient ${patientId}`, req.ip]
);

Error Handling

Global Error Handler

The handleError function (src/hooks.server.js:56-67) catches unhandled errors:
export function handleError({ error, event }) {
  console.error("🚨 Error Crítico en el Servidor:", error);

  return {
    message: "Lo sentimos, ha ocurrido un error inesperado.",
    error: error instanceof Error ? error.message : "Internal Server Error",
    status: 500,
  };
}
Security note: Never expose stack traces or internal errors to clients in production.

Security Best Practices

Implemented

✅ Password hashing with bcrypt (10 rounds)
✅ HTTP-only session cookies
✅ Same-site cookie policy (strict)
✅ Prepared SQL statements (no string concatenation)
✅ Input validation with Zod schemas
✅ Role-based access control (RBAC)
✅ Granular permission system
✅ Audit logging for sensitive operations
✅ Authentication guard on all routes
✅ Permission checks on API endpoints
⚠️ Enable CSRF origin checking in production
⚠️ Implement rate limiting for login attempts
⚠️ Add password complexity requirements
⚠️ Implement session timeout/idle detection
⚠️ Add two-factor authentication (2FA)
⚠️ Use HTTPS in production (secure cookies)
⚠️ Implement Content Security Policy (CSP)
⚠️ Add security headers (X-Frame-Options, etc.)
⚠️ Regular security audits and dependency updates

Build docs developers (and LLMs) love