Skip to main content

Overview

User management actions handle user accounts, team invitations, role assignments, and profile updates. All operations are scoped to the admin’s team for multi-tenant isolation. Actions are located in:
  • src/app/actions/admin.ts - Admin operations
  • src/app/actions/user.ts - Profile operations
  • src/app/actions/auth.ts - Registration

getUsers

Retrieve all users and pending invitations for the admin’s team.
import { getUsers } from '@/app/actions/admin'

const result = await getUsers()
Authorization: Admin role required Response:
success
boolean
Operation status
data
array
Combined array of users and pending invitations, sorted by creation date:User Objects:
  • id: User ID
  • name: Display name
  • email: Email address
  • role: User role
  • status: Account status (“ACTIVE” | “PENDING” | “SUSPENDED”)
  • image: Profile image URL
  • createdAt: Registration date
  • _count.vulnerabilities: Number of vulnerabilities created
  • isInvitation: false
Invitation Objects:
  • id: Invitation ID
  • name: “Pending User”
  • email: Invited email
  • role: Assigned role
  • status: “PENDING”
  • image: null
  • createdAt: Invitation date
  • _count.vulnerabilities: 0
  • isInvitation: true

createUser

Create a new user directly (without invitation flow).
import { createUser } from '@/app/actions/admin'

const result = await createUser({
  name: 'John Doe',
  email: '[email protected]',
  password: 'SecurePass123!',
  role: 'CONTRIBUTOR',
  status: 'ACTIVE'
})
Authorization: Admin role required
name
string
required
User’s display name
email
string
required
User’s email address (must be unique)
password
string
required
Initial password (will be hashed with bcrypt, cost factor 12)
role
'ADMIN' | 'CONTRIBUTOR' | 'VIEWER'
required
User’s role within the team
status
'ACTIVE' | 'SUSPENDED'
required
Account status
Behavior:
  • User is automatically assigned to the admin’s team
  • isOnboarded is set to true (skips onboarding flow)
  • createdById is set to the admin’s user ID
  • Duplicate email returns error
Response:
data
object
The created user object
Errors:
  • “Admin must belong to a team to create users.”
  • “A user with this email already exists”
  • “You must be an admin to create users”

createInvitation

Invite a user to join the team via email.
import { createInvitation } from '@/app/actions/admin'

const result = await createInvitation('[email protected]', 'CONTRIBUTOR')
Authorization: Admin role required
email
string
required
Email address to invite
role
'ADMIN' | 'CONTRIBUTOR' | 'VIEWER'
required
Role to assign when user registers
Behavior:
  1. Checks if email is already registered (returns error if exists)
  2. Deletes any existing invitation for this email (allows re-inviting)
  3. Generates a secure UUID token
  4. Creates invitation with 24-hour expiration
  5. Sends invitation email via Resend
  6. Creates audit log entry
Email Contents:
  • Subject: “You’ve been invited to VulnTrack”
  • Includes invitation link: {APP_URL}/register?token={token}
  • Plain text fallback included
Response:
success
boolean
Whether invitation was created
message
string
“Invitation sent successfully”
data.token
string
The generated invitation token (UUID)
Relative path to registration: /register?token={token}
Token Structure:
{
  id: string, // UUID
  email: string,
  token: string, // UUID
  role: string,
  teamId: string, // Inviter's team
  inviterId: string, // Admin who sent invite
  expiresAt: Date, // 24 hours from creation
  createdAt: Date
}

deleteInvitation

Revoke a pending invitation.
import { deleteInvitation } from '@/app/actions/admin'

const result = await deleteInvitation('invitation_id')
Authorization: Admin role required
invitationId
string
required
ID of the invitation to revoke
Authorization:
  • Verifies admin and invitation belong to the same team
  • Returns “Unauthorized access to invitation” for cross-team attempts
Audit Log: Records: DELETE_INVITATION with email address

updateUser

Update user account details.
import { updateUser } from '@/app/actions/admin'

const result = await updateUser('user_id', {
  name: 'Jane Smith',
  role: 'ADMIN',
  status: 'ACTIVE'
})
Authorization: Admin role required
userId
string
required
ID of user to update
data.name
string
Updated display name
data.role
string
Updated role
data.status
string
Updated account status
Authorization:
  • Admin and target user must be in the same team
  • Returns “Unauthorized access to user” for cross-team attempts
Email updates are intentionally disabled due to authentication implications. To change a user’s email, they must register a new account.

updateUserRole

Update only a user’s role (specialized operation).
import { updateUserRole } from '@/app/actions/admin'

