User Management

User Management

Recipes for managing users, roles, permissions, tokens, SSH keys, and sessions. ModulaCMS uses role-based access control (RBAC) with resource:operation granular permissions.

For background, see authentication and authorization.

Bootstrap Roles

ModulaCMS ships with three built-in roles. These are system-protected and cannot be deleted or renamed.

Role Permissions Description
admin All 72 permissions Full access, bypasses permission checks
editor 36 permissions Content management, media, routes, datatypes, fields, field types
viewer 5 permissions Read-only access (content, media, routes, field types)

Create a User

New users created via the admin API can be assigned any role. Users created via /auth/register are always assigned the viewer role.

curl:

curl -X POST http://localhost:8080/api/v1/users \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "jdoe",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "password": "secure-password-123",
    "role": "editor"
  }'

Response (201):

{
  "user_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
  "username": "jdoe",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "editor",
  "date_created": "2026-01-15T10:00:00Z",
  "date_modified": "2026-01-15T10:00:00Z"
}

Go SDK:

user, err := client.Users.Create(ctx, modula.CreateUserParams{
    Username: "jdoe",
    Name:     "Jane Doe",
    Email:    modula.Email("jane@example.com"),
    Password: "secure-password-123",
    Role:     "editor",
})
if err != nil {
    // handle error
}

fmt.Printf("Created user %s with role %s\n", user.UserID, user.Role)

TypeScript SDK (admin):

const user = await admin.users.create({
  username: 'jdoe',
  name: 'Jane Doe',
  email: 'jane@example.com' as Email,
  password: 'secure-password-123',
  role: 'editor',
})

console.log(`Created user ${user.user_id} with role ${user.role}`)

Assign a Role

Update a user's role by setting the role field. The role must be a valid role label.

curl:

curl -X PUT http://localhost:8080/api/v1/users/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
    "username": "jdoe",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "role": "admin"
  }'

Go SDK:

updated, err := client.Users.Update(ctx, modula.UpdateUserParams{
    UserID:   modula.UserID("01HXK4N2F8RJZGP6VTQY3MCSW9"),
    Username: "jdoe",
    Name:     "Jane Doe",
    Email:    modula.Email("jane@example.com"),
    Role:     "admin",
})

TypeScript SDK (admin):

const updated = await admin.users.update({
  user_id: '01HXK4N2F8RJZGP6VTQY3MCSW9' as UserID,
  username: 'jdoe',
  name: 'Jane Doe',
  email: 'jane@example.com' as Email,
  role: 'admin',
})

Create a Custom Role with Specific Permissions

Custom roles are created in two steps: create the role, then assign permissions to it via the role-permissions junction table.

Step 1: Create the role.

curl:

curl -X POST http://localhost:8080/api/v1/roles \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "contributor"}'

Step 2: Assign permissions.

# Get the list of available permissions
curl http://localhost:8080/api/v1/permissions \
  -H "Authorization: Bearer YOUR_API_KEY"

# Assign specific permissions to the new role
curl -X POST http://localhost:8080/api/v1/role-permissions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role_id": "01HXK7A1...", "permission_id": "01HXK8B2..."}'

Go SDK:

// Step 1: Create role
role, err := client.Roles.Create(ctx, modula.CreateRoleParams{
    Label: "contributor",
})
if err != nil {
    // handle error
}

// Step 2: List all available permissions
perms, err := client.Permissions.List(ctx)
if err != nil {
    // handle error
}

// Step 3: Assign selected permissions
wantedPerms := []string{"content:read", "content:create", "media:read", "media:create"}
for _, p := range perms {
    for _, wanted := range wantedPerms {
        if p.Label == wanted {
            _, err := client.RolePermissions.Create(ctx, modula.CreateRolePermissionParams{
                RoleID:       role.RoleID,
                PermissionID: p.PermissionID,
            })
            if err != nil {
                // handle error
            }
        }
    }
}

TypeScript SDK (admin):

// Step 1: Create role
const role = await admin.roles.create({ label: 'contributor' })

// Step 2: List available permissions
const perms = await admin.permissions.list()

// Step 3: Assign selected permissions
const wanted = ['content:read', 'content:create', 'media:read', 'media:create']
for (const p of perms.filter(p => wanted.includes(p.label))) {
  await admin.rolePermissions.create({
    role_id: role.role_id,
    permission_id: p.permission_id,
  })
}

List Permissions for a Role

curl:

curl "http://localhost:8080/api/v1/role-permissions/role/?q=01HXK7A1..." \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

rolePerms, err := client.RolePermissions.ListByRole(ctx, modula.RoleID("01HXK7A1..."))
if err != nil {
    // handle error
}

for _, rp := range rolePerms {
    fmt.Printf("Permission: %s\n", rp.PermissionID)
}

TypeScript SDK (admin):

const rolePerms = await admin.rolePermissions.listByRole('01HXK7A1...' as RoleID)

for (const rp of rolePerms) {
  console.log(`Permission: ${rp.permission_id}`)
}

Generate an API Token

API tokens authenticate programmatic access. The token carries the permissions of the user it belongs to.

curl:

curl -X POST http://localhost:8080/api/v1/tokens \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
    "token_type": "api_key",
    "token": "my-generated-token-value",
    "issued_at": "2026-01-15T10:00:00Z",
    "expires_at": "2027-01-15T10:00:00Z",
    "revoked": false
  }'

