Complete reference for all REST API endpoints in the Self-Learning Platform.
Base URL: http://localhost:3000 (development)
Authentication: Session-based via iron-session cookies. Endpoints marked Auth: Yes require an active session. Endpoints marked Auth: Admin require an active session with role: "admin".
Content-Type: All request bodies are JSON (application/json). All responses return JSON unless otherwise noted.
- Auth
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/logout
- GET /api/auth/me
- PATCH /api/auth/profile
- PATCH /api/auth/preferences
- GET /api/auth/verify-email
- POST /api/auth/forgot-password
- POST /api/auth/reset-password
- GET /api/auth/oauth/[provider]
- GET /api/auth/oauth/[provider]/callback
- POST /api/auth/totp/setup
- POST /api/auth/totp/verify
- POST /api/auth/totp/disable
- POST /api/auth/passkey/register-options
- POST /api/auth/passkey/register-verify
- POST /api/auth/passkey/auth-options
- POST /api/auth/passkey/auth-verify
- GET /api/auth/passkey/list
- DELETE /api/auth/passkey/[id]
- Modules
- Exercises
- Validation
- Terminal
- Progress
- Settings
- Admin - Settings
- Admin - Users
- Admin - Modules
- Admin - Exercises
- Admin - Audit
Create a new user account.
Auth: No
Rate limited: Yes (by IP)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address |
password |
string | Yes | Minimum 8 characters |
username |
string | No | 2-50 characters; defaults to email prefix |
Response (200):
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"role": "user"
}
}A session cookie is set automatically upon successful registration.
If SMTP is configured, a verification email is sent to the provided address.
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 400 | { "error": "<password policy message>" } |
Password does not meet policy |
| 403 | { "error": "Registration is disabled" } |
Registration disabled in settings |
| 409 | { "error": "Email already registered" } |
Duplicate email |
| 429 | { "error": "Too many attempts. Try again later." } |
Rate limit exceeded |
Authenticate with email and password.
Auth: No
Rate limited: Yes (by IP)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address |
password |
string | Yes | User password |
Response (200) -- Standard login:
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"displayName": "User Name",
"role": "user",
"avatarUrl": null
}
}Response (200) -- TOTP 2FA required:
When the user has TOTP enabled, the login is not yet complete. The client must call /api/auth/totp/verify with isLogin: true.
{
"requires2FA": true,
"tempToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input" } |
Validation failed |
| 401 | { "error": "Invalid email or password" } |
Wrong credentials |
| 403 | { "error": "Account disabled" } |
Account disabled by admin |
| 429 | { "error": "Too many attempts. Try again later." } |
Rate limit exceeded |
Destroy the current session.
Auth: No (works even if already unauthenticated)
Request Body: None
Response (200):
{ "ok": true }Get the currently authenticated user and available OAuth providers.
Auth: No (returns user: null if unauthenticated)
Response (200) -- Authenticated:
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"displayName": "User Name",
"role": "user",
"avatarUrl": null,
"totpEnabled": false,
"preferences": "{\"theme\":\"dark\",\"language\":\"es\"}"
},
"oauthProviders": ["google", "github"]
}Response (200) -- Not authenticated:
{
"user": null,
"oauthProviders": ["google", "github"]
}Update profile information or change password. The endpoint distinguishes between the two operations based on the presence of currentPassword and newPassword fields.
Auth: Yes
Request Body -- Profile update:
| Field | Type | Required | Description |
|---|---|---|---|
displayName |
string | No | 1-100 characters |
username |
string | No | 2-50 characters |
Response (200) -- Profile update:
{ "ok": true }Request Body -- Password change:
| Field | Type | Required | Description |
|---|---|---|---|
currentPassword |
string | Yes | Current password |
newPassword |
string | Yes | Minimum 8 characters |
Response (200) -- Password change:
{ "ok": true, "message": "Password updated" }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input" } |
Validation failed |
| 400 | { "error": "<password policy message>" } |
New password does not meet policy |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 401 | { "error": "Current password is incorrect" } |
Wrong current password |
| 404 | { "error": "User not found" } |
User record missing |
Update user preferences (theme, language). Merges with existing preferences.
Auth: Yes
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
theme |
string | No | "dark" or "light" |
language |
string | No | Language code, 2-5 chars (e.g. "es", "en") |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input" } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
Verify a user's email address via a token link (typically sent via email).
Auth: No
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Email verification token |
Response: Redirects to /login?verified=true on success.
Error Responses (redirects):
| Condition | Redirect |
|---|---|
| Missing token | /login?error=invalid_token |
| Invalid token | /login?error=invalid_token |
| Expired token | /login?error=token_expired |
Request a password reset email. Always returns success to prevent email enumeration.
Auth: No
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address |
Response (200):
{ "success": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 503 | { "error": "Email service not configured" } |
SMTP not configured |
Reset a user's password using a valid reset token.
Auth: No
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Password reset token |
newPassword |
string | Yes | Minimum 8 characters |
Response (200):
{ "success": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 400 | { "error": "<password policy message>" } |
Password does not meet policy |
| 400 | { "error": "Invalid or already used token" } |
Token not found or already used |
| 400 | { "error": "Token expired" } |
Token past expiry (1 hour TTL) |
Initiate an OAuth login flow. Redirects the browser to the provider's authorization URL.
Auth: No
Path Parameters:
| Param | Type | Description |
|---|---|---|
provider |
string | One of: google, github, azure |
Response: 302 redirect to the provider's authorization page.
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "OAuth provider \"<name>\" is not configured" } |
Provider credentials not set |
OAuth callback handler. Exchanges the authorization code for user profile data, creates or links the user account, and establishes a session.
Auth: No
Path Parameters:
| Param | Type | Description |
|---|---|---|
provider |
string | One of: google, github, azure |
Query Parameters (set by the OAuth provider):
| Param | Type | Description |
|---|---|---|
code |
string | Authorization code |
state |
string | CSRF state token |
error |
string | Error code from provider |
Response: 302 redirect to / on success, or to /login?error=<reason> on failure.
Possible error redirect reasons:
oauth_<error>-- Provider returned an erroroauth_missing_params-- Missing code or stateoauth_state_mismatch-- CSRF state mismatchoauth_invalid_state-- HMAC verification failedoauth_exchange_failed-- Token exchange or profile fetch failed
Generate a new TOTP secret and QR code for 2FA setup. The secret is stored in the session until verified.
Auth: Yes
Request Body: None
Response (200):
{
"qrCode": "data:image/png;base64,...",
"secret": "JBSWY3DPEHPK3PXP",
"uri": "otpauth://totp/DevOps%20Learning%20Platform:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=DevOps%20Learning%20Platform"
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "2FA is already enabled" } |
TOTP already active |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 404 | { "error": "User not found" } |
User record missing |
Verify a TOTP code. Used for two flows:
- Setup flow -- Confirm the code from the authenticator app to enable 2FA.
- Login flow -- Complete a login that requires 2FA (when
isLogin: true).
Auth: Depends on flow (session required for setup; pending session for login)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | 6-digit TOTP code |
isLogin |
boolean | No | true for login completion; omit for setup |
Response (200) -- Setup flow:
{ "enabled": true }Response (200) -- Login flow:
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"displayName": "User Name",
"role": "user",
"avatarUrl": null
}
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input" } |
Validation failed |
| 400 | { "error": "No pending 2FA verification" } |
Login flow: no pending user |
| 400 | { "error": "No pending TOTP setup. Call /api/auth/totp/setup first." } |
Setup flow: no pending secret |
| 401 | { "error": "Authentication required" } |
Setup flow: not authenticated |
| 401 | { "error": "Invalid code" } |
Wrong TOTP code |
| 404 | { "error": "User not found" } |
User record missing |
Disable TOTP 2FA. Requires password confirmation.
Auth: Yes
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
password |
string | Yes | Current password |
Response (200):
{ "disabled": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Password required" } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 401 | { "error": "Invalid password" } |
Wrong password |
| 404 | { "error": "User not found" } |
User record missing |
Generate WebAuthn registration options for adding a new passkey.
Auth: Yes
Request Body: None
Response (200):
Returns a WebAuthn PublicKeyCredentialCreationOptions object (structure defined by the WebAuthn spec). The challenge is stored in the session.
{
"challenge": "base64url-encoded-challenge",
"rp": { "name": "DevOps Learning Platform", "id": "localhost" },
"user": { "id": "...", "name": "user@example.com", "displayName": "user" },
"pubKeyCredParams": [...],
"timeout": 60000,
"attestation": "none",
"excludeCredentials": [...]
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 404 | { "error": "User not found" } |
User record missing |
Verify and save a new passkey registration.
Auth: Yes
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
response |
object | Yes | WebAuthn AuthenticatorAttestationResponse |
name |
string | No | Friendly name for the passkey (default: "Passkey") |
Response (200):
{ "verified": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "No pending challenge" } |
No challenge in session |
| 400 | { "error": "Verification failed" } |
WebAuthn verification failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
Generate WebAuthn authentication options for passkey login.
Auth: No
Request Body: None
Response (200):
Returns a WebAuthn PublicKeyCredentialRequestOptions object. The challenge is stored in the session.
{
"challenge": "base64url-encoded-challenge",
"timeout": 60000,
"rpId": "localhost",
"allowCredentials": []
}Verify a passkey authentication response and log the user in.
Auth: No
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
response |
object | Yes | WebAuthn AuthenticatorAssertionResponse |
Response (200):
{
"verified": true,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"displayName": "User Name",
"role": "user",
"avatarUrl": null
}
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "No pending challenge" } |
No challenge in session |
| 400 | { "error": "Passkey not found" } |
Credential ID not in database |
| 400 | { "error": "Verification failed" } |
WebAuthn verification failed |
| 404 | { "error": "User not found" } |
User record missing |
List all passkeys registered by the authenticated user.
Auth: Yes
Response (200):
{
"passkeys": [
{
"id": 1,
"name": "MacBook Touch ID",
"deviceType": "platform",
"createdAt": "2025-01-15T10:30:00.000Z",
"lastUsedAt": "2025-01-20T08:15:00.000Z"
}
]
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
Remove a passkey belonging to the authenticated user.
Auth: Yes
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
integer | Passkey ID |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid ID" } |
Non-numeric ID |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 404 | { "error": "Passkey not found" } |
Not found or belongs to another user |
List all available learning modules.
Auth: No
Response (200):
{
"modules": [
{
"id": "terraform",
"title": "Terraform",
"description": { "es": "Aprende Terraform...", "en": "Learn Terraform..." },
"icon": "Terminal",
"prefix": "tf",
"language": "hcl",
"sortOrder": 0
},
{
"id": "kubernetes",
"title": "Kubernetes",
"description": { "es": "Aprende Kubernetes...", "en": "Learn Kubernetes..." },
"icon": "Container",
"prefix": "k8s",
"language": "yaml",
"sortOrder": 1
}
]
}List all exercises for a specific module with their basic metadata.
Auth: Conditional (required unless demo mode is enabled)
Path Parameters:
| Param | Type | Description |
|---|---|---|
module |
string | Module ID (e.g. terraform) |
Response (200):
{
"module": {
"id": "terraform",
"title": "Terraform",
"description": { "es": "...", "en": "..." },
"icon": "Terminal",
"prefix": "tf",
"language": "hcl",
"sortOrder": 0
},
"exercises": [
{
"id": "tf-01",
"title": "Provider Configuration",
"language": "hcl",
"prerequisites": [],
"sortOrder": 0
}
]
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated and demo mode off |
| 404 | { "error": "Module not found" } |
Invalid module ID |
Get full metadata for a specific exercise (briefing, initial code, hints, etc.).
Auth: No
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Exercise ID (e.g. tf-01) |
Response (200):
{
"id": "tf-01",
"title": "Provider Configuration",
"briefing": "Configure the AWS provider in Terraform...",
"language": "hcl",
"initialCode": "# Configure the AWS provider\n",
"prerequisites": [],
"hints": ["Use the provider block", "Set the region argument"],
"successMessage": "Correct! The AWS provider is configured.",
"i18n": {
"en": {
"title": "Provider Configuration",
"briefing": "Configure the AWS provider...",
"hints": ["Use the provider block", "Set the region argument"],
"successMessage": "Correct!"
}
}
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 404 | { "error": "Exercise not found" } |
Invalid exercise ID |
Submit code for validation against an exercise's rules. Also records submissions and updates progress for authenticated users.
Auth: Conditional (required unless demo mode is enabled)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
exerciseId |
string | Yes | Exercise ID |
code |
string | Yes | User's code submission |
failureCount |
number | No | Number of previous failures (used for hint unlocking; default: 0) |
lang |
string | No | Language code for feedback messages (default: "es") |
Response (200):
{
"passed": true,
"summary": "All validations passed!",
"checks": [
{
"type": "syntax",
"passed": true,
"message": "Valid HCL syntax"
},
{
"type": "semantic",
"passed": true,
"message": "Provider block found"
}
],
"hints": []
}{
"passed": false,
"summary": "Some checks failed",
"checks": [
{
"type": "syntax",
"passed": true,
"message": "Valid HCL syntax"
},
{
"type": "semantic",
"passed": false,
"message": "Missing required provider block"
}
],
"hints": ["Use the provider block to configure AWS"]
}Side effects (authenticated users only):
- A submission record is created in the database
- If passed, user progress is updated to
"completed" - Dependent exercises are unlocked if all their prerequisites are met
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "exerciseId and code are required" } |
Missing required fields |
| 401 | { "error": "Authentication required" } |
Not authenticated and demo mode off |
Simulate a terminal command in the context of an exercise. Commands are not executed on the real system; responses are generated by a simulator based on the exercise definition and the current code.
Auth: No
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
exerciseId |
string | Yes | Exercise ID |
command |
string | Yes | Shell command to simulate |
code |
string | Yes | Current code in the editor |
lang |
string | No | Language code (default: "es") |
Response (200):
{
"output": "Terraform has been successfully initialized!",
"exitCode": 0
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "exerciseId, command, and code are required" } |
Missing required fields |
Get the user's exercise progress for a specific module.
Auth: Conditional (required unless demo mode is enabled)
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
module |
string | No | Module name to filter by (e.g. terraform) |
Response (200):
{
"progress": {
"tf-01": "completed",
"tf-02": "available",
"tf-03": "locked"
}
}Progress statuses:
"available"-- Exercise can be started (prerequisites met or none)"completed"-- Exercise passed"locked"-- Prerequisites not yet met
If no progress records exist for the module, they are initialized automatically (exercises with no prerequisites get "available", others get "locked").
In demo mode without authentication, returns default progress without persisting.
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated and demo mode off |
Get a summary of completed vs total exercises per module.
Auth: No (returns empty data for unauthenticated users)
Response (200):
{
"modules": {
"terraform": { "total": 10, "completed": 3 },
"kubernetes": { "total": 8, "completed": 0 }
}
}Response (200) -- Not authenticated:
{
"modules": {}
}Get public platform settings (no authentication required).
Auth: No
Response (200):
{
"registrationEnabled": true,
"demoMode": false,
"platformTitle": "Self-Learning Platform"
}Get all platform settings with their current values. Secret values (passwords, client secrets) are masked.
Auth: Admin
Response (200):
{
"settings": {
"registration_enabled": "true",
"demo_mode": "false",
"platform_title": "Self-Learning Platform",
"session_ttl": "604800",
"base_url": "http://localhost:3000",
"totp_issuer": "DevOps Learning Platform",
"smtp_host": "",
"smtp_port": "587",
"smtp_user": "",
"smtp_pass": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
"smtp_from": "noreply@devopslab.local",
"smtp_secure": "false",
"oauth_google_client_id": "",
"oauth_google_client_secret": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
"oauth_github_client_id": "",
"oauth_github_client_secret": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
"oauth_azure_client_id": "",
"oauth_azure_client_secret": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
"oauth_azure_tenant": "common"
}
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Update a single platform setting.
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Setting key (see allowed keys below) |
value |
string | Yes | New value |
Allowed keys: registration_enabled, demo_mode, platform_title, session_ttl, base_url, totp_issuer, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from, smtp_secure, oauth_google_client_id, oauth_google_client_secret, oauth_github_client_id, oauth_github_client_secret, oauth_azure_client_id, oauth_azure_client_secret, oauth_azure_tenant
Response (200):
{ "ok": true }If a masked value placeholder is sent for a secret key, the update is silently skipped (existing value preserved).
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input" } |
Validation failed |
| 400 | { "error": "Unknown setting" } |
Key not in allowed list |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
List all users with pagination and search.
Auth: Admin
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
search |
string | "" |
Filter by email or username (case-insensitive) |
page |
number | 1 |
Page number (1-based) |
limit |
number | 50 |
Results per page (1-100) |
Response (200):
{
"users": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@example.com",
"username": "admin",
"displayName": "Admin User",
"role": "admin",
"createdAt": "2025-01-01T00:00:00.000Z",
"totpEnabled": true,
"emailVerified": true,
"disabled": false
}
],
"total": 1,
"page": 1,
"limit": 50
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Create a new user account (admin-initiated).
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address |
password |
string | Yes | Minimum 8 characters |
username |
string | No | 2-50 characters |
role |
string | No | "admin" or "user" (default: "user") |
Response (200):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "newuser@example.com",
"role": "user"
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 409 | { "error": "Email already registered" } |
Duplicate email |
Get details for a specific user.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | User UUID |
Response (200):
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "user",
"displayName": "User Name",
"role": "user",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z",
"totpEnabled": false,
"emailVerified": true,
"disabled": false,
"avatarUrl": null
}
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "User not found" } |
Invalid user ID |
Update a user's details (admin-initiated). All fields are optional.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | User UUID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | No | "admin" or "user" |
displayName |
string | No | 1-100 characters |
username |
string | No | 2-50 characters |
password |
string | No | Minimum 8 characters (reset password) |
disabled |
boolean | No | Disable/enable the account |
emailVerified |
boolean | No | Manually verify/unverify email |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "User not found" } |
Invalid user ID |
Delete a user account. An admin cannot delete their own account.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | User UUID |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Cannot delete your own account" } |
Self-deletion attempt |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "User not found" } |
Invalid user ID |
Resend the email verification link for a user. Requires SMTP to be configured.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | User UUID |
Request Body: None
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "SMTP not configured" } |
SMTP settings missing |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "User not found" } |
Invalid user ID or no email |
List all modules ordered by sort order.
Auth: Admin
Response (200):
{
"modules": [
{
"id": "terraform",
"title": "Terraform",
"description": { "es": "Aprende Terraform...", "en": "Learn Terraform..." },
"icon": "Terminal",
"prefix": "tf",
"language": "hcl",
"sortOrder": 0,
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Create a new learning module.
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | URL-safe identifier (/^[a-z0-9-]+$/, max 50 chars) |
title |
string | Yes | Display title (max 100 chars) |
description |
Record<string, string> |
Yes | Per-language descriptions (e.g. { "es": "...", "en": "..." }) |
icon |
string | No | Lucide icon name (default: "Terminal") |
prefix |
string | Yes | Exercise ID prefix (max 10 chars) |
language |
string | No | Default code language (default: "yaml") |
sortOrder |
number | No | Display order (default: 0) |
Response (200):
{ "id": "ansible", "title": "Ansible" }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Get a single module by ID.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Module ID |
Response (200):
{
"id": "terraform",
"title": "Terraform",
"description": { "es": "...", "en": "..." },
"icon": "Terminal",
"prefix": "tf",
"language": "hcl",
"sortOrder": 0,
"createdAt": "2025-01-01T00:00:00.000Z"
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "Not found" } |
Invalid module ID |
Update a module. All fields are optional.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Module ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | Display title (max 100 chars) |
description |
Record<string, string> |
No | Per-language descriptions |
icon |
string | No | Lucide icon name |
prefix |
string | No | Exercise ID prefix (max 10 chars) |
language |
string | No | Default code language |
showDifficulty |
boolean | No | Show difficulty badges on exercises |
image |
string | null | No | Module image (base64, max ~700KB) |
sortOrder |
number | No | Display order |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "Not found" } |
Invalid module ID |
Delete a module and all its exercises.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Module ID |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Reorder modules by providing an ordered array of module IDs.
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
moduleIds |
string[] | Yes | Ordered array of module IDs |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
List exercises with optional module filtering and pagination.
Auth: Admin
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
module |
string | -- | Filter by module ID |
page |
number | 1 |
Page number (1-based) |
limit |
number | 50 |
Results per page (1-100) |
Response (200):
{
"exercises": [
{
"id": "tf-01",
"moduleId": "terraform",
"title": "Provider Configuration",
"language": "hcl",
"sortOrder": 0,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": null
}
],
"total": 10,
"page": 1,
"limit": 50,
"totalPages": 1
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Create a new exercise.
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique exercise ID (max 100 chars) |
moduleId |
string | Yes | Parent module ID (must exist) |
title |
string | Yes | Exercise title |
briefing |
string | Yes | Exercise description/instructions |
language |
string | Yes | Code language (e.g. "hcl", "yaml") |
initialCode |
string | Yes | Starting code template |
prerequisites |
string[] | No | Array of prerequisite exercise IDs (default: []) |
hints |
string[] | No | Progressive hints (default: []) |
successMessage |
string | Yes | Message shown on completion |
validations |
array | Yes | Validation rules (see below) |
terminalCommands |
object | Yes | Terminal command simulation rules (see below) |
i18n |
object | No | Per-language overrides |
sortOrder |
number | No | Display order (default: 0) |
Validation rule structure:
{
"type": "syntax | semantic | intention",
"errorMessage": "Displayed when check fails",
"check": {
"contains": "string to find",
"not_contains": "string that must be absent",
"match": "regex pattern",
"not_match": "regex that must not match",
"yaml_valid": true,
"yaml_has": "path.to.key",
"yaml_not_has": "path.to.key",
"yaml_is_array": "path.to.key",
"yaml_equals": { "path": "path.to.key", "value": "expected" },
"yaml_items_have": { "path": "path.to.array", "fields": ["field1", "field2"] },
"custom": "custom validator name",
"all": [ /* nested checks, all must pass */ ],
"any": [ /* nested checks, at least one must pass */ ],
"not": { /* nested check, must fail */ }
},
"failMessage": "Detailed failure explanation"
}Terminal command structure:
{
"terraform init": [
{
"when": { "contains": "provider" },
"output": "Terraform has been successfully initialized!",
"exitCode": 0
}
]
}Response (200):
{ "id": "tf-01", "title": "Provider Configuration" }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 400 | { "error": "Module not found" } |
Referenced module does not exist |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Get the full definition of a single exercise, including parsed JSON fields.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Exercise ID |
Response (200):
{
"id": "tf-01",
"moduleId": "terraform",
"title": "Provider Configuration",
"briefing": "Configure the AWS provider...",
"language": "hcl",
"initialCode": "# Configure the provider\n",
"prerequisites": [],
"hints": ["Use the provider block"],
"successMessage": "Correct!",
"validations": [ { "type": "syntax", "errorMessage": "...", "check": { "contains": "provider" }, "failMessage": "..." } ],
"terminalCommands": { "terraform init": [{ "output": "...", "exitCode": 0 }] },
"i18n": null,
"sortOrder": 0,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": null
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "Not found" } |
Invalid exercise ID |
Update an exercise. All fields are optional.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Exercise ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | Exercise title |
briefing |
string | No | Exercise instructions |
language |
string | No | Code language |
initialCode |
string | No | Starting code template |
prerequisites |
string[] | No | Prerequisite exercise IDs |
hints |
string[] | No | Progressive hints |
successMessage |
string | No | Completion message |
validations |
array | No | Validation rules |
terminalCommands |
object | No | Terminal simulation rules |
i18n |
object | null | No | Per-language overrides (null to remove) |
difficulty |
string | null | No | "easy", "medium", "hard", or null |
sortOrder |
number | No | Display order |
moduleId |
string | No | Move exercise to a different module |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
| 404 | { "error": "Not found" } |
Invalid exercise ID |
Delete an exercise.
Auth: Admin
Path Parameters:
| Param | Type | Description |
|---|---|---|
id |
string | Exercise ID |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Reorder exercises within a module by providing an ordered array of exercise IDs.
Auth: Admin
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
moduleId |
string | Yes | Module ID containing the exercises |
exerciseIds |
string[] | Yes | Ordered array of exercise IDs |
Response (200):
{ "ok": true }Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | { "error": "Invalid input", "details": {...} } |
Validation failed |
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |
Query the audit log with optional action filtering and pagination.
Auth: Admin
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
action |
string | "" |
Filter by action type (partial match) |
page |
number | 1 |
Page number (1-based) |
limit |
number | 50 |
Results per page (1-100) |
Common action types: login, login_failed, logout, register, password_change, totp_enable, totp_disable, passkey_register, passkey_remove, admin_user_edit, admin_user_delete, admin_resend_verification, oauth_link
Response (200):
{
"entries": [
{
"id": 1,
"userId": "550e8400-e29b-41d4-a716-446655440000",
"action": "login",
"details": "{\"method\":\"totp\"}",
"ip": "127.0.0.1",
"userAgent": "Mozilla/5.0 ...",
"createdAt": "2025-01-15T10:30:00.000Z"
}
],
"total": 150,
"page": 1,
"limit": 50
}Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | { "error": "Authentication required" } |
Not authenticated |
| 403 | { "error": "Forbidden" } |
Not an admin |