Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
872 changes: 564 additions & 308 deletions docs/API.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ app.use('/api', routes)
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))

// Health check endpoint
/**
* @openapi
* /health:
* get:
* operationId: healthCheck
* summary: Service health check
* description: Returns HTTP 200 when the server is running. No authentication required.
* tags: [Health]
* security: []
* responses:
* 200:
* description: Service is healthy
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: ok
* timestamp:
* type: string
* format: date-time
*/
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() })
})
Expand Down
67 changes: 55 additions & 12 deletions src/config/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,84 @@
import swaggerJsdoc from 'swagger-jsdoc'


const options: swaggerJsdoc.Options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Learnault API Documentation',
title: 'Learnault API',
version: '1.0.0',
description: 'Comprehensive API documentation for Learnault - a decentralized learn-to-earn platform on Stellar',
description: [
'REST API for Learnault — a decentralized learn-to-earn platform on Stellar.',
'',
'**Base path for all v1 routes:** `/api/v1`',
'',
'**Authentication:** Most routes require a Bearer JWT obtained from `POST /api/v1/auth/login`.',
'Pass it as `Authorization: Bearer <token>`.',
'',
'**Rate-limiting headers** are returned on every response:',
'`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.',
'A `Retry-After` header is added when the limit is exceeded (HTTP 429).',
'',
'**Error envelope** (all 4xx/5xx responses):',
'```json',
'{ "success": false, "error": { "message": "...", "code": 500 } }',
'```',
'',
'> ⚠️ **Preview / not-yet-implemented routes** — `PATCH /api/v1/users/password` and',
'> `PATCH /api/v1/users/wallet` are wired but their underlying service methods are stubs.',
'> They will return errors in production until the service layer is completed.',
].join('\n'),
contact: {
name: 'Learnault Contributors',
url: 'https://github.com/learnault/learnault',
email: 'learnault@toneflix.net',
},
license: {
name: 'MIT',
},
},
servers: [
{
url: '/api',
description: 'Main API base path',
url: '/api/v1',
description: 'Current version (v1)',
},
{
url: 'http://localhost:3000/api/v1',
description: 'Local development',
},
],
tags: [
{ name: 'Health', description: 'Service health check' },
{ name: 'Auth', description: 'Registration, login, email verification, password reset' },
{ name: 'Users', description: 'User profile management' },
{ name: 'Modules', description: 'Learning module catalogue and progress tracking' },
{ name: 'Credentials', description: 'On-chain verifiable credentials' },
{ name: 'Rewards', description: 'XLM balance, transaction history, and withdrawals' },
{ name: 'Referrals', description: 'Referral code generation and bonus tracking' },
{ name: 'Notifications', description: 'Push-notification device tokens and preferences' },
{ name: 'Sync', description: 'Offline progress and completion reconciliation' },
{ name: 'Employer', description: 'B2B talent search and candidate outreach (employer role required)' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT obtained from POST /auth/login',
},
},
// All component schemas are defined via JSDoc in src/docs/schemas.ts
},
security: [
{
bearerAuth: [],
},
],
// Global security — individual operations that are public override this with security: []
security: [{ bearerAuth: [] }],
},
apis: ['./src/controllers/**/*.ts', './src/docs/*.ts'], // Path to the API docs
// Scan controllers (operations) and the dedicated schema file
apis: [
'./src/controllers/**/*.ts',
'./src/routes/**/*.ts',
'./src/docs/*.ts',
'./src/app.ts',
],
}

export const specs = swaggerJsdoc(options)

