Grafikarsa API Documentation: Base URL
Grafikarsa API Documentation: Base URL
Overview
API untuk platform Grafikarsa - Katalog Portofolio & Social Network Warga
SMKN 4 Malang.
Tech Stack
• Backend: Golang + Gofiber
• Database: PostgreSQL
• Authentication: JWT (Dual Token - Access + Refresh)
• Object Storage: MinIO (Presigned URL Strategy)
Authentication
Sistem menggunakan JWT Dual Token Strategy: - Access Token: Short-
lived (15 menit), dikirim via Authorization header - Refresh Token: Long-lived
(7 hari), disimpan di HttpOnly cookie
Format Authorization Header:
Authorization: Bearer <access_token>
Response Format
Semua response menggunakan format JSON dengan struktur konsisten.
Success Response (Single Resource):
{
"data": {
"id": "uuid",
"field": "value"
}
}
Success Response (Collection):
{
"data": [
{ "id": "uuid1" },
{ "id": "uuid2" }
],
"meta": {
1
"current_page": 1,
"per_page": 20,
"total_pages": 5,
"total_count": 100
}
}
Error Response:
{
"error": {
"code": "ERROR_CODE",
"message": "Human readable error message",
"details": [
{
"field": "field_name",
"message": "Field specific error"
}
]
}
}
Code Description
200 OK - Request berhasil
201 Created - Resource berhasil dibuat
204 No Content - Request berhasil tanpa response body
400 Bad Request - Request tidak valid
401 Unauthorized - Authentication diperlukan atau token invalid
403 Forbidden - Tidak memiliki akses
404 Not Found - Resource tidak ditemukan
409 Conflict - Konflik data (duplicate, dll)
422 Unprocessable Entity - Validasi gagal
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error - Error server
Naming Conventions
• URL: lowercase, kebab-case, plural nouns
• JSON Fields: snake_case
• Dates: ISO 8601 format with UTC timezone (YYYY-MM-DDTHH:mm:ssZ)
2
Table of Contents
1. Authentication
2. Users
3. Profile Management
4. Portfolios
5. Content Blocks
6. Tags
7. File Upload (MinIO)
8. Social - Follow
9. Likes
10. Search
11. Feed
12. Admin - Majors
13. Admin - Academic Years
14. Admin - Classes
15. Admin - Tags
16. Admin - Users
17. Admin - Portfolios
18. Admin - Moderation
1. Authentication
POST /auth/login
Login user dan dapatkan access token + refresh token.
Authentication: None
Request Body:
{
"username": "john_doe",
"password": "securepassword123"
}
Success Response (200):
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",
"email": "john@[Link]",
3
"role": "student",
"avatar_url": "[Link]
}
}
}
Response Headers:
Set-Cookie: refresh_token=abc123...; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; M
Error Responses:
401 Unauthorized - Kredensial salah:
{
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Username atau password salah"
}
}
403 Forbidden - Akun nonaktif:
{
"error": {
"code": "ACCOUNT_DISABLED",
"message": "Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut."
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "username",
"message": "Username wajib diisi"
},
{
"field": "password",
"message": "Password wajib diisi"
}
]
}
}
4
POST /auth/refresh
Refresh access token menggunakan refresh token dari cookie.
Authentication: None (menggunakan HttpOnly cookie)
Request: Cookie refresh_token dikirim otomatis oleh browser
Success Response (200):
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}
}
Error Responses:
401 Unauthorized - Token expired/invalid:
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "Refresh token telah expired. Silakan login ulang."
}
}
401 Unauthorized - Token revoked:
{
"error": {
"code": "TOKEN_REVOKED",
"message": "Token telah dicabut. Silakan login ulang."
}
}
401 Unauthorized - Token reuse detected:
{
"error": {
"code": "TOKEN_REUSE_DETECTED",
"message": "Aktivitas mencurigakan terdeteksi. Semua sesi telah diakhiri untuk keamanan.
}
}
POST /auth/logout
Logout dari sesi saat ini dan hapus refresh token.
5
Authentication: Required
Success Response (200):
{
"message": "Berhasil logout"
}
Response Headers:
Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=0
POST /auth/logout-all
Logout dari semua perangkat/sesi (revoke semua refresh token user).
Authentication: Required
Success Response (200):
{
"message": "Berhasil logout dari semua perangkat",
"data": {
"sessions_terminated": 3
}
}
GET /auth/sessions
Lihat semua sesi aktif user (daftar refresh token yang belum expired).
Authentication: Required
Success Response (200):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"device_info": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/[Link]",
"ip_address": "[Link]",
"created_at": "2026-02-09T10:00:00Z",
"expires_at": "2026-02-16T10:00:00Z",
"is_current": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"device_info": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)",
6
"ip_address": "[Link]",
"created_at": "2026-02-08T15:30:00Z",
"expires_at": "2026-02-15T15:30:00Z",
"is_current": false
}
]
}
DELETE /auth/sessions/{session_id}
Hapus/revoke sesi tertentu.
Authentication: Required
Path Parameters:
7
2. Users
GET /users
Daftar semua user (publik). Digunakan untuk halaman “Siswa & Alumni”.
Authentication: Optional
Query Parameters:
8
}
},
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"username": "jane_smith",
"name": "Jane Smith",
"avatar_url": "[Link]
"role": "alumni",
"class": null,
"major": {
"id": "770e8400-e29b-41d4-a716-446655440001",
"name": "Desain Komunikasi Visual",
"code": "dkv"
}
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 5,
"total_count": 100
}
}
GET /users/{username}
Detail profil user berdasarkan username. Digunakan untuk halaman profil pub-
lik.
Authentication: Optional (jika login, akan ada field is_following)
Path Parameters:
9
"banner_url": "[Link]
"role": "student",
"status": "active",
"entry_year": 2023,
"graduation_year": null,
"class": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A",
"grade_level": 12
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"class_history": [
{
"class_name": "X-RPL-A",
"academic_year": 2023,
"started_at": "2023-07-15T00:00:00Z",
"ended_at": "2024-06-30T23:59:59Z"
},
{
"class_name": "XI-RPL-A",
"academic_year": 2024,
"started_at": "2024-07-01T00:00:00Z",
"ended_at": "2025-06-30T23:59:59Z"
},
{
"class_name": "XII-RPL-A",
"academic_year": 2025,
"started_at": "2025-07-01T00:00:00Z",
"ended_at": null
}
],
"social_links": {
"github": "[Link]
"instagram": "[Link]
"linkedin": "[Link]
},
"follower_count": 150,
"following_count": 75,
"portfolio_count": 12,
"is_following": false,
"created_at": "2023-07-15T08:00:00Z"
}
10
}
Error Responses:
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
}
}
GET /users/{username}/followers
Daftar follower user.
Authentication: Optional
Path Parameters:
Query Parameters:
11
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 8,
"total_count": 150
}
}
Error Responses:
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
}
}
GET /users/{username}/following
Daftar user yang di-follow.
Authentication: Optional
Path Parameters:
3. Profile Management
GET /me
Profil user yang sedang login (termasuk data private seperti email, NISN, NIS).
Authentication: Required
Success Response (200):
12
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"email": "john@[Link]",
"name": "John Doe",
"bio": "Siswa RPL yang suka coding dan desain UI/UX",
"avatar_url": "[Link]
"banner_url": "[Link]
"role": "student",
"status": "active",
"nisn": "0098115881",
"nis": "25491/02000.0411",
"entry_year": 2023,
"graduation_year": null,
"class": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A",
"grade_level": 12
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"social_links": {
"github": "[Link]
"instagram": "[Link]
},
"follower_count": 150,
"following_count": 75,
"created_at": "2023-07-15T08:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
}
}
PATCH /me
Update profil user yang sedang login.
Authentication: Required
Request Body:
{
"name": "John Doe Updated",
13
"username": "john_doe_new",
"bio": "Updated bio - Passionate about web development",
"email": "newemail@[Link]"
}
Field Rules: - name: 1-100 karakter - username: 3-50 karakter, hanya huruf,
angka, underscore, harus unik - bio: max 500 karakter - email: format email
valid, harus unik
Success Response (200):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe_new",
"email": "newemail@[Link]",
"name": "John Doe Updated",
"bio": "Updated bio - Passionate about web development",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Profil berhasil diperbarui"
}
Error Responses:
409 Conflict - Username sudah dipakai:
{
"error": {
"code": "USERNAME_TAKEN",
"message": "Username sudah digunakan oleh user lain"
}
}
409 Conflict - Email sudah dipakai:
{
"error": {
"code": "EMAIL_TAKEN",
"message": "Email sudah digunakan oleh user lain"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
14
"field": "username",
"message": "Username minimal 3 karakter"
},
{
"field": "email",
"message": "Format email tidak valid"
},
{
"field": "bio",
"message": "Bio maksimal 500 karakter"
}
]
}
}
PATCH /me/password
Ubah password user yang sedang login.
Authentication: Required
Request Body:
{
"current_password": "oldpassword123",
"new_password": "newpassword456",
"new_password_confirmation": "newpassword456"
}
Field Rules: - current_password: wajib diisi - new_password: minimal 8
karakter - new_password_confirmation: harus sama dengan new_password
Success Response (200):
{
"message": "Password berhasil diubah"
}
Error Responses:
400 Bad Request - Password lama salah:
{
"error": {
"code": "INVALID_PASSWORD",
"message": "Password lama tidak sesuai"
}
}
15
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "new_password",
"message": "Password minimal 8 karakter"
},
{
"field": "new_password_confirmation",
"message": "Konfirmasi password tidak cocok"
}
]
}
}
PUT /me/social-links
Update semua social links sekaligus (replace all).
Authentication: Required
Request Body:
{
"social_links": {
"github": "[Link]
"instagram": "[Link]
"linkedin": "[Link]
"personal_website": "[Link]
}
}
Valid Platforms: facebook, instagram, github, linkedin, twitter,
personal_website, tiktok, youtube, behance, dribbble, threads, bluesky,
medium, gitlab
Success Response (200):
{
"data": {
"social_links": {
"github": "[Link]
"instagram": "[Link]
"linkedin": "[Link]
16
"personal_website": "[Link]
}
},
"message": "Social links berhasil diperbarui"
}
Error Responses:
422 Unprocessable Entity - URL tidak valid:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "social_links.github",
"message": "URL tidak valid"
},
{
"field": "social_links.instagram",
"message": "Platform tidak valid. Platform yang diizinkan: facebook, instagram, gith
}
]
}
}
GET /me/check-username
Cek ketersediaan username (untuk validasi real-time saat edit profil).
Authentication: Required
Query Parameters:
17
Success Response (200) - Username tidak tersedia:
{
"data": {
"username": "existing_username",
"available": false
}
}
Error Responses:
422 Unprocessable Entity - Username tidak valid:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Username tidak valid",
"details": [
{
"field": "username",
"message": "Username minimal 3 karakter"
}
]
}
}
4. Portfolios
GET /portfolios
Daftar semua portfolio yang published (publik). Digunakan untuk halaman
“Portofolio Siswa” dan “Explore”.
Authentication: Optional (jika login, akan ada field is_liked)
Query Parameters:
18
Parameter Type Required Description Example
major_id UUID No Filter ?major_id=xxx
berdasarkan
jurusan pembuat
class_id UUID No Filter ?class_id=xxx
berdasarkan kelas
pembuat
user_id UUID No Filter ?user_id=xxx
berdasarkan user
pembuat
sort string No Sorting: ?sort=-published_at
-published_at
(default),
-like_count,
title
page integer No Halaman (default: ?page=1
1)
limit integer No Jumlah per ?limit=20
halaman (default:
20, max: 50)
19
"name": "Web Development"
},
{
"id": "tag-uuid-2",
"name": "UI/UX Design"
}
]
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 10,
"total_count": 200
}
}
GET /portfolios/{username}/{slug}
Detail portfolio berdasarkan username dan slug. Digunakan untuk halaman
detail portfolio publik.
Authentication: Optional (jika login, akan ada field is_liked)
Path Parameters:
20
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",
"avatar_url": "[Link]
"role": "student",
"class_name": "XII-RPL-A",
"major_name": "Rekayasa Perangkat Lunak"
},
"tags": [
{
"id": "tag-uuid-1",
"name": "Web Development"
},
{
"id": "tag-uuid-2",
"name": "UI/UX Design"
}
],
"content_blocks": [
{
"id": "block-uuid-1",
"block_type": "text",
"block_order": 0,
"payload": {
"content": "<p>Ini adalah portfolio website pribadi saya yang dibuat menggunakan N
}
},
{
"id": "block-uuid-2",
"block_type": "image",
"block_order": 1,
"payload": {
"url": "[Link]
"caption": "Tampilan homepage"
}
},
{
"id": "block-uuid-3",
"block_type": "youtube",
"block_order": 2,
"payload": {
"video_id": "dQw4w9WgXcQ"
}
}
]
}
21
}
Error Responses:
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
GET /me/portfolios
Daftar semua portfolio milik user yang login (termasuk draft, pending_review,
rejected, archived).
Authentication: Required
Query Parameters:
22
{
"id": "880e8400-e29b-41d4-a716-446655440001",
"title": "Desain Logo Keren",
"slug": "desain-logo-keren",
"thumbnail_url": null,
"status": "draft",
"published_at": null,
"created_at": "2026-02-05T08:00:00Z",
"updated_at": "2026-02-05T08:00:00Z",
"like_count": 0
},
{
"id": "880e8400-e29b-41d4-a716-446655440002",
"title": "Aplikasi Mobile",
"slug": "aplikasi-mobile",
"thumbnail_url": "[Link]
"status": "rejected",
"admin_review_note": "Konten tidak sesuai dengan ketentuan. Mohon perbaiki bagian desk
"published_at": null,
"created_at": "2026-02-03T08:00:00Z",
"updated_at": "2026-02-04T10:00:00Z",
"like_count": 0
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 1,
"total_count": 3
}
}
POST /portfolios
Buat portfolio baru (status default: draft).
Authentication: Required
Request Body:
{
"title": "Website Portfolio Pribadi",
"tag_ids": [
"tag-uuid-1",
"tag-uuid-2"
]
23
}
Field Rules: - title: 1-150 karakter, wajib diisi - tag_ids: array of UUID,
optional
Success Response (201):
{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"status": "draft",
"thumbnail_url": null,
"tags": [
{
"id": "tag-uuid-1",
"name": "Web Development"
},
{
"id": "tag-uuid-2",
"name": "UI/UX Design"
}
],
"content_blocks": [],
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Portfolio berhasil dibuat"
}
Error Responses:
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "title",
"message": "Judul wajib diisi"
}
]
}
}
429 Too Many Requests - Rate limit (max 10 portfolio/hari):
24
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Anda sudah mencapai batas maksimal pembuatan portfolio hari ini (10 portfoli
"retry_after": 43200
}
}
GET /portfolios/id/{id}
Detail portfolio berdasarkan ID (untuk edit). Hanya bisa diakses oleh owner
atau admin.
Authentication: Required (owner atau admin)
Path Parameters:
PATCH /portfolios/{id}
Update portfolio (title, thumbnail, tags).
25
Authentication: Required (owner atau admin)
Path Parameters:
Request Body:
{
"title": "Website Portfolio Pribadi - Updated",
"thumbnail_url": "[Link]
"tag_ids": [
"tag-uuid-1",
"tag-uuid-3"
]
}
Success Response (200):
{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi - Updated",
"slug": "website-portfolio-pribadi-updated",
"status": "draft",
"thumbnail_url": "[Link]
"tags": [
{
"id": "tag-uuid-1",
"name": "Web Development"
},
{
"id": "tag-uuid-3",
"name": "React"
}
],
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil diperbarui"
}
Error Responses:
403 Forbidden:
{
"error": {
26
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk mengedit portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
409 Conflict - Slug sudah dipakai oleh portfolio lain milik user yang sama:
{
"error": {
"code": "SLUG_CONFLICT",
"message": "Anda sudah memiliki portfolio dengan slug yang sama"
}
}
POST /portfolios/{id}/submit
Submit portfolio untuk review (ubah status dari draft/rejected ke pend-
ing_review).
Authentication: Required (owner)
Path Parameters:
27
Error Responses:
400 Bad Request - Status tidak valid:
{
"error": {
"code": "INVALID_STATUS_TRANSITION",
"message": "Portfolio hanya bisa disubmit dari status draft atau rejected"
}
}
422 Unprocessable Entity - Portfolio belum lengkap:
{
"error": {
"code": "INCOMPLETE_PORTFOLIO",
"message": "Portfolio belum lengkap",
"details": [
{
"field": "thumbnail_url",
"message": "Thumbnail wajib diisi sebelum submit"
},
{
"field": "content_blocks",
"message": "Portfolio harus memiliki minimal 1 content block"
}
]
}
}
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk submit portfolio ini"
}
}
POST /portfolios/{id}/archive
Arsipkan portfolio (sembunyikan dari publik, status menjadi archived).
Authentication: Required (owner atau admin)
Path Parameters:
28
Parameter Type Required Description
id UUID Yes ID portfolio
POST /portfolios/{id}/unarchive
Batalkan arsip (kembalikan ke status draft).
Authentication: Required (owner atau admin)
Path Parameters:
29
Error Responses:
400 Bad Request - Status tidak valid:
{
"error": {
"code": "INVALID_STATUS_TRANSITION",
"message": "Hanya portfolio dengan status archived yang bisa di-unarchive"
}
}
DELETE /portfolios/{id}
Hapus portfolio (soft delete).
Authentication: Required (owner atau admin)
Path Parameters:
30
5. Content Blocks
Content Block Types
POST /portfolios/{portfolio_id}/blocks
Tambah content block ke portfolio.
Authentication: Required (owner atau admin)
Path Parameters:
31
}
}
Request Body - YouTube Block:
{
"block_type": "youtube",
"block_order": 2,
"payload": {
"video_id": "dQw4w9WgXcQ"
}
}
Request Body - Table Block:
{
"block_type": "table",
"block_order": 3,
"payload": {
"headers": ["Fitur", "Deskripsi", "Status"],
"rows": [
["Login", "Autentikasi user dengan JWT", "Done"],
["Dashboard", "Halaman utama aplikasi", "In Progress"],
["Profile", "Halaman profil user", "Done"]
]
}
}
Request Body - Button Block:
{
"block_type": "button",
"block_order": 4,
"payload": {
"label": "Lihat Demo",
"url": "[Link]
}
}
Success Response (201):
{
"data": {
"id": "block-uuid-1",
"block_type": "text",
"block_order": 0,
"payload": {
"content": "<p>Ini adalah paragraf pertama...</p>"
},
"created_at": "2026-02-09T10:00:00Z",
32
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Content block berhasil ditambahkan"
}
Error Responses:
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses ke portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "block_type",
"message": "Block type tidak valid. Pilihan: text, image, table, youtube, button"
},
{
"field": "[Link]",
"message": "Content wajib diisi untuk block type text"
},
{
"field": "[Link]",
"message": "URL wajib diisi untuk block type image"
}
]
}
}
409 Conflict - Block order sudah dipakai:
33
{
"error": {
"code": "BLOCK_ORDER_CONFLICT",
"message": "Block order sudah digunakan. Gunakan order yang berbeda atau reorder blocks.
}
}
PATCH /portfolios/{portfolio_id}/blocks/{block_id}
Update content block.
Authentication: Required (owner atau admin)
Path Parameters:
Request Body:
{
"payload": {
"content": "<p>Konten yang sudah diupdate dengan informasi baru...</p>"
}
}
Success Response (200):
{
"data": {
"id": "block-uuid-1",
"block_type": "text",
"block_order": 0,
"payload": {
"content": "<p>Konten yang sudah diupdate dengan informasi baru...</p>"
},
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Content block berhasil diperbarui"
}
Error Responses:
403 Forbidden:
34
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses ke portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "BLOCK_NOT_FOUND",
"message": "Content block tidak ditemukan"
}
}
PUT /portfolios/{portfolio_id}/blocks/reorder
Ubah urutan content blocks (drag & drop).
Authentication: Required (owner atau admin)
Path Parameters:
Request Body:
{
"block_orders": [
{
"id": "block-uuid-3",
"order": 0
},
{
"id": "block-uuid-1",
"order": 1
},
{
"id": "block-uuid-2",
"order": 2
}
]
}
35
Success Response (200):
{
"message": "Urutan content blocks berhasil diperbarui"
}
Error Responses:
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses ke portfolio ini"
}
}
422 Unprocessable Entity - Block ID tidak valid:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "block_orders",
"message": "Semua block ID harus valid dan milik portfolio ini"
}
]
}
}
DELETE /portfolios/{portfolio_id}/blocks/{block_id}
Hapus content block.
Authentication: Required (owner atau admin)
Path Parameters:
36
{
"message": "Content block berhasil dihapus"
}
Error Responses:
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses ke portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "BLOCK_NOT_FOUND",
"message": "Content block tidak ditemukan"
}
}
6. Tags
GET /tags
Daftar semua tags yang tersedia.
Authentication: None
Query Parameters:
37
"name": "Mobile App"
},
{
"id": "tag-uuid-3",
"name": "UI/UX Design"
},
{
"id": "tag-uuid-4",
"name": "Graphic Design"
},
{
"id": "tag-uuid-5",
"name": "3D Modeling"
}
]
}
Upload Flow
����������� ����������� �����������
� Client � � Backend � � MinIO �
����������� ����������� �����������
� � �
� 1. Request presigned URL �
��������������������>� �
� � �
� � 2. Generate URL �
� ��������������������>�
� � �
� 3. Return presigned URL �
�<�������������������� �
� � �
� 4. Upload file directly �
�����������������������������������������>�
� � �
� 5. Upload success �
�<�����������������������������������������
� � �
� 6. Confirm upload �
��������������������>� �
38
� � 7. Verify & update �
� � �
� 8. Return final URL �
�<�������������������� �
� � �
POST /uploads/presign
Request presigned URL untuk upload file ke MinIO.
Authentication: Required
Request Body - Avatar:
{
"upload_type": "avatar",
"filename": "[Link]",
"content_type": "image/jpeg",
"file_size": 102400
}
Request Body - Portfolio Thumbnail:
{
"upload_type": "thumbnail",
"filename": "[Link]",
"content_type": "image/png",
"file_size": 512000,
"portfolio_id": "880e8400-e29b-41d4-a716-446655440000"
}
Request Body - Portfolio Image:
39
{
"upload_type": "portfolio_image",
"filename": "[Link]",
"content_type": "image/jpeg",
"file_size": 1024000,
"portfolio_id": "880e8400-e29b-41d4-a716-446655440000"
}
Request Body - Avatar (Admin for User):
{
"upload_type": "avatar",
"filename": "[Link]",
"content_type": "image/jpeg",
"file_size": 102400,
"target_user_id": "550e8400-e29b-41d4-a716-446655440000"
}
Field Rules:
40
Error Responses:
400 Bad Request - File size exceeds limit:
{
"error": {
"code": "FILE_TOO_LARGE",
"message": "Ukuran file melebihi batas maksimal",
"details": [
{
"field": "file_size",
"message": "Ukuran file avatar maksimal 2MB"
}
]
}
}
400 Bad Request - Invalid content type:
{
"error": {
"code": "INVALID_CONTENT_TYPE",
"message": "Tipe file tidak diizinkan",
"details": [
{
"field": "content_type",
"message": "Tipe file yang diizinkan: image/jpeg, image/png, image/webp"
}
]
}
}
403 Forbidden - Not owner of portfolio:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk upload ke portfolio ini"
}
}
422 Unprocessable Entity - Missing portfolio_id:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "portfolio_id",
41
"message": "Portfolio ID wajib diisi untuk upload type thumbnail atau portfolio_imag
}
]
}
}
if (![Link]) {
throw new Error('Upload failed');
}
return true;
}
// Usage
const file = [Link]('fileInput').files[0];
const presignedData = await getPresignedUrl(file);
await uploadToMinIO(presignedData, file);
await confirmUpload(presignedData.upload_id, presignedData.object_key);
POST /uploads/confirm
Konfirmasi upload selesai dan update database.
Authentication: Required
Request Body:
{
"upload_id": "upload-uuid-123",
"object_key": "avatars/550e8400-e29b-41d4-a716-446655440000/[Link]",
"target_user_id": "550e8400-e29b-41d4-a716-446655440000"
}
42
Success Response (200) - Avatar:
{
"data": {
"type": "avatar",
"url": "[Link]
"object_key": "avatars/550e8400-e29b-41d4-a716-446655440000/[Link]"
},
"message": "Avatar berhasil diperbarui"
}
Success Response (200) - Portfolio Thumbnail:
{
"data": {
"type": "thumbnail",
"url": "[Link]
"portfolio_id": "880e8400-e29b-41d4-a716-446655440000",
"object_key": "thumbnails/880e8400-e29b-41d4-a716-446655440000/[Link]"
},
"message": "Thumbnail portfolio berhasil diperbarui"
}
Success Response (200) - Portfolio Image:
{
"data": {
"type": "portfolio_image",
"url": "[Link]
"portfolio_id": "880e8400-e29b-41d4-a716-446655440000",
"object_key": "portfolio-images/880e8400-e29b-41d4-a716-446655440000/[Link]"
},
"message": "Image berhasil diupload"
}
Error Responses:
404 Not Found - Upload ID tidak ditemukan:
{
"error": {
"code": "UPLOAD_NOT_FOUND",
"message": "Upload tidak ditemukan atau sudah expired"
}
}
400 Bad Request - File tidak ditemukan di storage:
{
"error": {
"code": "OBJECT_NOT_FOUND",
43
"message": "File tidak ditemukan di storage. Pastikan upload berhasil."
}
}
8. Social - Follow
POST /users/{username}/follow
Follow user.
Authentication: Required
Path Parameters:
44
409 Conflict - Sudah follow:
{
"error": {
"code": "ALREADY_FOLLOWING",
"message": "Anda sudah follow user ini"
}
}
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
}
}
DELETE /users/{username}/follow
Unfollow user.
Authentication: Required
Path Parameters:
45
}
}
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
}
}
9. Likes
POST /portfolios/{id}/like
Like portfolio.
Authentication: Required
Path Parameters:
46
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
403 Forbidden - Portfolio tidak published:
{
"error": {
"code": "PORTFOLIO_NOT_PUBLISHED",
"message": "Hanya portfolio yang published yang bisa di-like"
}
}
DELETE /portfolios/{id}/like
Unlike portfolio.
Authentication: Required
Path Parameters:
47
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
10. Search
GET /search/users
Cari user berdasarkan nama, username, atau bio.
Authentication: Optional
Query Parameters:
48
"total_pages": 1,
"total_count": 5
}
}
Error Responses:
422 Unprocessable Entity - Query terlalu pendek:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "q",
"message": "Query pencarian minimal 2 karakter"
}
]
}
}
GET /search/portfolios
Cari portfolio berdasarkan judul atau nama user.
Authentication: Optional
Query Parameters:
49
"message": "Validasi gagal",
"details": [
{
"field": "q",
"message": "Query pencarian minimal 2 karakter"
}
]
}
}
11. Feed
GET /feed
Timeline portfolio dari user yang di-follow (untuk halaman Feed).
Authentication: Required
Query Parameters:
50
"tags": [
{
"id": "tag-uuid-1",
"name": "Web Development"
}
]
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 5,
"total_count": 100
}
}
Note: Jika user belum follow siapapun, response akan berisi array kosong.
GET /admin/majors
Daftar semua jurusan.
Authentication: Required (admin)
Success Response (200):
{
"data": [
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
"id": "770e8400-e29b-41d4-a716-446655440001",
"name": "Teknik Komputer dan Jaringan",
"code": "tkj",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
51
"id": "770e8400-e29b-41d4-a716-446655440002",
"name": "Desain Komunikasi Visual",
"code": "dkv",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
POST /admin/majors
Buat jurusan baru.
Authentication: Required (admin)
Request Body:
{
"name": "Animasi",
"code": "ani"
}
Field Rules: - name: 1-100 karakter, wajib diisi, harus unik - code: 1-10
karakter, hanya huruf lowercase, wajib diisi, harus unik
Success Response (201):
{
"data": {
"id": "770e8400-e29b-41d4-a716-446655440003",
"name": "Animasi",
"code": "ani",
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Jurusan berhasil dibuat"
}
Error Responses:
409 Conflict - Kode sudah digunakan:
{
"error": {
"code": "DUPLICATE_CODE",
"message": "Kode jurusan sudah digunakan"
}
}
52
409 Conflict - Nama sudah digunakan:
{
"error": {
"code": "DUPLICATE_NAME",
"message": "Nama jurusan sudah digunakan"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "code",
"message": "Kode hanya boleh berisi huruf lowercase"
},
{
"field": "name",
"message": "Nama jurusan wajib diisi"
}
]
}
}
PATCH /admin/majors/{id}
Update jurusan.
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"name": "Animasi - Updated",
"code": "ani"
}
Success Response (200):
53
{
"data": {
"id": "770e8400-e29b-41d4-a716-446655440003",
"name": "Animasi - Updated",
"code": "ani",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Jurusan berhasil diperbarui"
}
Error Responses: Sama dengan POST /admin/majors
DELETE /admin/majors/{id}
Hapus jurusan.
Authentication: Required (admin)
Path Parameters:
54
13. Admin - Academic Years
GET /admin/academic-years
Daftar semua tahun ajaran.
Authentication: Required (admin)
Success Response (200):
{
"data": [
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"start_year": 2025,
"is_active": true,
"promotion_month": 7,
"promotion_day": 1,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
"id": "990e8400-e29b-41d4-a716-446655440001",
"start_year": 2024,
"is_active": false,
"promotion_month": 7,
"promotion_day": 1,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}
POST /admin/academic-years
Buat tahun ajaran baru.
Authentication: Required (admin)
Request Body:
{
"start_year": 2026,
"is_active": false,
"promotion_month": 7,
55
"promotion_day": 1
}
Field Rules: - start_year: integer, wajib diisi, harus unik - is_active:
boolean, default false (hanya satu tahun ajaran yang bisa aktif) - promotion_month:
integer 1-12, wajib diisi - promotion_day: integer 1-31, wajib diisi
Success Response (201):
{
"data": {
"id": "990e8400-e29b-41d4-a716-446655440002",
"start_year": 2026,
"is_active": false,
"promotion_month": 7,
"promotion_day": 1,
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Tahun ajaran berhasil dibuat"
}
Error Responses:
409 Conflict - Tahun sudah ada:
{
"error": {
"code": "DUPLICATE_YEAR",
"message": "Tahun ajaran sudah ada"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "promotion_month",
"message": "Bulan promosi harus antara 1-12"
},
{
"field": "promotion_day",
"message": "Tanggal promosi harus antara 1-31"
}
]
56
}
}
PATCH /admin/academic-years/{id}
Update tahun ajaran.
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"is_active": true,
"promotion_month": 7,
"promotion_day": 15
}
Note: Jika is_active diubah menjadi true, tahun ajaran lain yang
aktif akan otomatis dinonaktifkan (hanya satu yang bisa aktif).
Success Response (200):
{
"data": {
"id": "990e8400-e29b-41d4-a716-446655440002",
"start_year": 2026,
"is_active": true,
"promotion_month": 7,
"promotion_day": 15,
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Tahun ajaran berhasil diperbarui"
}
DELETE /admin/academic-years/{id}
Hapus tahun ajaran.
Authentication: Required (admin)
Path Parameters:
57
Parameter Type Required Description
id UUID Yes ID tahun ajaran
58
{
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A",
"grade_level": 12,
"homeroom": "A",
"academic_year": {
"id": "990e8400-e29b-41d4-a716-446655440000",
"start_year": 2025,
"is_active": true
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"student_count": 32,
"created_at": "2025-07-01T00:00:00Z",
"updated_at": "2025-07-01T00:00:00Z"
}
]
}
POST /admin/classes
Buat kelas baru.
Authentication: Required (admin)
Request Body:
{
"academic_year_id": "990e8400-e29b-41d4-a716-446655440000",
"major_id": "770e8400-e29b-41d4-a716-446655440000",
"grade_level": 10,
"homeroom": "A"
}
Field Rules: - academic_year_id: UUID, wajib diisi - major_id: UUID, wa-
jib diisi - grade_level: integer (10, 11, atau 12), wajib diisi - homeroom: single
uppercase letter (A-Z), wajib diisi - name: auto-generated (format: Roman-
MajorCode-Homeroom, contoh: X-RPL-A)
Success Response (201):
{
59
"data": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "X-RPL-A",
"grade_level": 10,
"homeroom": "A",
"academic_year": {
"id": "990e8400-e29b-41d4-a716-446655440000",
"start_year": 2025,
"is_active": true
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Kelas berhasil dibuat"
}
Error Responses:
409 Conflict - Kelas sudah ada:
{
"error": {
"code": "DUPLICATE_CLASS",
"message": "Kelas dengan kombinasi tahun ajaran, jurusan, tingkat, dan rombel sudah ada"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "grade_level",
"message": "Tingkat harus 10, 11, atau 12"
},
{
"field": "homeroom",
"message": "Rombel harus satu huruf kapital (A-Z)"
}
]
60
}
}
PATCH /admin/classes/{id}
Update kelas.
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"homeroom": "B"
}
Note: Nama kelas akan otomatis di-regenerate jika ada perubahan
pada major, grade_level, atau homeroom.
Success Response (200):
{
"data": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "X-RPL-B",
"grade_level": 10,
"homeroom": "B",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Kelas berhasil diperbarui"
}
DELETE /admin/classes/{id}
Hapus kelas.
Authentication: Required (admin)
Path Parameters:
61
Parameter Type Required Description
id UUID Yes ID kelas
62
Success Response (200):
{
"data": [
{
"id": "tag-uuid-1",
"name": "Web Development",
"portfolio_count": 45,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
"id": "tag-uuid-2",
"name": "Mobile App",
"portfolio_count": 23,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
{
"id": "tag-uuid-3",
"name": "UI/UX Design",
"portfolio_count": 67,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
POST /admin/tags
Buat tag baru.
Authentication: Required (admin)
Request Body:
{
"name": "Machine Learning"
}
Field Rules: - name: 1-50 karakter, wajib diisi, harus unik
Success Response (201):
{
"data": {
"id": "tag-uuid-10",
"name": "Machine Learning",
63
"portfolio_count": 0,
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "Tag berhasil dibuat"
}
Error Responses:
409 Conflict - Tag sudah ada:
{
"error": {
"code": "DUPLICATE_TAG",
"message": "Tag dengan nama tersebut sudah ada"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "name",
"message": "Nama tag wajib diisi"
}
]
}
}
PATCH /admin/tags/{id}
Update tag.
Authentication: Required (admin)
Path Parameters:
Request Body:
64
{
"name": "Machine Learning & AI"
}
Success Response (200):
{
"data": {
"id": "tag-uuid-10",
"name": "Machine Learning & AI",
"portfolio_count": 0,
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Tag berhasil diperbarui"
}
Error Responses: Sama dengan POST /admin/tags
DELETE /admin/tags/{id}
Hapus tag.
Authentication: Required (admin)
Path Parameters:
65
{
"error": {
"code": "TAG_NOT_FOUND",
"message": "Tag tidak ditemukan"
}
}
66
"nis": "25491/02000.0411",
"avatar_url": "[Link]
"banner_url": "[Link]
"class": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A"
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"entry_year": 2023,
"graduation_year": null,
"portfolio_count": 12,
"created_at": "2023-07-15T08:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 10,
"total_count": 200
}
}
POST /admin/users
Buat user baru (karena tidak ada fitur register).
Authentication: Required (admin)
Request Body - Student:
{
"username": "jane_smith",
"email": "jane@[Link]",
"password": "defaultpassword123",
"name": "Jane Smith",
"role": "student",
"status": "active",
"nisn": "0098115882",
"nis": "25492/02000.0412",
"class_id": "660e8400-e29b-41d4-a716-446655440000",
"entry_year": 2024
67
}
Request Body - Alumni:
{
"username": "bob_alumni",
"email": "bob@[Link]",
"password": "defaultpassword123",
"name": "Bob Alumni",
"role": "alumni",
"status": "graduated",
"nisn": "0098115883",
"nis": "25493/02000.0413",
"major_id": "770e8400-e29b-41d4-a716-446655440000",
"entry_year": 2020,
"graduation_year": 2023
}
Request Body - Admin:
{
"username": "admin_user",
"email": "admin@[Link]",
"password": "secureadminpass",
"name": "Admin User",
"role": "admin",
"status": "active"
}
Field Rules: - username: 3-50 karakter, hanya huruf, angka, underscore,
wajib diisi, harus unik - email: format email valid, wajib diisi, harus unik -
password: minimal 8 karakter, wajib diisi - name: 1-100 karakter, wajib di-
isi - role: student, alumni, admin, wajib diisi - status: active, graduated,
dropped_out, inactive, default active - nisn: optional, hanya untuk stu-
dent/alumni - nis: optional, hanya untuk student/alumni - class_id: wajib
untuk student dengan status active - major_id: wajib untuk alumni (jika tidak
ada class_id) - entry_year: wajib untuk student/alumni - graduation_year:
optional, hanya untuk alumni
Success Response (201):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440010",
"username": "jane_smith",
"email": "jane@[Link]",
"name": "Jane Smith",
"role": "student",
"status": "active",
68
"nisn": "0098115882",
"nis": "25492/02000.0412",
"avatar_url": null,
"banner_url": null,
"class": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A"
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"entry_year": 2024,
"graduation_year": null,
"created_at": "2026-02-09T10:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
},
"message": "User berhasil dibuat"
}
Error Responses:
409 Conflict - Username sudah digunakan:
{
"error": {
"code": "USERNAME_TAKEN",
"message": "Username sudah digunakan"
}
}
409 Conflict - Email sudah digunakan:
{
"error": {
"code": "EMAIL_TAKEN",
"message": "Email sudah digunakan"
}
}
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
69
"field": "username",
"message": "Username minimal 3 karakter"
},
{
"field": "password",
"message": "Password minimal 8 karakter"
},
{
"field": "class_id",
"message": "Class ID wajib diisi untuk student dengan status active"
}
]
}
}
GET /admin/users/{id}
Detail user (untuk admin panel).
Authentication: Required (admin)
Path Parameters:
70
"name": "XII-RPL-A",
"grade_level": 12
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"
},
"class_history": [
{
"class_name": "X-RPL-A",
"academic_year": 2023,
"started_at": "2023-07-15T00:00:00Z",
"ended_at": "2024-06-30T23:59:59Z"
},
{
"class_name": "XI-RPL-A",
"academic_year": 2024,
"started_at": "2024-07-01T00:00:00Z",
"ended_at": "2025-06-30T23:59:59Z"
},
{
"class_name": "XII-RPL-A",
"academic_year": 2025,
"started_at": "2025-07-01T00:00:00Z",
"ended_at": null
}
],
"social_links": {
"github": "[Link]
"instagram": "[Link]
},
"follower_count": 150,
"following_count": 75,
"portfolio_count": 12,
"created_at": "2023-07-15T08:00:00Z",
"updated_at": "2026-02-09T10:00:00Z"
}
}
Error Responses:
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
71
"message": "User tidak ditemukan"
}
}
PATCH /admin/users/{id}
Update user (oleh admin).
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"name": "John Doe Updated",
"username": "john_doe_new",
"email": "newemail@[Link]",
"role": "alumni",
"status": "graduated",
"class_id": null,
"graduation_year": 2026
}
Success Response (200):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe_new",
"email": "newemail@[Link]",
"name": "John Doe Updated",
"role": "alumni",
"status": "graduated",
"graduation_year": 2026,
"avatar_url": "[Link]
"banner_url": "[Link]
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "User berhasil diperbarui"
}
Error Responses: Sama dengan POST /admin/users
72
PATCH /admin/users/{id}/reset-password
Reset password user (manual oleh admin, karena tidak ada forgot password).
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"new_password": "newpassword123"
}
Field Rules: - new_password: minimal 8 karakter, wajib diisi
Success Response (200):
{
"message": "Password user berhasil direset"
}
Error Responses:
422 Unprocessable Entity - Validasi gagal:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
"field": "new_password",
"message": "Password minimal 8 karakter"
}
]
}
}
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
73
}
}
DELETE /admin/users/{id}
Hapus user (soft delete).
Authentication: Required (admin)
Path Parameters:
74
Query Parameters:
75
"role": "student",
"class_name": "XII-RPL-A",
"major_name": "Rekayasa Perangkat Lunak"
},
"tags": [
{
"id": "tag-uuid-1",
"name": "Web Development"
}
]
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 15,
"total_count": 300
}
}
POST /admin/portfolios
Buat portfolio baru untuk user tertentu (oleh admin).
Authentication: Required (admin)
Request Body:
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Project Baru",
"tag_ids": ["tag-uuid-1", "tag-uuid-2"]
}
Success Response (201):
{
"data": {
"id": "new-portfolio-id",
"title": "Project Baru",
"slug": "project-baru",
"status": "draft",
"user": { ... },
"tags": [ ... ],
"content_blocks": [],
"created_at": "..."
},
76
"message": "Portfolio berhasil dibuat"
}
GET /admin/portfolios/{id}
Detail portfolio (untuk admin panel).
Authentication: Required (admin)
Path Parameters:
PATCH /admin/portfolios/{id}
Update portfolio (oleh admin).
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"title": "Website Portfolio Pribadi - Edited by Admin",
"status": "published",
"thumbnail_url": "[Link]
"tag_ids": ["tag-uuid-1", "tag-uuid-3"],
"admin_review_note": null
}
Success Response (200):
{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi - Edited by Admin",
"status": "published",
77
"thumbnail_url": "[Link]
"tags": [ ... ],
"admin_review_note": null,
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil diperbarui"
}
DELETE /admin/portfolios/{id}
Hapus portfolio (oleh admin, hard delete).
Authentication: Required (admin)
Path Parameters:
POST /admin/portfolios/{id}/blocks
Tambah content block ke portfolio.
Authentication: Required (admin)
Request Body:
{
"block_type": "text",
78
"block_order": 1,
"payload": { "content": "<p>Hello</p>" }
}
Success Response (201):
{
"data": { "id": "...", "block_type": "text", ... },
"message": "Content block berhasil ditambahkan"
}
PATCH /admin/portfolios/{id}/blocks/{blockId}
Update content block.
Authentication: Required (admin)
Request Body:
{
"block_type": "text",
"payload": { "content": "<p>Updated</p>" }
}
Success Response (200):
{
"data": { ... },
"message": "Content block berhasil diperbarui"
}
DELETE /admin/portfolios/{id}/blocks/{blockId}
Hapus content block.
Authentication: Required (admin)
Success Response (200):
{
"message": "Content block berhasil dihapus"
}
PUT /admin/portfolios/{id}/blocks/reorder
Urutkan ulang content blocks.
79
Authentication: Required (admin)
Request Body:
{
"block_orders": [
{ "id": "block-id-1", "order": 0 },
{ "id": "block-id-2", "order": 1 }
]
}
Success Response (200):
{
"message": "Urutan content blocks berhasil diperbarui"
}
80
Parameter Type Required Description
limit integer No Jumlah per halaman
(default: 20, max: 100)
81
POST /admin/moderation/portfolios/{id}/approve
Setujui portfolio (ubah status dari pending_review ke published).
Authentication: Required (admin)
Path Parameters:
82
POST /admin/moderation/portfolios/{id}/reject
Tolak portfolio (ubah status dari pending_review ke rejected).
Authentication: Required (admin)
Path Parameters:
Request Body:
{
"admin_review_note": "Konten tidak sesuai dengan ketentuan. Mohon perbaiki bagian deskrips
}
Field Rules: - admin_review_note: 1-500 karakter, wajib diisi saat reject
Success Response (200):
{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440005",
"status": "rejected",
"admin_review_note": "Konten tidak sesuai dengan ketentuan. Mohon perbaiki bagian deskri
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio ditolak"
}
Error Responses:
400 Bad Request - Status tidak valid:
{
"error": {
"code": "INVALID_STATUS",
"message": "Hanya portfolio dengan status pending_review yang bisa ditolak"
}
}
422 Unprocessable Entity - Review note kosong:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validasi gagal",
"details": [
{
83
"field": "admin_review_note",
"message": "Catatan review wajib diisi saat menolak portfolio"
}
]
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}
End of Documentation
Last Updated: 2026-02-25
Version: 1.0.0
Contact: [Link]@[Link]
84