LMS-BGN/docs/api/openapi.yaml

865 lines
27 KiB
YAML

openapi: 3.0.3
info:
title: LMS-BGN API — Draft Skeleton
version: 0.1.0-draft
license:
name: MIT
url: https://opensource.org/licenses/MIT
description: Non-implementasi skeleton untuk stories Epik 1 & 2.
x-status: drafted
x-contract-principles:
align_to_frontend_models: true
sources:
- path: src/app/course/[courseId]/page.tsx
note: Course & Module fields
- path: src/app/exam-session/page.tsx
note: Exam Session UI shape
- path: src/app/exam-summary/page.tsx
note: Exam Summary UI shape
- path: src/app/exams/page.tsx
note: Exams listing UI
servers:
- url: https://api.lms-bgn.dev
description: Draft dev server
tags:
- name: Courses
description: Operasi daftar dan detail kursus
- name: Modules
description: Progres dan ringkasan modul
- name: Assignments
description: Pengumpulan tugas dan respons
- name: Exams
description: Sesi ujian, penskoran, dan ringkasan
- name: Certificates
description: Sertifikat dan pengiriman ulang email
security:
- bearerAuth: []
paths:
/api/courses:
get:
tags: [Courses]
summary: List Courses
operationId: listCourses
x-story-id: 1-1-course-catalog-read-api
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: q
in: query
schema:
type: string
- name: category
in: query
schema:
type: string
- name: level
in: query
schema:
type: string
- name: sortBy
in: query
schema:
type: string
enum: [updatedAt, title]
- name: order
in: query
schema:
type: string
enum: [asc, desc]
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CourseListResponse'
example:
page: 1
pageSize: 20
total: 2
items:
- id: "c1"
title: "Basic Knife Skills"
instructor: "Chef A"
progress: 30
status: "in_progress"
color: "#FF9800"
thumbnail: "/images/knife-skills.jpg"
duration: "6h"
modules: 12
enrolled: "1.2k"
- id: "c2"
title: "Bread Baking 101"
instructor: "Chef B"
progress: 0
status: "not_started"
color: "#3F51B5"
thumbnail: "/images/bread.jpg"
duration: "4h"
modules: 8
enrolled: "860"
"400":
description: Invalid query parameters
/api/courses/{courseId}:
get:
tags: [Courses]
summary: Get Course Detail
operationId: getCourseDetail
x-story-id: 1-2-course-detail-read-api
parameters:
- name: courseId
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CourseDetailResponse'
example:
id: "c1"
title: "Basic Knife Skills"
description: "Learn core knife handling techniques."
instructor: "Chef A"
duration: "6h"
totalModules: 12
completedModules: 3
progress: 25
thumbnail: "/images/knife-skills.jpg"
color: "#FF9800"
modules:
- id: "m1"
title: "Intro"
description: "Overview"
duration: "15m"
isUnlocked: true
isCompleted: false
progress: 0
type: "reading"
hasQuiz: false
hasAssignment: false
"404":
description: Not Found
/api/modules/{moduleId}/progress:
post:
tags: [Modules]
summary: Update Module Progress
operationId: updateModuleProgress
x-story-id: 1-3-module-progress-update-api
parameters:
- name: moduleId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ModuleProgressUpdateRequest'
responses:
"202":
description: Accepted
content:
application/json:
schema:
$ref: '#/components/schemas/ModuleProgressUpdateResponse'
"400":
description: Invalid request
/api/assignments/{assignmentId}/submission:
post:
tags: [Assignments]
summary: Persist Assignment Submission
operationId: persistAssignmentSubmission
x-story-id: 1-4-assignment-submission-persist
parameters:
- name: assignmentId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AssignmentSubmissionRequest'
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/AssignmentSubmissionResponse'
"400":
description: Invalid request
/api/exam-session:
post:
tags: [Exams]
summary: Create Exam Session
operationId: createExamSession
x-story-id: 2-1-exam-session-create-api
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExamSessionCreateRequest'
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/ExamSessionCreateResponse'
example:
sessionId: "sess-001"
status: "created"
startTime: "2024-01-26T09:00:00Z"
"400":
description: Invalid request
"409":
description: Conflict (duplicate idempotency key)
"429":
description: Too Many Requests
/api/exam-session/{sessionId}/score:
post:
tags: [Exams]
summary: Score Exam Session
operationId: scoreExamSession
x-story-id: 2-2-exam-scoring-api
parameters:
- name: sessionId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExamScoreRequest'
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ExamScoreResponse'
example:
totalScore: 85
totalQuestions: 10
correctAnswers: 8
scorePercent: 85
perQuestion:
- questionId: "q1"
isCorrect: true
earnedPoints: 10
maxPoints: 10
- questionId: "q2"
isCorrect: false
earnedPoints: 0
maxPoints: 10
"400":
description: Invalid request
"404":
description: Session not found
"409":
description: Conflict (duplicate idempotency key)
"429":
description: Too Many Requests
/api/exam-session/{sessionId}/summary:
get:
tags: [Exams]
summary: Read Exam Summary
operationId: readExamSummary
x-story-id: 2-3-exam-summary-read-api
parameters:
- name: sessionId
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ExamSummaryResponse'
example:
examId: "exam-001"
examTitle: "Kuis Interaktif - Keamanan Dapur BGN"
totalQuestions: 10
correctAnswers: 8
score: 85
timeSpent: "25 menit 30 detik"
completedAt: "2024-01-26T09:30:00Z"
answers:
- questionId: "q1"
question: "Apa langkah pertama memastikan kebersihan dapur?"
userAnswer: "Mencuci tangan sebelum mulai"
correctAnswer: "Mencuci tangan sebelum mulai"
isCorrect: true
type: "enhanced_mcq"
- questionId: "q2"
question: "Pada suhu berapa ayam harus dimasak?"
userAnswer: "60°C"
correctAnswer: "75°C"
isCorrect: false
type: "enhanced_mcq"
"400":
description: Invalid request
/api/certificates/{certificateId}/resend:
post:
tags: [Certificates]
summary: Resend Certificate Email (Stub)
operationId: resendCertificateEmail
x-story-id: 2-4-certificate-resend-email-stub
parameters:
- name: certificateId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateResendRequest'
responses:
"202":
description: Accepted
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateResendResponse'
"400":
description: Invalid request
/api/exams:
get:
tags: [Exams]
summary: List Exams
operationId: listExams
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: q
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
enum: [upcoming, available, completed, missed]
- name: type
in: query
schema:
type: string
- name: dateFrom
in: query
schema:
type: string
format: date
- name: dateTo
in: query
schema:
type: string
format: date
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ExamListResponse'
example:
page: 1
pageSize: 20
total: 5
items:
- id: 1
title: "Kuis Interaktif - Keamanan Dapur BGN"
course: "Keamanan Pangan dan Gizi - Badan Gizi Nasional"
type: "Interactive Quiz"
date: "2024-01-26"
startTime: "09:00"
endTime: "10:00"
duration: 60
questions: 10
status: "available"
attempts: 0
maxAttempts: 3
passingScore: 70
description: "Kuis interaktif tentang keamanan pangan dan praktik dapur yang baik di lingkungan BGN"
instructions: ["Kuis ini menggunakan berbagai jenis soal interaktif", "Termasuk drag & drop, simulasi dapur, dan skenario nyata", "Baca instruksi setiap soal dengan teliti"]
isInteractive: true
- id: 2
title: "Ujian Tengah Semester - HACCP"
course: "HACCP - Hazard Analysis Critical Control Point"
type: "Midterm Exam"
date: "2024-01-28"
startTime: "10:00"
endTime: "12:00"
duration: 120
questions: 50
status: "upcoming"
attempts: 0
maxAttempts: 1
passingScore: 75
description: "Ujian tengah semester mencakup materi HACCP dari modul 1-6"
instructions: ["Pastikan koneksi internet stabil", "Siapkan alat tulis", "Baca soal dengan teliti"]
"400":
description: Invalid query parameters
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
CourseSummary:
type: object
properties:
id: { type: string }
title: { type: string }
instructor: { type: string }
progress: { type: integer }
status: { type: string }
color: { type: string }
thumbnail: { type: string }
duration: { type: string }
modules: { type: integer }
enrolled: { type: string }
CourseListResponse:
type: object
properties:
page: { type: integer }
pageSize: { type: integer }
total: { type: integer }
items:
type: array
items:
$ref: '#/components/schemas/CourseSummary'
ModuleSummary:
type: object
properties:
id: { type: string }
title: { type: string }
description: { type: string }
duration: { type: string }
isUnlocked: { type: boolean }
isCompleted: { type: boolean }
progress: { type: integer }
type: { type: string, enum: [video, reading, quiz, assignment] }
hasQuiz: { type: boolean }
hasAssignment: { type: boolean }
CourseDetailResponse:
type: object
properties:
id: { type: string }
title: { type: string }
description: { type: string }
instructor: { type: string }
duration: { type: string }
totalModules: { type: integer }
completedModules: { type: integer }
progress: { type: integer }
thumbnail: { type: string }
color: { type: string }
modules:
type: array
items:
$ref: '#/components/schemas/ModuleSummary'
ModuleProgressUpdateRequest:
type: object
properties:
progressPercent: { type: integer, minimum: 0, maximum: 100 }
idempotencyKey: { type: string }
updatedAt: { type: string, format: date-time }
ModuleProgressUpdateResponse:
type: object
properties:
status: { type: string, enum: [accepted] }
AssignmentSubmissionRequest:
type: object
properties:
content: { type: string }
attachments:
type: array
items:
type: string
idempotencyKey: { type: string }
AssignmentSubmissionResponse:
type: object
properties:
submissionId: { type: string }
status: { type: string, enum: [created] }
ExamSessionCreateRequest:
type: object
properties:
examId: { type: string }
userId: { type: string }
isInteractive: { type: boolean }
idempotencyKey: { type: string }
clientContext:
type: object
properties:
device: { type: string }
appVersion: { type: string }
locale: { type: string }
ExamSessionCreateResponse:
type: object
properties:
sessionId: { type: string }
status: { type: string, enum: [created] }
startTime: { type: string, format: date-time }
ExamScoreRequest:
type: object
properties:
mode: { type: string, enum: [batch, stream] }
idempotencyKey: { type: string }
answers:
type: array
items:
oneOf:
- $ref: '#/components/schemas/EnhancedMcqAnswer'
- $ref: '#/components/schemas/VideoScenarioAnswer'
- $ref: '#/components/schemas/ImageHotspotAnswer'
- $ref: '#/components/schemas/MediaGalleryAnswer'
- $ref: '#/components/schemas/PuzzleAnswer'
- $ref: '#/components/schemas/ScenarioAnswer'
example:
mode: batch
idempotencyKey: score-sess-001
answers:
- questionId: mcq-1
type: enhanced_mcq
choiceIndex: 2
confidence: 4
- questionId: video-1
type: video_scenario
scenarioAnswers:
- stepId: s1
selectedOptionId: optA
- stepId: s2
selectedOptionId: optB
- questionId: hotspot-1
type: image_hotspot
selectedHotspots: [spot1, spot3]
- questionId: gallery-1
type: media_gallery
viewedItems: [itemA, itemB, itemC]
- questionId: puzzle-1
type: puzzle
matches:
- pieceId: p1
targetId: t1
- pieceId: p2
targetId: t2
- questionId: scenario-1
type: scenario
selectedOptionId: optX
ExamScoreResponse:
type: object
properties:
totalScore: { type: number }
totalQuestions: { type: integer }
correctAnswers: { type: integer }
scorePercent: { type: number }
perQuestion:
type: array
items:
type: object
properties:
questionId: { type: string }
isCorrect: { type: boolean }
earnedPoints: { type: number }
maxPoints: { type: number }
# Interactive Answer union items
EnhancedMcqAnswer:
type: object
required: [questionId, type, choiceIndex]
properties:
questionId: { type: string }
type: { type: string, enum: [enhanced_mcq] }
choiceIndex: { type: integer }
confidence: { type: integer, minimum: 0, maximum: 5 }
VideoScenarioAnswer:
type: object
required: [questionId, type, scenarioAnswers]
properties:
questionId: { type: string }
type: { type: string, enum: [video_scenario] }
scenarioAnswers:
type: array
items:
type: object
required: [stepId, selectedOptionId]
properties:
stepId: { type: string }
selectedOptionId: { type: string }
ImageHotspotAnswer:
type: object
required: [questionId, type, selectedHotspots]
properties:
questionId: { type: string }
type: { type: string, enum: [image_hotspot] }
selectedHotspots:
type: array
items: { type: string }
MediaGalleryAnswer:
type: object
required: [questionId, type, viewedItems]
properties:
questionId: { type: string }
type: { type: string, enum: [media_gallery] }
viewedItems:
type: array
items: { type: string }
PuzzleAnswer:
type: object
required: [questionId, type, matches]
properties:
questionId: { type: string }
type: { type: string, enum: [puzzle] }
matches:
type: array
items:
type: object
required: [pieceId, targetId]
properties:
pieceId: { type: string }
targetId: { type: string }
ScenarioAnswer:
type: object
required: [questionId, type, selectedOptionId]
properties:
questionId: { type: string }
type: { type: string, enum: [scenario] }
selectedOptionId: { type: string }
# Interactive Summary details union items
EnhancedMcqDetails:
type: object
properties:
choiceIndex: { type: integer }
confidence: { type: integer, minimum: 0, maximum: 5 }
VideoScenarioDetails:
type: object
properties:
steps:
type: array
items:
type: object
properties:
stepId: { type: string }
selectedOptionId: { type: string }
isCorrect: { type: boolean }
earnedPoints: { type: number }
ImageHotspotDetails:
type: object
properties:
selectedHotspots:
type: array
items: { type: string }
correctHotspots:
type: array
items: { type: string }
MediaGalleryDetails:
type: object
properties:
viewedItems:
type: array
items: { type: string }
requiredItems:
type: array
items: { type: string }
completionRate: { type: number }
PuzzleDetails:
type: object
properties:
matches:
type: array
items:
type: object
properties:
pieceId: { type: string }
targetId: { type: string }
correctPairs: { type: integer }
earnedPoints: { type: number }
ScenarioDetails:
type: object
properties:
selectedOptionId: { type: string }
isCorrect: { type: boolean }
ExamSummaryResponse:
type: object
properties:
examId: { type: string }
examTitle: { type: string }
totalQuestions: { type: integer }
correctAnswers: { type: integer }
score: { type: number }
timeSpent: { type: string }
completedAt: { type: string, format: date-time }
answers:
type: array
items:
type: object
properties:
questionId: { type: string }
question: { type: string }
userAnswer: { type: string }
correctAnswer: { type: string }
isCorrect: { type: boolean }
type: { type: string, enum: [enhanced_mcq, video_scenario, image_hotspot, media_gallery, puzzle, scenario] }
details:
oneOf:
- $ref: '#/components/schemas/EnhancedMcqDetails'
- $ref: '#/components/schemas/VideoScenarioDetails'
- $ref: '#/components/schemas/ImageHotspotDetails'
- $ref: '#/components/schemas/MediaGalleryDetails'
- $ref: '#/components/schemas/PuzzleDetails'
- $ref: '#/components/schemas/ScenarioDetails'
example:
examId: interactive-dapur-mbg-001
examTitle: Kuis Interaktif Dapur MBG
totalQuestions: 10
correctAnswers: 8
score: 85
timeSpent: "25 menit 30 detik"
completedAt: "2025-11-12T08:30:00Z"
answers:
- questionId: "1"
question: "Apa langkah pertama dalam memastikan keamanan pangan di Dapur MBG?"
userAnswer: "Melakukan analisis bahaya"
correctAnswer: "Melakukan analisis bahaya"
isCorrect: true
type: enhanced_mcq
details:
choiceIndex: 0
confidence: 4
- questionId: "video-scenario-1"
question: "Analisis Situasi Dapur: Identifikasi Masalah Keamanan Pangan"
userAnswer: "step1: opt4; step2: opt4"
correctAnswer: "step1: opt4; step2: opt4"
isCorrect: true
type: video_scenario
details:
steps:
- stepId: step1
selectedOptionId: opt4
isCorrect: true
earnedPoints: 10
- stepId: step2
selectedOptionId: opt4
isCorrect: true
earnedPoints: 10
- questionId: "hotspot-scenario-1"
question: "Identifikasi Area Bermasalah dalam Tata Letak Dapur"
userAnswer: "hotspot1,hotspot2,hotspot4"
correctAnswer: "hotspot1,hotspot2,hotspot3,hotspot4,hotspot5"
isCorrect: false
type: image_hotspot
details:
selectedHotspots: [hotspot1, hotspot2, hotspot4]
correctHotspots: [hotspot1, hotspot2, hotspot3, hotspot4, hotspot5]
- questionId: "3"
question: "Cocokkan istilah keamanan pangan dengan definisinya"
userAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4"
correctAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4"
isCorrect: true
type: puzzle
details:
matches:
- pieceId: ccp
targetId: def1
- pieceId: haccp
targetId: def2
correctPairs: 4
earnedPoints: 10
CertificateResendRequest:
type: object
properties:
recipientEmail: { type: string, format: email }
idempotencyKey: { type: string }
CertificateResendResponse:
type: object
properties:
jobId: { type: string }
status: { type: string, enum: [accepted, throttled] }
ExamListItem:
type: object
properties:
id: { type: integer }
title: { type: string }
course: { type: string }
type: { type: string }
date: { type: string, format: date }
startTime: { type: string }
endTime: { type: string }
duration: { type: integer }
questions: { type: integer }
status: { type: string, enum: [upcoming, available, completed, missed] }
attempts: { type: integer }
maxAttempts: { type: integer }
passingScore: { type: integer }
description: { type: string }
instructions:
type: array
items: { type: string }
isInteractive: { type: boolean }
score: { type: integer }
grade: { type: string }
ExamListResponse:
type: object
properties:
page: { type: integer }
pageSize: { type: integer }
total: { type: integer }
items:
type: array
items:
$ref: '#/components/schemas/ExamListItem'