115 changes: 98 additions & 17 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ export class AuthController {
* @openapi
* /auth/register:
* post:
* operationId: authRegister
* summary: Register a new user
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -91,17 +93,29 @@ export class AuthController {
* $ref: '#/components/schemas/RegisterInput'
* responses:
* 201:
* description: User registered successfully
* description: User registered successfully; a verification email is queued.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AuthResponse'
* 400:
* description: Validation failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 409:
* description: User already exists
* description: Email or username already taken
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async register(req: Request, res: Response): Promise<void> {
try {
Expand Down Expand Up @@ -184,8 +198,10 @@ export class AuthController {
* @openapi
* /auth/verify-email:
* post:
* operationId: authVerifyEmail
* summary: Verify email address with a token
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -194,13 +210,19 @@ export class AuthController {
* $ref: '#/components/schemas/VerifyEmailInput'
* responses:
* 200:
* description: Email verified successfully
* description: Email verified (or already verified)
* 400:
* description: Invalid or malformed token
* 410:
* description: Token expired or already used
* description: Invalid or expired token
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async verifyEmail(req: Request, res: Response): Promise<void> {
try {
Expand Down Expand Up @@ -280,8 +302,13 @@ export class AuthController {
* @openapi
* /auth/resend-verification:
* post:
* operationId: authResendVerification
* summary: Resend verification email
* description: >
* Always returns 200 with a neutral message to avoid leaking user existence.
* Rate-limited by IP (1 req/min) and per account (5 req/24 h).
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -290,11 +317,19 @@ export class AuthController {
* $ref: '#/components/schemas/ResendVerificationInput'
* responses:
* 200:
* description: If the account exists, a verification email will be sent
* description: If the account exists, a verification email will be sent.
* 429:
* description: Too many requests
* description: Too many requests — IP or account rate limit reached.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async resendVerification(req: Request, res: Response): Promise<void> {
try {
Expand Down Expand Up @@ -386,8 +421,11 @@ export class AuthController {
* @openapi
* /auth/login:
* post:
* summary: Login a user
* operationId: authLogin
* summary: Log in and receive a JWT
* description: Rate-limited to 10 requests per 15 minutes per IP.
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -403,10 +441,22 @@ export class AuthController {
* $ref: '#/components/schemas/AuthResponse'
* 400:
* description: Validation failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 401:
* description: Invalid credentials
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async login(req: Request, res: Response): Promise<void> {
try {
Expand Down Expand Up @@ -466,11 +516,16 @@ export class AuthController {
* @openapi
* /auth/logout:
* post:
* summary: Logout user
* operationId: authLogout
* summary: Log out (stateless — client must discard the token)
* description: >
* The server has no session state; this endpoint simply returns a
* reminder to clear the token client-side.
* tags: [Auth]
* security: []
* responses:
* 200:
* description: Logged out successfully
* description: Logged out successfully.
*/
async logout(req: Request, res: Response): Promise<void> {
res.status(200).json({ message: 'Logged out successfully. Please clear your token client-side.' })
Expand All @@ -480,8 +535,13 @@ export class AuthController {
* @openapi
* /auth/forgot-password:
* post:
* summary: Request a password reset email
* operationId: authForgotPassword
* summary: Request a password-reset email
* description: >
* Always returns 200 to avoid leaking user existence.
* Token expires in 30 minutes. Rate-limited by IP and per account.
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -490,11 +550,19 @@ export class AuthController {
* $ref: '#/components/schemas/ForgotPasswordInput'
* responses:
* 200:
* description: If the account exists, a password reset email will be sent
* description: If the account exists, a password reset email will be sent.
* 429:
* description: Too many requests
* description: Too many requests.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async forgotPassword(req: Request, res: Response): Promise<void> {
try {
Expand Down Expand Up @@ -572,8 +640,13 @@ export class AuthController {
* @openapi
* /auth/reset-password:
* post:
* summary: Reset password with a token
* operationId: authResetPassword
* summary: Reset password using a token from the reset email
* description: >
* On success, all existing sessions are revoked and all pending
* verification tokens are cancelled.
* tags: [Auth]
* security: []
* requestBody:
* required: true
* content:
Expand All @@ -582,11 +655,19 @@ export class AuthController {
* $ref: '#/components/schemas/ResetPasswordInput'
* responses:
* 200:
* description: Password reset successful
* description: Password reset successful.
* 400:
* description: Invalid token or password
* description: Invalid, expired, or already-used token; or weak password.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
async resetPassword(req: Request, res: Response): Promise<void> {
try {
Expand Down
Loading
Loading