Go SDK:

userID := modula.UserID("01HXK4N2F8RJZGP6VTQY3MCSW9")
token, err := client.Tokens.Create(ctx, modula.CreateTokenParams{
    UserID:    &userID,
    TokenType: "api_key",
    Token:     "my-generated-token-value",
    IssuedAt:  "2026-01-15T10:00:00Z",
    ExpiresAt: modula.Timestamp("2027-01-15T10:00:00Z"),
    Revoked:   false,
})
if err != nil {
    // handle error
}

fmt.Printf("Token created: %s\n", token.ID)

TypeScript SDK (admin):

const token = await admin.tokens.create({
  user_id: '01HXK4N2F8RJZGP6VTQY3MCSW9' as UserID,
  token_type: 'api_key',
  token: 'my-generated-token-value',
  issued_at: '2026-01-15T10:00:00Z',
  expires_at: '2027-01-15T10:00:00Z',
  revoked: false,
})

Revoke a token:

_, err = client.Tokens.Update(ctx, modula.UpdateTokenParams{
    ID:        token.ID,
    Token:     token.Token,
    IssuedAt:  token.IssuedAt,
    ExpiresAt: token.ExpiresAt,
    Revoked:   true,
})

Manage SSH Keys

SSH keys authenticate users for TUI access via the built-in SSH server.

Add an SSH Key

curl:

curl -X POST http://localhost:8080/api/v1/ssh-keys \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...", "label": "work laptop"}'

Go SDK:

key, err := client.SSHKeys.Create(ctx, modula.CreateSSHKeyParams{
    PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...",
    Label:     "work laptop",
})
if err != nil {
    // handle error
}

fmt.Printf("Key added: %s (fingerprint: %s)\n", key.Label, key.Fingerprint)

TypeScript SDK (admin):

const key = await admin.sshKeys.create({
  public_key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...',
  label: 'work laptop',
})

console.log(`Key added: ${key.label} (fingerprint: ${key.fingerprint})`)

List SSH Keys

curl:

curl http://localhost:8080/api/v1/ssh-keys \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

keys, err := client.SSHKeys.List(ctx)
if err != nil {
    // handle error
}

for _, k := range keys {
    fmt.Printf("%s  %s  %s  (last used: %s)\n",
        k.SshKeyID, k.KeyType, k.Label, k.LastUsed)
}

TypeScript SDK (admin):

const keys = await admin.sshKeys.list()

for (const k of keys) {
  console.log(`${k.ssh_key_id}  ${k.key_type}  ${k.label}  (last used: ${k.last_used})`)
}

Delete an SSH Key

curl:

curl -X DELETE "http://localhost:8080/api/v1/ssh-keys/01HXK4N2F8RJZGP6VTQY3MCSW9" \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

err := client.SSHKeys.Delete(ctx, modula.UserSshKeyID("01HXK4N2F8RJZGP6VTQY3MCSW9"))

TypeScript SDK (admin):

await admin.sshKeys.remove('01HXK4N2F8RJZGP6VTQY3MCSW9')

List Active Sessions

Sessions are created on login. List them to see which devices/IPs have active sessions for your users.

curl:

curl http://localhost:8080/api/v1/sessions \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

sessions, err := client.Sessions.List(ctx)
if err != nil {
    // handle error
}

for _, s := range sessions {
    ip := ""
    if s.IpAddress != nil {
        ip = *s.IpAddress
    }
    ua := ""
    if s.UserAgent != nil {
        ua = *s.UserAgent
    }
    fmt.Printf("Session %s: IP=%s UA=%s expires=%s\n",
        s.SessionID, ip, ua, s.ExpiresAt)
}

TypeScript SDK (admin):

// List is not exposed; sessions are managed via update and remove
// Use the REST API directly to list sessions

// Invalidate a specific session
await admin.sessions.remove('01HXK9C3...' as SessionID)

Invalidate a Session

curl:

curl -X DELETE "http://localhost:8080/api/v1/sessions/?q=01HXK9C3..." \
  -H "Authorization: Bearer YOUR_API_KEY"

Go SDK:

err := client.Sessions.Remove(ctx, modula.SessionID("01HXK9C3..."))

TypeScript SDK (admin):

await admin.sessions.remove('01HXK9C3...' as SessionID)

Get a User's Full Profile

Retrieve a user with all associated data (OAuth connections, SSH keys, sessions, tokens).

curl:

curl "http://localhost:8080/api/v1/users/full/?q=01HXK4N2F8RJZGP6VTQY3MCSW9" \
  -H "Authorization: Bearer YOUR_API_KEY"

TypeScript SDK (admin):

const fullUser = await admin.users.getFull('01HXK4N2F8RJZGP6VTQY3MCSW9' as UserID)
console.log(`User: ${fullUser.username}, Role: ${fullUser.role}`)

List Users with Role Labels

curl:

curl http://localhost:8080/api/v1/users/full \
  -H "Authorization: Bearer YOUR_API_KEY"

TypeScript SDK (admin):

const users = await admin.users.listFull()

for (const u of users) {
  console.log(`${u.username} (${u.role_label})`)
}

Next Steps