ASP.NET Core 8 · REST + SignalR · JWT + SessionToken · OpenAPI / Swagger
- Genel Kurallar
- Kimlik Doğrulama
- Auth Endpointleri
- Kod Çalıştırma
- Quiz Yönetimi
- Soru Yönetimi
- Quiz Katılımı
- Session ve Replay
- Admin Endpointleri
- SignalR Hub
- Hata Kodları
- Pagination
- Rate Limit Özeti
Geliştirme: http://localhost:5000/api
Production: https://<domain>/api
- Content-Type:
application/json - Zaman damgaları: ISO 8601, UTC (
2025-05-10T14:30:00Z) - UUID formatı: standart (
3fa85f64-5717-4562-b3fc-2c963f66afa6)
| Seviye | Açıklama | Nasıl gönderilir |
|---|---|---|
| Public | Kimlik doğrulama gerekmez | — |
| User | Kayıtlı kullanıcı (User veya Admin rolü) | Authorization: Bearer <jwt> |
| Admin | Yalnızca Admin rolü | Authorization: Bearer <jwt> |
| SessionToken | Anonim quiz katılımcısı | X-Session-Token: <uuid> |
| Owner | Quiz sahibi veya Admin | Authorization: Bearer <jwt> |
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...X-Session-Token: 3fa85f64-5717-4562-b3fc-2c963f66afa6Session token geçerlilik koşulları:
quiz_sessions.is_active = truequiz_sessions.is_locked = falsequiz_sessions.ends_at > now()
Erişim: Public
İstek:
{
"displayName": "Ahmet Yılmaz",
"email": "ahmet@example.com",
"password": "Guclu@2025!",
"passwordConfirm": "Guclu@2025!"
}Validasyon:
displayName: 2–100 karakter, zorunluemail: geçerli format, benzersiz, zorunlupassword: min 8 karakter, büyük/küçük harf + rakam + özel karakterpasswordConfirm:passwordile aynı
Başarı (201 Created):
{
"id": "uuid",
"email": "ahmet@example.com",
"displayName": "Ahmet Yılmaz",
"role": "User"
}Erişim: Public
İstek:
{
"email": "ahmet@example.com",
"password": "Guclu@2025!"
}Başarı (200 OK):
{
"accessToken": "eyJ...",
"refreshToken": "d9f3e2...",
"expiresIn": 900,
"user": {
"id": "uuid",
"email": "ahmet@example.com",
"displayName": "Ahmet Yılmaz",
"role": "User"
}
}Hata (401): Hatalı kimlik bilgisi
Erişim: Public
İstek:
{ "refreshToken": "d9f3e2..." }Başarı (200 OK):
{
"accessToken": "eyJ...",
"refreshToken": "a7b1c3...",
"expiresIn": 900
}Hata (401): Geçersiz veya süresi dolmuş refresh token
Erişim: User / Admin
Başarı (200 OK):
{
"id": "uuid",
"email": "ahmet@example.com",
"displayName": "Ahmet Yılmaz",
"role": "User",
"createdAt": "2025-04-27T10:00:00Z"
}Erişim: Public (IP bazlı rate limit: 10/dk)
İstek:
{
"language": "python",
"code": "print('Hello, World!')",
"stdin": ""
}language değerleri: "python" | "javascript" | "cpp"
Başarı (202 Accepted):
{
"jobId": "uuid"
}Job kuyruğa alındı; GET /api/execute/:jobId ile sonuç polling yapılır.
Erişim: Public
Başarı (200 OK) – Tamamlandı:
{
"jobId": "uuid",
"status": "Passed",
"stdout": "Hello, World!\n",
"stderr": "",
"executionTimeMs": 42,
"memoryUsedKb": 8192
}Başarı (200 OK) – Devam ediyor:
{
"jobId": "uuid",
"status": "Running"
}status değerleri: Pending | Running | Passed | Failed | Error | TLE
Polling Stratejisi (Frontend):
- 500ms interval
- Max 30 saniye (60 deneme)
status∈ {Passed,Failed,Error,TLE} → dur
Erişim: User (kendi quizleri) / Admin (tüm quizler)
Query Params:
status:Draft|Active|Endedpage: sayfa (default 1)pageSize: kayıt sayısı (default 20)
Başarı (200 OK):
{
"data": [
{
"id": "uuid",
"title": "Python Temelleri",
"description": "...",
"durationMinutes": 60,
"mode": "RealTime",
"status": "Active",
"participantCount": 12,
"questionCount": 5,
"createdAt": "2025-05-01T10:00:00Z",
"publishedAt": "2025-05-02T08:00:00Z"
}
],
"pagination": { "page": 1, "pageSize": 20, "total": 3, "totalPages": 1 }
}Erişim: User / Admin
İstek:
{
"title": "Python Temelleri Sınavı",
"description": "Opsiyonel açıklama",
"durationMinutes": 60,
"mode": "RealTime",
"antiCheatOptions": {
"tabSwitch": true,
"fullscreen": true,
"clipboard": false
},
"formSchema": [
{ "key": "name", "label": "Ad Soyad", "type": "text", "required": true },
{ "key": "studentId", "label": "Öğrenci Numarası", "type": "text", "required": true }
],
"accessCode": null
}Başarı (201 Created):
{
"id": "uuid",
"title": "Python Temelleri Sınavı",
"status": "Draft",
...
}Erişim: Owner / Admin
Başarı (200 OK): Quiz detayı (tüm alanlar, soru sayısı dahil)
Erişim: Owner / Admin
Not: status: "Active" quizde yalnızca description ve antiCheatOptions güncellenebilir.
İstek: POST /api/quizzes ile aynı şema.
Başarı (200 OK): Güncellenmiş quiz
Erişim: Owner / Admin
Not: status: "Active" quiz silinemez (409 Conflict).
Başarı (204 No Content)
Erişim: Owner / Admin
Koşullar:
- Quiz
Draftdurumunda olmalı - En az 1 soru eklenmiş olmalı
Başarı (200 OK):
{
"id": "uuid",
"status": "Active",
"publishedAt": "2025-05-02T08:00:00Z",
"quizUrl": "/q/uuid"
}Erişim: Owner / Admin
Başarı (200 OK): status: "Ended", tüm aktif session'lar kilitlenir.
Erişim: Owner / Admin
Başarı (200 OK):
[
{
"id": "uuid",
"type": "Coding",
"title": "Fibonacci",
"body": "n. Fibonacci sayısını döndüren fonksiyonu yazın.",
"points": 20,
"orderNo": 1,
"options": { "starterCode": "def fib(n):\n pass", "supportedLanguages": ["python"] },
"testCaseCount": 5,
"visibleTestCaseCount": 2
}
]Erişim: Owner / Admin
İstek (Coding sorusu):
{
"type": "Coding",
"title": "Fibonacci",
"body": "n. Fibonacci sayısını döndüren fonksiyonu yazın.",
"points": 20,
"options": {
"starterCode": "def fib(n):\n pass",
"supportedLanguages": ["python", "javascript"]
},
"testCases": [
{ "input": "5", "expectedOutput": "5", "isVisible": true },
{ "input": "10", "expectedOutput": "55", "isVisible": true },
{ "input": "20", "expectedOutput": "6765", "isVisible": false }
]
}İstek (Multiple Choice sorusu):
{
"type": "MultipleChoice",
"title": "Python list slice",
"body": "x = [0,1,2,3,4] → x[1:3] sonucu nedir?",
"points": 5,
"options": {
"choices": ["[1, 2]", "[1, 2, 3]", "[0, 1, 2]", "[2, 3]"],
"correctIndices": [0],
"multiSelect": false
}
}Başarı (201 Created): Oluşturulan soru
Erişim: Owner / Admin
İstek: Güncellenecek alanlar (partial update desteklenir)
Başarı (200 OK): Güncellenmiş soru
Erişim: Owner / Admin
Başarı (204 No Content)
Erişim: Owner / Admin
İstek:
{ "orderNo": 3 }Başarı (200 OK)
Erişim: Owner / Admin
İstek:
{
"testCases": [
{ "input": "0", "expectedOutput": "0", "isVisible": false },
{ "input": "1", "expectedOutput": "1", "isVisible": false }
]
}Başarı (201 Created): Oluşturulan test case'ler
Erişim: Public
Başarı (200 OK):
{
"id": "uuid",
"title": "Python Temelleri Sınavı",
"description": "...",
"durationMinutes": 60,
"questionCount": 5,
"status": "Active",
"antiCheatOptions": {
"tabSwitch": true,
"fullscreen": true,
"clipboard": false
},
"formSchema": [
{ "key": "name", "label": "Ad Soyad", "type": "text", "required": true },
{ "key": "studentId", "label": "Öğrenci Numarası", "type": "text", "required": true }
]
}Hata (404): Quiz bulunamadı
Hata (409): Quiz Draft veya Ended durumunda → "Quiz aktif değil"
Erişim: Public (IP bazlı rate limit: 5/dk)
İstek:
{
"formData": {
"name": "Ayşe Demir",
"studentId": "20230099"
}
}Backend işlemleri:
- Quiz
Activemi kontrol et formData'yıformSchema'ya göre doğrula (required alanlar, tip uyumu)- UUID
sessionTokenüret QuizSessionoluştur:ends_at = now() + duration_minutes- Response dön
Başarı (200 OK):
{
"sessionToken": "uuid",
"sessionId": "uuid",
"quizId": "uuid",
"endsAt": "2025-05-10T15:30:00Z",
"questions": [
{
"id": "uuid",
"type": "Coding",
"title": "Fibonacci",
"body": "...",
"points": 20,
"orderNo": 1,
"options": { "starterCode": "...", "supportedLanguages": ["python"] },
"testCases": [
{ "id": "uuid", "input": "5", "expectedOutput": "5" },
{ "id": "uuid", "input": "10", "expectedOutput": "55" }
]
}
]
}Not: Yalnızca isVisible: true test case'ler döner. Hidden test case'ler gizlenir.
Erişim: SessionToken
İstek:
{
"questionId": "uuid",
"language": "python",
"code": "def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)"
}Backend işlemleri:
- Session geçerli ve kilitli değil mi kontrol et
ends_at > now()kontrol et- Aynı soruya önceki submission varsa
version++ - Coding sorusu → execution job enqueue, diğer tipler → anında değerlendir
- Submission kaydı oluştur
Başarı (202 Accepted) – Coding:
{
"submissionId": "uuid",
"jobId": "uuid",
"status": "Pending"
}Başarı (200 OK) – Diğer tipler:
{
"submissionId": "uuid",
"status": "Passed",
"score": 10,
"feedback": null
}Erişim: SessionToken
Başarı (200 OK):
{
"submissionId": "uuid",
"status": "Passed",
"executionTimeMs": 87,
"memoryUsedKb": 4096,
"score": 20,
"testCaseResults": [
{ "input": "5", "expectedOutput": "5", "actualOutput": "5", "passed": true },
{ "input": "10", "expectedOutput": "55", "actualOutput": "55", "passed": true }
]
}Erişim: SessionToken (rate limit: 60/dk)
İstek:
{
"eventType": "TabSwitch",
"metadata": {
"fromTitle": "CodExam",
"toTitle": "Google"
}
}Başarı (204 No Content)
Otomatik severity mapping:
| eventType | severity |
|---|---|
TabSwitch |
High |
FullscreenExit |
High |
ClipboardAttempt |
Medium |
Keydown |
Low |
Erişim: SessionToken
Başarı (200 OK):
{
"sessionId": "uuid",
"finishedAt": "2025-05-10T15:25:00Z",
"totalScore": 45,
"maxScore": 60,
"completedQuestions": 4,
"totalQuestions": 5
}Erişim: Owner / Admin
Query Params: page, pageSize, active (bool)
Başarı (200 OK):
{
"data": [
{
"sessionId": "uuid",
"formData": { "name": "Ayşe Demir", "studentId": "20230099" },
"startedAt": "2025-05-10T14:00:00Z",
"endsAt": "2025-05-10T15:00:00Z",
"finishedAt": "2025-05-10T14:45:00Z",
"isActive": false,
"totalScore": 45,
"eventCount": 2
}
],
"pagination": { ... }
}Erişim: Owner / Admin
Kullanım: Monitor sayfası ilk yüklendiğinde
Başarı (200 OK): Aktif session listesi (yukarıdaki format)
Erişim: Owner / Admin
Başarı (200 OK):
{
"quizId": "uuid",
"totalParticipants": 15,
"averageScore": 38.5,
"maxScore": 58,
"minScore": 10,
"sessions": [
{
"sessionId": "uuid",
"participantName": "Ayşe Demir",
"formData": { ... },
"totalScore": 45,
"maxPossibleScore": 60,
"completedQuestions": 4,
"submittedAt": "2025-05-10T14:45:00Z",
"durationSeconds": 2700,
"eventCount": 2
}
],
"questionStats": [
{
"questionId": "uuid",
"title": "Fibonacci",
"successRate": 0.73,
"averageScore": 14.6
}
]
}Erişim: Owner / Admin
Başarı (200 OK): Session detayı + submission listesi + anti-cheat event listesi
{
"sessionId": "uuid",
"formData": { ... },
"submissions": [
{
"questionId": "uuid",
"questionTitle": "Fibonacci",
"language": "python",
"code": "def fib(n): ...",
"status": "Passed",
"score": 20,
"executionTimeMs": 87,
"version": 2,
"submittedAt": "..."
}
],
"examEvents": [
{
"eventType": "TabSwitch",
"severity": "High",
"timestamp": "2025-05-10T14:22:00Z",
"metadata": { ... }
}
]
}Erişim: Owner / Admin
Başarı (200 OK):
{
"sessionId": "uuid",
"questionReplays": [
{
"questionId": "uuid",
"questionTitle": "Fibonacci",
"submissionId": "uuid",
"startedAt": "2025-05-10T14:05:00Z",
"diffs": [
{ "time_ms": 0, "type": "snapshot", "code": "def fib(n):\n pass" },
{ "time_ms": 1200, "type": "delta", "diff": "..." },
{ "time_ms": 5000, "type": "snapshot", "code": "..." }
],
"examEvents": [
{ "eventType": "TabSwitch", "timestamp": "...", "timeMsFromStart": 12000 }
]
}
]
}Erişim: SessionToken
Kullanım: Frontend her 5 saniyede toplu diff gönderir
İstek:
{
"diffs": [
{ "time_ms": 1200, "type": "delta", "diff": "..." },
{ "time_ms": 2400, "type": "delta", "diff": "..." }
]
}Başarı (204 No Content)
Erişim: Admin
Başarı (200 OK):
{
"totalUsers": 42,
"activeQuizzes": 3,
"dailyExecutions": 156,
"errorsLast24h": 2,
"activeSessions": 7
}Erişim: Admin
Query Params: q (arama), role, status, page, pageSize
Başarı (200 OK):
{
"data": [
{
"id": "uuid",
"email": "user@example.com",
"displayName": "...",
"role": "User",
"status": "active",
"createdAt": "...",
"lastLoginAt": "..."
}
],
"pagination": { ... }
}Erişim: Admin
İstek:
{
"role": "Admin",
"status": "inactive"
}Başarı (200 OK): Güncellenmiş kullanıcı
Erişim: Admin (soft delete → deleted_at set edilir)
Başarı (204 No Content)
Erişim: Admin
Query Params: status, ownerId, page, pageSize
Başarı (200 OK): Tüm quizler, sahip bilgisi ve katılımcı sayısıyla
Erişim: Admin
Başarı (204 No Content)
Erişim: Admin
Query Params: active (bool), quizId, page, pageSize
Başarı (200 OK): Tüm session'lar
Erişim: Admin
Etki: is_locked = true, is_active = false → SignalR ile katılımcıya monitor.terminate gönderilir
Başarı (204 No Content)
Erişim: Admin
Query Params: source (API | Worker | Nginx), from, to, page, pageSize
Başarı (200 OK):
{
"data": [
{
"id": "uuid",
"sourceService": "API",
"errorTitle": "NullReferenceException",
"errorMessage": "...",
"createdAt": "..."
}
],
"pagination": { ... }
}URL: /hubs/monitor
| Kullanıcı | Yöntem | Parametre |
|---|---|---|
| Kayıtlı kullanıcı (User/Admin) | JWT Bearer | access_token query param veya Authorization header |
| Anonim katılımcı | Session Token | token query param (session UUID) |
// Kayıtlı kullanıcı
new HubConnectionBuilder()
.withUrl("/hubs/monitor?access_token=" + jwtToken)
// Anonim katılımcı
new HubConnectionBuilder()
.withUrl("/hubs/monitor?token=" + sessionToken)| Metod | Parametreler | Erişim | Açıklama |
|---|---|---|---|
CodeChanged |
questionId: string, code: string |
SessionToken | Kod değişikliği |
Heartbeat |
— | SessionToken | Canlılık sinyali (10sn) |
WarnParticipant |
targetSessionId: string, message: string |
Owner/Admin | Katılımcıya uyarı |
TerminateParticipant |
targetSessionId: string |
Owner/Admin | Katılımcıyı düşür |
| Event | Alıcı | Veri | Açıklama |
|---|---|---|---|
session.joined |
Monitor | { sessionId, formData } |
Yeni katılımcı bağlandı |
session.codeChanged |
Monitor | { sessionId, questionId, code } |
Kod güncellendi |
session.heartbeat |
Monitor | { sessionId, timestamp } |
Canlılık |
session.event |
Monitor | { sessionId, eventType, severity, timestamp } |
Anti-cheat event |
session.disconnected |
Monitor | { sessionId } |
Bağlantı kesildi |
monitor.warn |
Katılımcı | { message } |
Quiz sahibinden uyarı |
monitor.terminate |
Katılımcı | — | Sınavdan düşürme |
| Grup Adı | Üyeler | Alınan Eventler |
|---|---|---|
quiz:{quizId}:monitor |
Quiz sahibi, Admin | session.* |
session:{sessionId} |
Katılımcı | monitor.* |
| HTTP | Kod | Açıklama |
|---|---|---|
| 400 | validation_error |
İstek validasyonu başarısız |
| 401 | unauthorized |
Token yok veya geçersiz |
| 403 | forbidden |
Yetkisiz işlem |
| 404 | not_found |
Kaynak bulunamadı |
| 409 | conflict |
İşlem mevcut durumla çakışıyor |
| 410 | session_expired |
Session süresi dolmuş |
| 423 | session_locked |
Session kilitlenmiş (terminate) |
| 429 | rate_limit_exceeded |
Çok fazla istek |
| 500 | internal_error |
Sunucu hatası |
Hata Yanıt Formatı:
{
"type": "validation_error",
"title": "Doğrulama hatası",
"status": 400,
"errors": {
"title": ["Başlık zorunludur."],
"durationMinutes": ["Süre 0'dan büyük olmalıdır."]
},
"traceId": "0HMVD6CPCE1IT:00000001"
}Tüm liste endpoint'leri aynı sayfalama yapısını kullanır:
Query Params:
page: sayfa numarası (default:1, min:1)pageSize: sayfa başına kayıt (default:20, min:1, max:100)
Response Şablonu:
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 87,
"totalPages": 5
}
}| Endpoint | Limit | Pencere | 429 Yanıtı |
|---|---|---|---|
POST /api/auth/login |
10 istek | 1 dk | Retry-After: 60 |
POST /api/auth/register |
5 istek | 1 dk | Retry-After: 60 |
POST /api/execute |
10 istek | 1 dk | Retry-After: 60 |
POST /api/quizzes/:id/join |
5 istek | 1 dk | Retry-After: 60 |
POST /api/quizzes/:id/submit |
30 istek | 1 dk | Retry-After: 60 |
POST /api/quizzes/:id/event |
60 istek | 1 dk | Retry-After: 60 |
Limit aşıldığında:
{
"type": "rate_limit_exceeded",
"message": "Çok fazla istek gönderildi. 1 dakika bekleyin.",
"retryAfter": 60
}