Overview
This tutorial builds a complete auth system: registration, login, role-based access, and password reset — using IlanaORM models, mutators, and scopes.1. Migration
Already know your schema? Run
npx ilana make:model User --migration to scaffold both files at once and skip straight to Step 2.npx ilana make:migration create_users_table
npx ilana make:migration create_password_reset_tokens_table
create_users_table
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
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
// 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
// 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
// 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
// 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();
};
}
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
// 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();
}
}
// 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
// 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 |
