vkovalov-backend API

Authentication API: registration, password login, and two-factor login with authenticator-app codes (TOTP).

Basics

Base URL: https://api.vkovalov.dev

When running the server locally, use http://localhost:8080 instead (or whichever PORT is configured).

Request and response bodies are JSON. Send Content-Type: application/json with every POST.

Every error has the same shape, with the HTTP status code carrying the meaning:

{ "error": "Invalid request payload" }

Protected endpoints need an access token in the Authorization header. The scheme is case-sensitive:

Authorization: Bearer <access_token>

Login flow

  1. Call Login with the email and password.
  2. If the response has "requires_2fa": false, it contains the access_token and you are done.
  3. If it has "requires_2fa": true, it contains a pre_auth_token instead. Ask the user for the 6-digit code from their authenticator app and call Verify 2FA to receive the access_token.
TokenLifetimeAccepted by protected endpoints
access_token24 hoursYes
pre_auth_token5 minutesNo (403)

Both are JWTs signed with HS256. Their payload carries user_id, role, is_2fa and the standard exp, iat, nbf claims. The payload is signed, not encrypted, so a client can read user_id from it.

Register

POST/auth/registerpublic

Creates an account with the role user and returns the stored user. It does not log the user in.

Request body

FieldTypeRequired
emailstringYes
passwordstringYes
first_namestringNo
last_namestringNo

Example

curl -X POST https://api.vkovalov.dev/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"correct horse battery","first_name":"Ada"}'

Response 201

{
  "id": "3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10",
  "email": "ada@example.com",
  "first_name": "Ada",
  "role": "user",
  "is_totp_enabled": false,
  "is_active": true,
  "created_at": "2026-10-02T09:30:00Z",
  "updated_at": "2026-10-02T09:30:00Z"
}

Status codes

201Account created.
400Body is not valid JSON, or email or password is empty.
409An account with this email already exists.
500Unexpected server error.

Login

POST/auth/loginpublic

Checks the email and password. The response has one of two shapes, depending on whether the account has 2FA enabled.

Request body

FieldTypeRequired
emailstringYes
passwordstringYes

Example

curl -X POST https://api.vkovalov.dev/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"correct horse battery"}'

Response 200, 2FA not enabled

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "requires_2fa": false,
  "user": { "id": "3f1c2a9e-...", "email": "ada@example.com", "role": "user", ... }
}

Response 200, 2FA enabled

{
  "requires_2fa": true,
  "pre_auth_token": "eyJhbGciOiJIUzI1NiIs..."
}

Status codes

200Password accepted.
400Body is not valid JSON.
401Unknown email or wrong password. The message is the same for both.
500Unexpected server error. A deactivated account also returns this status.

Verify 2FA

POST/auth/2fa/verifypublic

Second login step for accounts with 2FA enabled. Checks the 6-digit code and returns the access token.

Request body

FieldTypeRequiredNotes
user_idstring (UUID)YesThe user_id claim of the pre_auth_token.
codestringYesCurrent 6-digit code from the authenticator app.

Example

curl -X POST https://api.vkovalov.dev/auth/2fa/verify \
  -H "Content-Type: application/json" \
  -d '{"user_id":"3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10","code":"492817"}'

Response 200

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "requires_2fa": false,
  "user": { "id": "3f1c2a9e-...", "email": "ada@example.com", "role": "user", ... }
}

Status codes

200Code accepted.
400Body is not valid JSON, or user_id is not a UUID.
401Wrong or expired code.
500Unexpected server error. An unknown user_id and an account without 2FA configured also return this status.

Profile

GET/protected/profileaccess token

Confirms that the access token is valid and returns the ID of the user it belongs to.

Example

curl https://api.vkovalov.dev/profile \
  -H "Authorization: Bearer <access_token>"

Response 200

{
  "message": "Access granted to protected route!",
  "user_id": "3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10"
}

Status codes

200Token accepted.
401Header missing, not in the form Bearer <token>, or the token is invalid or expired.
403The token is a pre_auth_token; finish Verify 2FA first.

User object

Returned by Register, and inside the Login and Verify 2FA responses. Fields marked optional are left out when they have no value.

FieldTypeNotes
idstring (UUID)
emailstring
first_namestringOptional.
last_namestringOptional.
avatar_urlstringOptional.
rolestringuser for accounts created through Register.
is_totp_enabledbooleanWhether login requires a 2FA code.
is_activebooleanDeactivated accounts cannot log in.
last_login_atstring (RFC 3339)Optional.
created_atstring (RFC 3339)
updated_atstring (RFC 3339)

The password hash and the 2FA secret are never included in any response.