Skip to content

Authentication

Technomantus Corvi edited this page Sep 5, 2026 · 1 revision

Authentication (tanuki_login)

A complete, session-based auth system — login, logout, registration, and password recovery by email. Lives in the repo from the start but stays inert: no route is registered by default, so it costs nothing until you uncomment its routes in routes.php.

What's included

Piece File
Mail sending (SMTP) config/mail.phpMail::send($to, $subject, $html)
Session auth helpers auth.phpauth_check(), auth_user(), auth_login(), auth_logout(), auth_require()
User model models/UserModel.php
Password reset tokens models/PasswordResetModel.php
Controller controllers/AuthController.php
Profile editing controllers/ProfileController.php
Views views/auth/*.php, views/profile/edit.php

Setup

1. Install PHPMailer

composer require phpmailer/phpmailer

2. Create the tables (MySQL/MariaDB shown; see the SQL comments for PostgreSQL types):

CREATE TABLE users (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    name          VARCHAR(255) NOT NULL,
    email         VARCHAR(255) NOT NULL UNIQUE,
    password      VARCHAR(255) NOT NULL,
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE password_resets (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    email         VARCHAR(255) NOT NULL,
    token         VARCHAR(255) NOT NULL,
    expires_at    TIMESTAMP NOT NULL,
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

If you register the routes without these tables, you get a clear 500 (or the full exception in debug mode) the moment a query touches users — that's the intended signal this step was skipped.

3. Wire it into the bootstrap (core/App.php, alongside the other config/ requires):

require_once __DIR__ . '/../config/mail.php';
require_once __DIR__ . '/../auth.php';

4. Configure SMTP in .env:

MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="${APP_NAME}"

5. Uncomment the routes in routes.php:

'GET  /login'                   => 'AuthController@showLogin',
'POST /login'                   => 'AuthController@login',
'POST /logout'                  => 'AuthController@logout',
'GET  /register'                => 'AuthController@showRegister',
'POST /register'                => 'AuthController@register',
'GET  /forgot-password'         => 'AuthController@showForgot',
'POST /forgot-password'         => 'AuthController@sendResetLink',
'GET  /reset-password/{token}'  => 'AuthController@showReset',
'POST /reset-password/{token}'  => 'AuthController@resetPassword',
'GET  /profile'                 => 'ProfileController@edit',
'POST /profile'                 => 'ProfileController@update',

Protecting a route

class DashboardController extends Controller
{
    public function index(): void
    {
        auth_require(); // redirects to /login if not authenticated
        $user = auth_user();
        $this->view('dashboard/index', ['user' => $user]);
    }
}

auth_require() saves the current URL before redirecting, so login sends the user back to where they wanted to go.

The nav user menu

includes/head.php already renders a user dropdown (avatar, profile link, logout) when auth_check() is true, and a "Log In" button otherwise — no extra wiring once the routes are active.

Password reset flow

  1. User submits their email at /forgot-password.
  2. A random token is generated, SHA-256-hashed, stored with a 1-hour expiry. The plain token is emailed (not the hash) — standard practice, so a leaked table alone can't be used to reset accounts.
  3. The same "link sent" message shows whether or not the email exists, to avoid leaking registered emails.
  4. /reset-password/{token} validates the token before allowing a new password.

Editing your profile

/profile lets a logged-in user change their name and, optionally, their password (current password required) — not their email, by design, to keep this simple. Email changes typically need their own re-verification flow.

Removing the extension

Remove the two require_once lines from App.php, comment/delete the routes in routes.php, and (optionally) delete the files listed above. Nothing else in the project depends on them.

Clone this wiki locally