Skip to content

Authentication & RBAC

JWT Authentication

Planner uses JSON Web Tokens for stateless authentication:

  • Access token: 15-minute lifetime, sent in Authorization: Bearer <token> header
  • Refresh token: 7-day lifetime, used to obtain new access tokens

Authentication Flow

  1. POST /api/auth/register — create account (firstName, lastName, email, password)
  2. POST /api/auth/verify-email — verify email with token from email
  3. POST /api/auth/login — get access + refresh tokens (requires verified email)
  4. All subsequent requests include Authorization: Bearer <access_token>
  5. POST /api/auth/refresh — exchange refresh token for new access token
  6. POST /api/auth/logout — revoke all refresh tokens

Email Verification

New users must verify their email before first login:

  • POST /api/auth/register — creates user, sends verification email (does NOT return tokens)
  • POST /api/auth/verify-email — validates token, marks email as verified
  • POST /api/auth/resend-verification — resends verification email (rate limited: 3/min)
  • POST /api/auth/login and POST /api/auth/refresh — throw EMAIL_NOT_VERIFIED if email not verified
  • Existing users are backfilled via seed (prisma/seed.ts sets emailVerifiedAt = now())

Rate Limiting

Per-endpoint rate limits protect against brute-force attacks:

EndpointLimit
Register5 requests/minute
Login10 requests/minute
Refresh20 requests/minute
Forgot-password3 requests/minute
Reset-password5 requests/minute
Verify-email5 requests/minute
Resend-verification3 requests/minute

Global rate limit: 60 requests/minute (stored in Redis in production).

Account Lockout

After 5 failed login attempts, the account is locked for 15 minutes via Redis (lockout:<email> key). Generic error messages (INVALID_CREDENTIALS) prevent user enumeration.

Password Policy

  • Minimum 8 characters, maximum 128
  • Requires: uppercase letter, lowercase letter, digit, special character
  • Hashed with bcrypt (cost factor 10)
  • Password change invalidates all existing refresh tokens
  • Password reset also invalidates all refresh tokens

Refresh Token Rotation

  • Refresh tokens stored as SHA-256 hash (not plaintext)
  • On each refresh: old token deleted, new token issued (rotation)
  • Reuse detection: if a rotated token is presented again, ALL tokens for that user are deleted and a warning is logged
  • POST /api/auth/logout deletes all refresh tokens for the user

Social Auth

In addition to email/password, login via external providers is supported:

ProviderPathDescription
Yandex ID/api/auth/social/yandexLogin via Yandex
VK ID/api/auth/social/vkLogin via VK

Flow: frontend opens a popup window to the provider URL → after successful auth, the provider redirects to callback with tokens in URL hash → popup sends tokens to parent window via postMessage → parent window closes popup and stores tokens.

Token Payload

json
{
  "sub": "uuid",
  "email": "user@example.com",
  "jti": "random-uuid",
  "iat": 1234567890,
  "exp": 1234568790
}

Role-Based Access Control (RBAC)

The RBAC system provides granular permissions at multiple levels.

Permission Scopes

ScopeLevelExample Controller
GLOBALSystem-wideAdmin panel, user management
ORGANIZATIONPer organizationOrg settings, member management
TEAMPer teamTeam settings, member management
PROJECTPer projectProject settings, task management
TASKPer taskTask CRUD (controlled by project scope)
CHATChat levelConversation management

Available Permissions (30+)

Organization (org:): create, view, edit, delete, members.view, members.invite, members.remove, members.roles, settings.view, settings.edit

Team (team:): create, view, edit, delete, members.view, members.add, members.remove, members.roles, settings.view, settings.edit

Project (project:): create, view, edit, delete, members.view, members.add, members.remove, task.create, task.view, task.edit, task.delete, task.assign, settings.view, settings.edit

Built-in Roles

RoleLevelDescription
OWNEROrg/Team/ProjectFull control, can delete
ADMINOrg/Team/ProjectUser and settings management
MANAGEROrg/Team/ProjectOperational management
MEMBEROrg/Team/ProjectBasic access

Custom roles can be created with any combination of permissions.

Permission Resolution

Permissions are computed from all user roles:

effective_permissions = union(
  global_roles.permissions,
  org_roles.permissions,
  team_roles.permissions,
  project_roles.permissions
)

Permission Checks in Code

Backend (NestJS):

typescript
@Permissions(['task.create', 'task.edit'])
@Controller('/projects/:projectId/tasks')

Frontend (Pinia store):

typescript
const auth = useAuthStore()
// auth.hasPermission — computed, auto-unwrapped in templates
if (auth.hasPermission('task.create')) { ... }

User Groups

User groups allow bulk role assignment. Create a group with a set of permissions, then add users to the group. All group members inherit the group's permissions.