491 lines
8.1 KiB
Markdown
491 lines
8.1 KiB
Markdown
# MetaCourse API — Frontend Integration Guide
|
||
|
||
> Target stack: Angular + Capacitor
|
||
> Base URL (local): `http://localhost:8080`
|
||
> Base URL (production): `http://romaric-thibault.fr`
|
||
|
||
---
|
||
|
||
## Deployment
|
||
|
||
### Requirements
|
||
- Docker + Docker Compose installed on the server
|
||
|
||
### Start the stack
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
The API will be available on port **8080**. SQL Server runs on port **1433** (internal). Migrations run automatically on startup.
|
||
|
||
### Swagger UI
|
||
```
|
||
http://<host>:8080/swagger
|
||
```
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
There is **no JWT** currently. Login returns user info directly. Store `UserId` in local storage / Capacitor Preferences to identify the current user across requests.
|
||
|
||
---
|
||
|
||
## Data Models (TypeScript interfaces)
|
||
|
||
```typescript
|
||
// Enums
|
||
type CourseStatus = 'Draft' | 'Published';
|
||
type ResourceType = 'Url' | 'Video' | 'Text' | 'File';
|
||
|
||
interface User {
|
||
id: string; // Guid
|
||
name: string;
|
||
email: string;
|
||
createdAt: string; // ISO date
|
||
}
|
||
|
||
interface LoginResponse {
|
||
userId: string;
|
||
name: string;
|
||
email: string;
|
||
}
|
||
|
||
interface Course {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
status: CourseStatus;
|
||
creatorId: string;
|
||
creatorName: string;
|
||
topicCount: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
interface CourseDetails extends Course {
|
||
topics: Topic[];
|
||
}
|
||
|
||
interface Topic {
|
||
id: string;
|
||
title: string;
|
||
description?: string;
|
||
position: number;
|
||
courseId: string;
|
||
resources: Resource[];
|
||
}
|
||
|
||
interface Resource {
|
||
id: string;
|
||
type: ResourceType;
|
||
title: string;
|
||
content: string; // URL, video URL, text body, or file path
|
||
createdAt: string;
|
||
}
|
||
|
||
interface Enrollment {
|
||
userId: string;
|
||
courseId: string;
|
||
courseTitle: string;
|
||
enrolledAt: string;
|
||
completedAt?: string;
|
||
}
|
||
|
||
interface CourseProgress {
|
||
courseId: string;
|
||
userId: string;
|
||
totalTopics: number;
|
||
completedTopics: number;
|
||
totalResources: number;
|
||
completedResources: number;
|
||
progressPercentage: number; // 0.0 - 100.0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## API Endpoints
|
||
|
||
### Users
|
||
|
||
#### Register
|
||
```
|
||
POST /api/users/register
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"name": "Alice",
|
||
"email": "alice@example.com",
|
||
"password": "Password1"
|
||
}
|
||
```
|
||
**Validation:** name 2–100 chars, valid email, password ≥8 chars + 1 uppercase + 1 digit
|
||
**Response:** `201` → `User`
|
||
**Errors:** `400` validation | `409` email already taken
|
||
|
||
---
|
||
|
||
#### Login
|
||
```
|
||
POST /api/users/login
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"email": "alice@example.com",
|
||
"password": "Password1"
|
||
}
|
||
```
|
||
**Response:** `200` → `LoginResponse`
|
||
**Errors:** `401` wrong credentials
|
||
|
||
---
|
||
|
||
#### Get User Profile
|
||
```
|
||
GET /api/users/{id}
|
||
```
|
||
**Response:** `200` → `User` | `404`
|
||
|
||
---
|
||
|
||
### Courses
|
||
|
||
#### List Published Courses
|
||
```
|
||
GET /api/courses
|
||
```
|
||
Optional query param: `?search=angular`
|
||
**Response:** `200` → `Course[]` (only Published courses, newest first)
|
||
|
||
---
|
||
|
||
#### Get Course with Topics & Resources
|
||
```
|
||
GET /api/courses/{id}
|
||
```
|
||
**Response:** `200` → `CourseDetails` | `404`
|
||
|
||
---
|
||
|
||
#### Create Course
|
||
```
|
||
POST /api/courses
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"title": "Introduction to Angular",
|
||
"description": "Learn Angular from scratch",
|
||
"creatorId": "<userId>"
|
||
}
|
||
```
|
||
**Response:** `201` → `Course` (status = `Draft`)
|
||
|
||
---
|
||
|
||
#### Update Course
|
||
```
|
||
PUT /api/courses/{id}
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"id": "<courseId>",
|
||
"title": "Updated Title",
|
||
"description": "Updated description"
|
||
}
|
||
```
|
||
**Response:** `200` → `Course` | `404`
|
||
|
||
---
|
||
|
||
#### Publish Course
|
||
```
|
||
PATCH /api/courses/{id}/publish
|
||
```
|
||
**Response:** `200` → `Course` | `422` if no topics yet | `404`
|
||
|
||
---
|
||
|
||
#### Delete Course
|
||
```
|
||
DELETE /api/courses/{id}
|
||
```
|
||
**Response:** `204` | `409` if course has enrollments | `404`
|
||
|
||
---
|
||
|
||
#### Courses Created by a User
|
||
```
|
||
GET /api/users/{userId}/courses
|
||
```
|
||
**Response:** `200` → `Course[]`
|
||
|
||
---
|
||
|
||
### Topics
|
||
|
||
#### Get Topic
|
||
```
|
||
GET /api/topics/{id}
|
||
```
|
||
**Response:** `200` → `Topic` (includes resources ordered by position) | `404`
|
||
|
||
---
|
||
|
||
#### Create Topic
|
||
```
|
||
POST /api/topics
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"courseId": "<courseId>",
|
||
"title": "Components",
|
||
"description": "Optional description",
|
||
"position": 1
|
||
}
|
||
```
|
||
**Response:** `201` → `Topic`
|
||
|
||
---
|
||
|
||
#### Update Topic
|
||
```
|
||
PUT /api/topics/{id}
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"id": "<topicId>",
|
||
"title": "Updated Title",
|
||
"description": "Updated",
|
||
"position": 2
|
||
}
|
||
```
|
||
**Response:** `200` → `Topic` | `404`
|
||
|
||
---
|
||
|
||
#### Delete Topic
|
||
```
|
||
DELETE /api/topics/{id}
|
||
```
|
||
**Response:** `204` | `404`
|
||
|
||
---
|
||
|
||
#### Link Resource to Topic
|
||
```
|
||
POST /api/topics/{topicId}/resources/{resourceId}
|
||
```
|
||
**Body:**
|
||
```json
|
||
{ "position": 1 }
|
||
```
|
||
**Response:** `204`
|
||
|
||
---
|
||
|
||
#### Unlink Resource from Topic
|
||
```
|
||
DELETE /api/topics/{topicId}/resources/{resourceId}
|
||
```
|
||
**Response:** `204`
|
||
|
||
---
|
||
|
||
### Resources
|
||
|
||
#### List All Resources
|
||
```
|
||
GET /api/resources
|
||
```
|
||
**Response:** `200` → `Resource[]` (ordered by creation date desc)
|
||
|
||
---
|
||
|
||
#### Get Resource
|
||
```
|
||
GET /api/resources/{id}
|
||
```
|
||
**Response:** `200` → `Resource` | `404`
|
||
|
||
---
|
||
|
||
#### Create Resource
|
||
```
|
||
POST /api/resources
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"type": "Url",
|
||
"title": "Angular Docs",
|
||
"content": "https://angular.io"
|
||
}
|
||
```
|
||
**Types & content:**
|
||
| Type | content field |
|
||
|------|--------------|
|
||
| `Url` | Full URL (https required) |
|
||
| `Video` | Video URL (https required) |
|
||
| `Text` | Markdown or plain text body |
|
||
| `File` | File path or download URL |
|
||
|
||
**Response:** `201` → `Resource`
|
||
|
||
---
|
||
|
||
#### Update Resource
|
||
```
|
||
PUT /api/resources/{id}
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"id": "<resourceId>",
|
||
"type": "Video",
|
||
"title": "Updated",
|
||
"content": "https://youtube.com/..."
|
||
}
|
||
```
|
||
**Response:** `200` → `Resource` | `404`
|
||
|
||
---
|
||
|
||
#### Delete Resource
|
||
```
|
||
DELETE /api/resources/{id}
|
||
```
|
||
**Response:** `204` | `404`
|
||
|
||
---
|
||
|
||
### Enrollments
|
||
|
||
#### Enroll in a Course
|
||
```
|
||
POST /api/courses/{courseId}/enroll
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"userId": "<userId>",
|
||
"courseId": "<courseId>"
|
||
}
|
||
```
|
||
**Response:** `201` → `Enrollment`
|
||
**Errors:** `409` already enrolled | `422` course not Published | `404`
|
||
|
||
---
|
||
|
||
#### User's Enrollments
|
||
```
|
||
GET /api/users/{userId}/enrollments
|
||
```
|
||
**Response:** `200` → `Enrollment[]`
|
||
|
||
---
|
||
|
||
### Progress
|
||
|
||
#### Mark Topic Progress
|
||
```
|
||
POST /api/topics/{topicId}/progress
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"userId": "<userId>",
|
||
"topicId": "<topicId>",
|
||
"completed": true
|
||
}
|
||
```
|
||
**Response:** `204` (upsert — safe to call multiple times)
|
||
|
||
---
|
||
|
||
#### Mark Resource Progress
|
||
```
|
||
POST /api/resources/{resourceId}/progress
|
||
```
|
||
**Body:**
|
||
```json
|
||
{
|
||
"userId": "<userId>",
|
||
"resourceId": "<resourceId>",
|
||
"completed": true
|
||
}
|
||
```
|
||
**Response:** `204` (upsert)
|
||
|
||
---
|
||
|
||
#### Get Course Progress
|
||
```
|
||
GET /api/courses/{courseId}/progress?userId={userId}
|
||
```
|
||
**Response:** `200` → `CourseProgress`
|
||
```json
|
||
{
|
||
"courseId": "...",
|
||
"userId": "...",
|
||
"totalTopics": 5,
|
||
"completedTopics": 3,
|
||
"totalResources": 10,
|
||
"completedResources": 7,
|
||
"progressPercentage": 66.67
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Error Response Format
|
||
|
||
All validation and business errors follow this shape:
|
||
```json
|
||
{
|
||
"statusCode": 400,
|
||
"errors": {
|
||
"email": ["'Email' is not a valid email address."],
|
||
"password": ["Password must be at least 8 characters."]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## CORS
|
||
|
||
All origins, methods, and headers are allowed (`AllowAll` policy). No special headers needed from Angular/Capacitor.
|
||
|
||
---
|
||
|
||
## Typical User Flow (Angular/Capacitor)
|
||
|
||
```
|
||
1. POST /api/users/register → store userId in Capacitor Preferences
|
||
2. POST /api/users/login → verify credentials
|
||
3. GET /api/courses → show published course catalog
|
||
4. GET /api/courses/{id} → show course detail with topics
|
||
5. POST /api/courses/{id}/enroll → enroll user
|
||
6. GET /api/users/{id}/enrollments → show My Courses
|
||
7. POST /api/topics/{id}/progress → mark topic done
|
||
8. POST /api/resources/{id}/progress → mark resource done
|
||
9. GET /api/courses/{id}/progress?userId=... → show progress bar
|
||
```
|
||
|
||
---
|
||
|
||
## Notes for Angular Service Layer
|
||
|
||
- All IDs are **UUIDs (string)** — no integer IDs.
|
||
- Dates are **ISO 8601 UTC strings** — use `new Date(dateString)` or a pipe.
|
||
- `progressPercentage` is a `number` (float 0–100), round for display.
|
||
- `status` and `type` come back as **strings**, not numbers.
|
||
- No auth headers needed currently — all endpoints are `AllowAnonymous`.
|