> ## Documentation Index
> Fetch the complete documentation index at: https://ilanaorm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth System

> Build a complete authentication system with users, roles, and password hashing

## Overview

This tutorial builds a complete auth system: registration, login, role-based access, and password reset — using IlanaORM models, mutators, and scopes.

## 1. Migration

<Tip>
  Already know your schema? Run `npx ilana make:model User --migration` to scaffold both files at once and skip straight to [Step 2](#2-user-model).
</Tip>

```bash theme={null}
npx ilana make:migration create_users_table
npx ilana make:migration create_password_reset_tokens_table
```

**`create_users_table`**

```javascript theme={null}
export async function up(schema) {
  await schema.createTable('users', (table) => {
    table.increments('id');
    table.string('name').notNullable();
    table.string('email').notNullable().unique();
    table.string('password').notNullable();
    table.enum('role', ['user', 'admin', 'moderator']).defaultTo('user');
    table.timestamp('email_verified_at').nullable();
    table.timestamps(true, true);
    table.timestamp('deleted_at').nullable();
  });
}
```

**`create_password_reset_tokens_table`**

```javascript theme={null}
export async function up(schema) {
  await schema.createTable('password_reset_tokens', (table) => {
    table.string('email').notNullable().index();
    table.string('token').notNullable();
    table.timestamp('created_at').defaultTo('CURRENT_TIMESTAMP');
  });
}
```

## 2. User Model

```javascript theme={null}
// models/User.js
import bcrypt from 'bcrypt';
import Model from 'ilana-orm/orm/Model.js';

export default class User extends Model {
  static table = 'users';
  static softDeletes = true;
  static fillable = ['name', 'email', 'password', 'role'];
  static hidden = ['password'];

  static {
    this.register();
  }

  // Hash password automatically on set
  setPasswordAttribute(value) {
    return bcrypt.hashSync(value, 12);
  }

  // Scopes
  static scopeVerified(query) {
    return query.whereNotNull('email_verified_at');
  }

  static scopeAdmins(query) {
    return query.where('role', 'admin');
  }

  static scopeRole(query, role) {
    return query.where('role', role);
  }

  // Instance helpers
  verifyPassword(plain) {
    return bcrypt.compareSync(plain, this.password);
  }

  isAdmin() {
    return this.role === 'admin';
  }

  isModerator() {
    return ['admin', 'moderator'].includes(this.role);
  }

  isVerified() {
    return this.email_verified_at !== null;
  }
}
```

## 3. Registration

```javascript theme={null}
// POST /auth/register
app.post('/auth/register', async (req, res) => {
  const { name, email, password } = req.body;

  // Check for existing user
  const existing = await User.query().where('email', email).first();
  if (existing) {
    return res.status(422).json({ message: 'Email already taken' });
  }

  // Create user — password is hashed automatically via mutator
  const user = await User.create({ name, email, password });

  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' });

  res.status(201).json({ user, token });
});
```

## 4. Login

```javascript theme={null}
// POST /auth/login
app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await User.query().where('email', email).first();

  if (!user || !user.verifyPassword(password)) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' });

  res.json({ user, token });
});
```

## 5. Auth Middleware

```javascript theme={null}
// middleware/auth.js
export async function authenticate(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ message: 'Unauthenticated' });

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.find(payload.id);
    if (!req.user) return res.status(401).json({ message: 'User not found' });
    next();
  } catch {
    res.status(401).json({ message: 'Invalid token' });
  }
}

// Role guard factory
export function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user?.role)) {
      return res.status(403).json({ message: 'Forbidden' });
    }
    next();
  };
}
```

Usage:

```javascript theme={null}
app.get('/admin/users', authenticate, requireRole('admin'), async (req, res) => {
  const users = await User.query().paginate(req.query.page, 20);
  res.json(users);
});
```

## 6. Password Reset

```javascript theme={null}
// models/PasswordResetToken.js
import Model from 'ilana-orm/orm/Model.js';

export default class PasswordResetToken extends Model {
  static table = 'password_reset_tokens';
  static timestamps = false;
  static primaryKey = 'email';
  static incrementing = false;

  static {
    this.register();
  }
}
```

```javascript theme={null}
// POST /auth/forgot-password
app.post('/auth/forgot-password', async (req, res) => {
  const user = await User.query().where('email', req.body.email).first();
  if (!user) return res.json({ message: 'If that email exists, a link was sent.' });

  const token = crypto.randomBytes(32).toString('hex');

  await PasswordResetToken.query().where('email', user.email).delete();
  await PasswordResetToken.create({ email: user.email, token });

  await sendPasswordResetEmail(user.email, token); // your mailer

  res.json({ message: 'If that email exists, a link was sent.' });
});

// POST /auth/reset-password
app.post('/auth/reset-password', async (req, res) => {
  const { token, password } = req.body;

  const record = await PasswordResetToken.query().where('token', token).first();
  if (!record) return res.status(400).json({ message: 'Invalid or expired token' });

  const user = await User.query().where('email', record.email).first();
  if (!user) return res.status(400).json({ message: 'User not found' });

  await user.update({ password }); // hashed by mutator
  await PasswordResetToken.query().where('email', record.email).delete();

  res.json({ message: 'Password reset successfully' });
});
```

## 7. Email Verification

```javascript theme={null}
// POST /auth/verify-email
app.post('/auth/verify-email/:token', async (req, res) => {
  const payload = jwt.verify(req.params.token, process.env.JWT_SECRET);
  const user = await User.find(payload.id);
  if (!user) return res.status(404).json({ message: 'User not found' });

  await user.update({ email_verified_at: new Date() });

  res.json({ message: 'Email verified' });
});
```

## Summary

| Feature              | How                                                     |
| -------------------- | ------------------------------------------------------- |
| Password hashing     | Mutator `setPasswordAttribute` — automatic on every set |
| Login                | `user.verifyPassword(plain)` using bcrypt compare       |
| Role-based access    | `requireRole('admin')` middleware + `scopeRole`         |
| Password reset       | Temporary token in `password_reset_tokens` table        |
| Soft delete accounts | `static softDeletes = true` on User                     |
