Arcwayv0.3.0
Cookbook

Authentication

Signup, login, logout, API key auth, and role-based access control patterns

Common authentication patterns for Arcway apps.

Signup with Password Hashing

// api/auth/signup.js
import { vault } from 'arcway';

export const POST = {
  schema: {
    body: {
      name: 'string>0',
      email: 'string.email',
      password: 'string>=8',
    },
  },
  handler: async (ctx) => {
    const { db, events } = ctx;
    const { name, email, password } = ctx.req.body;

    const existing = await db('users').where('email', email).first();
    if (existing) {
      return { status: 409, error: { code: 'EMAIL_TAKEN', message: 'Email already registered' } };
    }

    const hash = await vault.hashPassword(password);
    const [id] = await db('users').insert({
      id: vault.generateNanoId(),
      name,
      email,
      password_hash: hash,
    });

    const user = await db('users').where('id', id).select('id', 'name', 'email').first();
    await events.emit('users/created', { id: user.id, email: user.email });

    return {
      status: 201,
      data: user,
      session: { userId: user.id },
    };
  },
};

Login

// api/auth/login.js
import { vault } from 'arcway';

export const POST = {
  schema: {
    body: { email: 'string.email', password: 'string>0' },
  },
  handler: async (ctx) => {
    const { db } = ctx;
    const { email, password } = ctx.req.body;
    const user = await db('users').where('email', email).first();

    if (!user || !(await vault.verifyPassword(password, user.password_hash))) {
      return { status: 401, error: { code: 'INVALID_CREDENTIALS', message: 'Wrong email or password' } };
    }

    return {
      data: { id: user.id, name: user.name, email: user.email },
      session: { userId: user.id, role: user.role },
    };
  },
};

Logout

// api/auth/logout.js
export const POST = {
  handler: async (ctx) => {
    return { data: { ok: true }, session: null };
  },
};

Current User Endpoint

// api/auth/me.js
export const GET = {
  handler: async (ctx) => {
    const { db } = ctx;
    if (!ctx.req.session?.userId) {
      return { status: 401, error: { code: 'UNAUTHORIZED' } };
    }
    const user = await db('users')
      .where('id', ctx.req.session.userId)
      .select('id', 'name', 'email', 'role')
      .first();

    if (!user) {
      return { status: 401, error: { code: 'UNAUTHORIZED' }, session: null };
    }
    return { data: user };
  },
};

Protected Routes with Middleware

Use global middleware to require authentication on all routes, allowing auth endpoints through:

// api/_middleware.js
export default {
  handler: async (ctx) => {
    // Allow auth routes through
    if (ctx.req.path.startsWith('/api/auth/')) return;

    if (!ctx.req.session?.userId) {
      return { status: 401, error: { code: 'UNAUTHORIZED', message: 'Login required' } };
    }
  },
};

API Key Authentication

For machine-to-machine access, validate an API key header using method-specific middleware:

// api/webhooks/_middleware.js
// Only apply auth to non-GET requests
export const POST = {
  handler: async (ctx) => {
    const { db, log } = ctx;
    const key = ctx.req.headers['x-api-key'];
    if (!key) {
      return { status: 401, error: { code: 'MISSING_API_KEY', message: 'API key required' } };
    }

    const apiKey = await db('api_keys').where('key', key).where('active', true).first();
    if (!apiKey) {
      return { status: 401, error: { code: 'INVALID_API_KEY', message: 'Invalid or revoked API key' } };
    }

    // Log API key usage
    log.info('API key used', { keyId: apiKey.id, path: ctx.req.path });
    // You can attach data to the session for downstream handlers
    // (session changes aren't persisted unless returned in response)
  },
};

Role-Based Access Control

Enforce roles at the middleware level:

// api/admin/_middleware.js
export default {
  handler: async (ctx) => {
    if (!ctx.req.session?.userId) {
      return { status: 401, error: { code: 'UNAUTHORIZED', message: 'Login required' } };
    }
    if (ctx.req.session.role !== 'admin') {
      return { status: 403, error: { code: 'FORBIDDEN', message: 'Admin access required' } };
    }
  },
};

On this page