Admin UI
Authentication

Authentication

The Admin UI supports JWT-based authentication with customizable login and token validation.

Overview

Authentication flow:

  1. User submits email/password on login page
  2. login function validates credentials
  3. JWT token is generated and stored in cookie
  4. principalForToken validates token on each request
  5. Principal object provides permissions

Configuration

PropertyTypeDescription
loginFunctionValidates credentials and returns token
principalForTokenFunctionValidates token and returns Principal

login

login?: (
  runtime: ActiveQLServer,
  props: { email: string; password: string }
) => Promise<{ token?: string }>
TypeDefaultDescription
Function-Custom login handler

Return an object with token on successful authentication, or empty object on failure.

Example

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
 
{
  login: async (runtime, { email, password }) => {
    // Find user
    const user = await runtime.entity('User').findOneByAttribute({ email });
 
    if (!user) {
      return {}; // Invalid email
    }
 
    // Verify password
    const valid = await bcrypt.compare(password, user.password);
 
    if (!valid) {
      return {}; // Invalid password
    }
 
    // Check if user can access admin
    if (!user.roles.includes('admin')) {
      return {}; // Not authorized
    }
 
    // Generate JWT
    const token = jwt.sign(
      {
        userId: user.id,
        email: user.email,
        roles: user.roles
      },
      process.env.JWT_SECRET,
      { expiresIn: '8h' }
    );
 
    return { token };
  }
}

principalForToken

principalForToken?: (
  token: string,
  runtime: ActiveQLServer
) => Promise<Principal | undefined>
TypeDefaultDescription
Function-Token validation and Principal creation

Called on every authenticated request. Return a Principal object or undefined if token is invalid.

Example

import jwt from 'jsonwebtoken';
import { Principal } from 'activeql-server';
 
{
  principalForToken: async (token, runtime) => {
    try {
      // Verify token
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
 
      // Create Principal
      return new Principal(
        decoded.userId,
        decoded.roles
      );
    } catch (error) {
      // Token invalid or expired
      return undefined;
    }
  }
}

Principal Object

The Principal represents the authenticated user and controls permissions.

class Principal {
  constructor(
    public id: string | number,
    public roles: string[]
  ) {}
}

Permission Checks

Permissions are checked automatically for CRUD operations:

  • READ: List and show items
  • CREATE: Create new items
  • UPDATE: Edit existing items
  • DELETE: Delete items

Configure entity permissions in your ActiveQL schema.

Login Page Customization

Customize the login page content:

{
  content: {
    login: (activeAdmin) => ({
      title: 'Admin Login',
      subtitle: 'Please sign in to continue',
      logo: '/img/logo.png'
    })
  }
}

Session Management

  • Token is stored in HTTP-only cookie
  • Default expiration: 8 hours (configurable in JWT)
  • Logout clears the cookie

Complete Example

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { Principal } from 'activeql-server';
import { addActiveAdmin } from 'activeql-active-admin';
 
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
 
addActiveAdmin({
  path: '/admin',
  title: 'My Admin',
 
  login: async (runtime, { email, password }) => {
    // Find user by email
    const user = await runtime.entity('User').findOneByAttribute({ email });
 
    if (!user) {
      console.log('Login failed: user not found', email);
      return {};
    }
 
    // Verify password
    const valid = await bcrypt.compare(password, user.password);
 
    if (!valid) {
      console.log('Login failed: invalid password', email);
      return {};
    }
 
    // Check admin role
    if (!user.isAdmin && !user.roles?.includes('admin')) {
      console.log('Login failed: not an admin', email);
      return {};
    }
 
    // Generate token
    const token = jwt.sign(
      {
        userId: user.id,
        email: user.email,
        roles: user.roles || (user.isAdmin ? ['admin'] : [])
      },
      JWT_SECRET,
      { expiresIn: '8h' }
    );
 
    // Log successful login
    console.log('Login successful', email);
 
    return { token };
  },
 
  principalForToken: async (token, runtime) => {
    try {
      const decoded = jwt.verify(token, JWT_SECRET);
 
      // Optionally refresh user data
      const user = await runtime.entity('User').findById(decoded.userId);
 
      if (!user) {
        return undefined; // User deleted
      }
 
      if (!user.isActive) {
        return undefined; // User deactivated
      }
 
      return new Principal(
        decoded.userId,
        user.roles || decoded.roles
      );
    } catch (error) {
      if (error.name === 'TokenExpiredError') {
        console.log('Token expired');
      } else {
        console.log('Token validation failed', error.message);
      }
      return undefined;
    }
  },
 
  content: {
    login: () => ({
      title: 'Admin Panel',
      subtitle: 'Enter your credentials'
    })
  },
 
  entities: {
    // Entity configurations
  }
});

Security Best Practices

  1. Use strong JWT secret: At least 256 bits of entropy
  2. Set appropriate expiration: Balance security and usability
  3. Use HTTPS: Always in production
  4. Hash passwords: Use bcrypt with appropriate cost factor
  5. Validate on every request: Always verify token freshness
  6. Check user status: Account might be disabled after login
  7. Log authentication events: For security auditing
  8. Rate limit login attempts: Prevent brute force attacks

Environment Variables

JWT_SECRET=your-very-long-secret-key-with-high-entropy
JWT_EXPIRATION=8h
const config = {
  login: async (runtime, { email, password }) => {
    // ...
    const token = jwt.sign(payload, process.env.JWT_SECRET, {
      expiresIn: process.env.JWT_EXPIRATION || '8h'
    });
    return { token };
  }
};