const result = await updateUserRole('user_id', 'ADMIN')
Authorization: Admin role required
userId
string
required
ID of user to update
role
'ADMIN' | 'CONTRIBUTOR' | 'VIEWER'
required
New role
Effect:
  • Updates user’s role immediately
  • User’s JWT is refreshed on next request (no re-login needed)
  • Role change takes effect across all active sessions

deleteUser

Delete a user account.
import { deleteUser } from '@/app/actions/admin'

const result = await deleteUser('user_id')
Authorization: Admin role required
userId
string
required
ID of user to delete
Authorization:
  • Admin and target user must be in the same team
  • Cannot delete users from other teams
Deleting a user may leave orphaned vulnerabilities. Consider reassigning resources before deletion.

updateProfile

Update the current user’s profile (non-admin operation).
import { updateProfile } from '@/app/actions/user'

const result = await updateProfile({
  name: 'John Doe',
  image: 'https://example.com/avatar.jpg'
})
Authorization: Any authenticated user
name
string
required
Updated display name
image
string
Profile image URL
Effect:
  • Updates user’s name and profile image
  • Sets isOnboarded: true (completes onboarding)
  • Revalidates /dashboard and /dashboard/settings
Use Cases:
  • Onboarding flow completion
  • Profile settings page
  • Avatar updates

Invitation Flow

Complete invitation-based registration flow:

1. Admin Creates Invitation

const result = await createInvitation('[email protected]', 'CONTRIBUTOR')
const inviteLink = result.data.link // /register?token=abc-123

2. User Receives Email

Email contains link to registration page with token:
https://vulntrack.com/register?token=abc-123

3. User Registers

import { registerUser } from '@/app/actions/auth'

const result = await registerUser({
  email: '[email protected]', // Must match invitation
  password: 'SecurePass123!',
  name: 'New User',
  token: 'abc-123' // From URL parameter
})

4. System Processing

  • Validates token (checks expiration, email match)
  • Creates user with role from invitation
  • Assigns user to inviter’s team
  • Deletes invitation record
  • Sets createdById to inviter’s ID

5. User Can Login

User is now a team member with assigned role.

Rate Limiting

User creation and invitation actions inherit rate limiting from the registration endpoint:
  • Limit: 5 attempts per minute per IP
  • Window: 60 seconds

Audit Trail

All user management operations are logged:
ActionEvent TypeDetails
Create UserCREATE_USER”User created by [email protected]
Update UserUPDATE_USER”User updated by [email protected]
Update RoleUPDATE_ROLE”Role updated to ADMIN”
Delete UserDELETE_USER”User deleted”
Create InvitationCREATE_INVITATION”Invited [email protected] as CONTRIBUTOR”
Delete InvitationDELETE_INVITATION”Invitation revoked for [email protected]

Example: User Management Dashboard

'use client'

import { useState, useTransition } from 'react'
import {
  getUsers,
  createInvitation,
  deleteUser,
  deleteInvitation,
  updateUserRole
} from '@/app/actions/admin'

export function UserManagement() {
  const [users, setUsers] = useState([])
  const [isPending, startTransition] = useTransition()

  async function loadUsers() {
    const result = await getUsers()
    if (result.success) {
      setUsers(result.data)
    }
  }

  async function handleInvite(email: string, role: string) {
    startTransition(async () => {
      const result = await createInvitation(email, role)
      if (result.success) {
        alert(`Invitation sent! Link: ${result.data.link}`)
        loadUsers() // Refresh to show pending invitation
      } else {
        alert(result.error)
      }
    })
  }

  async function handleRoleChange(userId: string, newRole: string) {
    startTransition(async () => {
      const result = await updateUserRole(userId, newRole)
      if (result.success) {
        loadUsers()
      }
    })
  }

  async function handleDelete(user: any) {
    if (!confirm(`Delete ${user.email}?`)) return

    startTransition(async () => {
      const result = user.isInvitation
        ? await deleteInvitation(user.id)
        : await deleteUser(user.id)

      if (result.success) {
        loadUsers()
      }
    })
  }

  return (
    <div>
      <h1>Team Members</h1>
      <button onClick={() => handleInvite('[email protected]', 'CONTRIBUTOR')}>
        Invite User
      </button>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Role</th>
            <th>Status</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>
                <select
                  value={user.role}
                  onChange={(e) => handleRoleChange(user.id, e.target.value)}
                  disabled={user.isInvitation}
                >
                  <option value="ADMIN">Admin</option>
                  <option value="CONTRIBUTOR">Contributor</option>
                  <option value="VIEWER">Viewer</option>
                </select>
              </td>
              <td>{user.status}</td>
              <td>
                <button onClick={() => handleDelete(user)}>
                  {user.isInvitation ? 'Revoke' : 'Delete'}
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

Next Steps

Authentication

Learn about session management

Teams

Understand team isolation and structure

Build docs developers (and LLMs) love