Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ashcroft08/provesa-web/llms.txt

Use this file to discover all available pages before exploring further.

PROVESA Web uses Better Auth for secure, production-ready authentication with email/password login, session management, and rate limiting.

Authentication System

The auth configuration is located in src/lib/server/auth.js:
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { sveltekitCookies } from 'better-auth/svelte-kit';

export const auth = betterAuth({
  baseURL: env.ORIGIN,
  secret: env.BETTER_AUTH_SECRET,
  database: drizzleAdapter(db, { provider: 'pg' }),
  
  session: {
    expiresIn: 60 * 60,  // 1 hour
    updateAge: 0,        // No auto-refresh
  },
  
  emailAndPassword: {
    enabled: true,
    async sendResetPassword({ user, url }) {
      // Email sending logic
    }
  },
  
  plugins: [sveltekitCookies(getRequestEvent)]
});
Better Auth provides database-backed sessions, CSRF protection, and secure password hashing out of the box.

Login Flow

Login Page

The login interface is at /login (src/routes/login/+page.svelte):
1

Visit /login

Users are presented with a clean login form featuring:
  • Email input field
  • Password input field
  • “Forgot password” link
  • Submit button
2

Submit Credentials

Form submits to the server action with progressive enhancement:
<form
  method="POST"
  use:enhance={() => {
    loading = true;
    return async ({ update }) => {
      loading = false;
      await update();
    };
  }}
>
  <input type="email" name="email" required />
  <input type="password" name="password" required />
  <button type="submit" disabled={loading}>
    {loading ? 'Accediendo...' : 'Acceder al Panel'}
  </button>
</form>
3

Server Validation

Better Auth validates credentials against the database and creates a session if successful.
4

Redirect to Admin

On success, user is redirected to /admin. On failure, error message appears.

Login UI Features

Auto-dismissing Errors

Error messages automatically fade after 5 seconds:
$effect(() => {
  if (form?.error) {
    visibleError = form.error;
    const timer = setTimeout(() => {
      visibleError = null;
    }, 5000);
    return () => clearTimeout(timer);
  }
});

Loading States

Button disables during submission with loading text to prevent double-submission.

Glass Morphism Design

Modern UI with backdrop blur and subtle gradients:
.glass-card {
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 1);
}

Return to Site Link

Users can navigate back to the public homepage from the login screen.

Session Management

Session Configuration

session: {
  expiresIn: 60 * 60,  // 1 hour in seconds
  updateAge: 0,        // No automatic refresh
}
This configuration ensures:
  • Security: Sessions expire after 1 hour of creation, not last activity
  • Explicit re-authentication: Users must log in again after timeout
  • Audit trail clarity: Session timestamps are unambiguous
For longer sessions or auto-refresh behavior, adjust expiresIn and updateAge values.

Session Storage

Sessions are stored in the PostgreSQL database via Drizzle ORM:
database: drizzleAdapter(db, { provider: 'pg' })
Better Auth automatically creates and manages session tables.

Rate Limiting

Protection against brute force attacks with configurable rate limits:
rateLimit: {
  window: 60,    // 60 seconds
  max: 100,      // Max 100 requests per window globally
  customRules: {
    '/sign-in/email': {
      window: 60,
      max: 5,      // Max 5 login attempts per minute
    },
    '/forget-password': {
      window: 60,
      max: 3,      // Max 3 password reset requests per minute
    },
  },
}
After 5 failed login attempts in 60 seconds, the IP is temporarily blocked from further login attempts.

Password Reset

Forgot Password Flow

1

Click 'Forgot Password'

Link appears below password field on login page: /recuperar
2

Enter Email Address

User provides their registered email address.
3

Email Sent

Better Auth triggers the sendResetPassword function which sends an email via Gmail SMTP:
async sendResetPassword({ user, url }) {
  const nodemailer = await import('nodemailer');
  const transporter = nodemailer.createTransport({
    service: 'gmail',
    auth: {
      user: env.GMAIL_USER,
      pass: env.GMAIL_APP_PASSWORD
    }
  });
  
  await transporter.sendMail({
    from: `"PROVESA SCC" <${env.GMAIL_USER}>`,
    to: user.email,
    subject: 'Recuperación de contraseña - PROVESA',
    html: `
      <h2>Hola, ${user.name}</h2>
      <p>Recibimos una solicitud para restablecer tu contraseña.</p>
      <a href="${url}">Restablecer Contraseña</a>
    `
  });
}
4

Reset Password

User clicks link in email and sets new password at /restablecer-password.

Email Configuration

Requires environment variables:
GMAIL_USER=your-email@gmail.com
GMAIL_APP_PASSWORD=your-app-password
Use a Gmail App Password, not your regular password. Generate one at Google Account Settings.

Route Protection

Admin Layout Guard

All admin routes are protected via src/routes/admin/+layout.server.js:
export const load = async (event) => {
  const { user } = event.locals;
  
  if (!user) {
    throw redirect(302, '/login');
  }
  
  return { user };
};
The +layout.server.js file protects all routes under /admin/* automatically. Add new admin pages without additional auth checks.

How It Works

  1. User requests /admin or any sub-route
  2. SvelteKit runs the layout server load function
  3. Auth hooks (not shown) populate event.locals.user from session cookie
  4. If no user exists, redirect to /login
  5. If user exists, load proceeds and admin UI renders

Logout

Logout is handled via form submission in the sidebar:
<form action="?/logout" method="POST" use:enhance>
  <button title="Cerrar Sesión">
    <span class="material-icons text-lg">logout</span>
  </button>
</form>
The server action (defined in +page.server.js) destroys the session and redirects to the login page.

User Display

The sidebar shows logged-in user information:
let initials = $derived(
  user?.name
    ? user.name
        .split(' ')
        .map((n) => n[0])
        .join('')
        .toUpperCase()
        .substring(0, 2)
    : 'AD'
);
Displays:
  • Avatar: Circular badge with user initials
  • Name: Full user name
  • Role: “Panel de Control” label

Session Security

  • HTTP-only cookies prevent XSS access
  • CSRF tokens on all form submissions
  • Secure session storage in database

Password Security

  • Passwords hashed with bcrypt
  • Minimum length enforced
  • No plaintext storage

Environment Variables

Required for authentication:
# Better Auth
ORIGIN=http://localhost:5173
BETTER_AUTH_SECRET=your-long-random-secret-key-here

# Email (for password reset)
GMAIL_USER=your-email@gmail.com
GMAIL_APP_PASSWORD=your-16-char-app-password

# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname
Never commit .env files to version control. The BETTER_AUTH_SECRET should be a cryptographically random string of at least 32 characters.

Security Features

Better Auth automatically includes CSRF tokens in forms and validates them on submission.
IP-based rate limits prevent brute force attacks on login and password reset endpoints.
Session cookies are:
  • HTTP-only (not accessible via JavaScript)
  • Secure (HTTPS-only in production)
  • SameSite=Lax (CSRF protection)
Sessions expire after 1 hour and require re-authentication.
All passwords are hashed using bcrypt before database storage.

Extending Authentication

Better Auth supports additional features you can enable:
  • OAuth providers (Google, GitHub, etc.)
  • Two-factor authentication (2FA)
  • Magic link authentication
  • Role-based access control (RBAC)
  • Account linking
See the Better Auth documentation for implementation details.

Build docs developers (and LLMs) love