0% found this document useful (0 votes)
2 views84 pages

Grafikarsa API Documentation: Base URL

The Grafikarsa API documentation provides details on the API for the Grafikarsa platform, including authentication methods using JWT, response formats, and various endpoints for user management, profile management, and portfolio handling. It outlines the tech stack, HTTP status codes, and naming conventions for API requests and responses. The document serves as a comprehensive guide for developers to interact with the API effectively.

Uploaded by

mrafapradanaa
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views84 pages

Grafikarsa API Documentation: Base URL

The Grafikarsa API documentation provides details on the API for the Grafikarsa platform, including authentication methods using JWT, response formats, and various endpoints for user management, profile management, and portfolio handling. It outlines the tech stack, HTTP status codes, and naming conventions for API requests and responses. The document serves as a comprehensive guide for developers to interact with the API effectively.

Uploaded by

mrafapradanaa
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Grafikarsa API Documentation

Base URL: [Link]


Version: 1.0.0

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"
}
]
}
}

HTTP Status Codes

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:

Parameter Type Required Description


session_id UUID Yes ID sesi yang akan dihapus

Success Response (200):


{
"message": "Sesi berhasil dihapus"
}
Error Responses:
404 Not Found:
{
"error": {
"code": "SESSION_NOT_FOUND",
"message": "Sesi tidak ditemukan"
}
}
403 Forbidden - Mencoba hapus sesi user lain:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk menghapus sesi ini"
}
}

7
2. Users
GET /users
Daftar semua user (publik). Digunakan untuk halaman “Siswa & Alumni”.
Authentication: Optional
Query Parameters:

Parameter Type Required Description Example


search string No Cari berdasarkan ?search=john
nama, username,
bio
major_id UUID No Filter ?major_id=xxx
berdasarkan
jurusan
class_id UUID No Filter ?class_id=xxx
berdasarkan kelas
role string No Filter ?role=student
berdasarkan role:
student, alumni,
admin
page integer No Halaman (default: ?page=2
1)
limit integer No Jumlah per ?limit=20
halaman (default:
20, max: 50)

Success Response (200):


{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",
"avatar_url": "[Link]
"role": "student",
"class": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "XII-RPL-A"
},
"major": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Rekayasa Perangkat Lunak",
"code": "rpl"

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:

Parameter Type Required Description


username string Yes Username user

Success Response (200):


{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",
"bio": "Siswa RPL yang suka coding dan desain UI/UX",
"avatar_url": "[Link]

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:

Parameter Type Required Description


username string Yes Username user

Query Parameters:

Parameter Type Required Description


search string No Cari berdasarkan nama/username
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman (default: 20, max: 50)

Success Response (200):


{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"username": "jane_smith",
"name": "Jane Smith",
"avatar_url": "[Link]
"role": "student",
"class_name": "XI-DKV-B",
"is_following": true,
"followed_at": "2026-01-15T10:00:00Z"
}

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:

Parameter Type Required Description


username string Yes Username user

Query Parameters: Sama dengan /followers


Success Response (200): Sama dengan /followers
Error Responses: Sama dengan /followers

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:

Parameter Type Required Description


username string Yes Username yang ingin dicek

Success Response (200):


{
"data": {
"username": "new_username",
"available": true
}
}

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:

Parameter Type Required Description Example


search string No Cari berdasarkan ?search=website
judul atau nama
user
tag_ids string No Filter ?tag_ids=uuid1,uuid2
berdasarkan tag
(comma-
separated UUID)

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)

Success Response (200):


{
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"thumbnail_url": "[Link]
"published_at": "2026-02-01T10:00:00Z",
"like_count": 45,
"is_liked": false,
"user": {
"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",

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:

Parameter Type Required Description


username string Yes Username pemilik portfolio
slug string Yes Slug portfolio

Success Response (200):


{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"thumbnail_url": "[Link]
"status": "published",
"published_at": "2026-02-01T10:00:00Z",
"created_at": "2026-01-25T08:00:00Z",
"updated_at": "2026-02-01T09:30:00Z",
"like_count": 45,
"is_liked": false,
"user": {

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:

Parameter Type Required Description


status string No Filter: draft,
pending_review,
rejected, published,
archived
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman
(default: 20, max: 50)

Success Response (200):


{
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"thumbnail_url": "[Link]
"status": "published",
"published_at": "2026-02-01T10:00:00Z",
"created_at": "2026-01-25T08:00:00Z",
"updated_at": "2026-02-01T09:30:00Z",
"like_count": 45
},

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200): Sama dengan GET /portfolios/{username}/{slug}


Error Responses:
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk melihat portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}

PATCH /portfolios/{id}
Update portfolio (title, thumbnail, tags).

25
Authentication: Required (owner atau admin)
Path Parameters:

Parameter Type Required Description


id UUID Yes ID portfolio

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"status": "pending_review",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil diajukan untuk review"
}

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

Success Response (200):


{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"status": "archived",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil diarsipkan"
}
Error Responses:
400 Bad Request - Status tidak valid:
{
"error": {
"code": "INVALID_STATUS_TRANSITION",
"message": "Hanya portfolio dengan status published yang bisa diarsipkan"
}
}

POST /portfolios/{id}/unarchive
Batalkan arsip (kembalikan ke status draft).
Authentication: Required (owner atau admin)
Path Parameters:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"status": "draft",
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil dikembalikan"
}

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"message": "Portfolio berhasil dihapus"
}
Error Responses:
403 Forbidden:
{
"error": {
"code": "FORBIDDEN",
"message": "Anda tidak memiliki akses untuk menghapus portfolio ini"
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}

30
5. Content Blocks
Content Block Types

Type Description Payload Structure


text Rich text / paragraf { "content": "<p>HTML
content</p>" }
image Gambar dengan caption { "url": "[Link]
"caption": "Optional caption"
}
table Tabel dengan header & { "headers": ["col1", "col2"],
row "rows": [["data1", "data2"]] }
youtube Video YouTube embed { "video_id": "dQw4w9WgXcQ" }
button Tombol custom dengan { "label": "Click Me", "url":
link "[Link] }

POST /portfolios/{portfolio_id}/blocks
Tambah content block ke portfolio.
Authentication: Required (owner atau admin)
Path Parameters:

Parameter Type Required Description


portfolio_id UUID Yes ID portfolio

Request Body - Text Block:


{
"block_type": "text",
"block_order": 0,
"payload": {
"content": "<p>Ini adalah paragraf pertama...</p><p>Paragraf kedua dengan <strong>bold t
}
}
Request Body - Image Block:
{
"block_type": "image",
"block_order": 1,
"payload": {
"url": "[Link]
"caption": "Screenshot aplikasi homepage"

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:

Parameter Type Required Description


portfolio_id UUID Yes ID portfolio
block_id UUID Yes ID content block

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:

Parameter Type Required Description


portfolio_id UUID Yes ID portfolio

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:

Parameter Type Required Description


portfolio_id UUID Yes ID portfolio
block_id UUID Yes ID content block

Success Response (200):

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:

Parameter Type Required Description


search string No Cari berdasarkan nama tag

Success Response (200):


{
"data": [
{
"id": "tag-uuid-1",
"name": "Web Development"
},
{
"id": "tag-uuid-2",

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"
}
]
}

7. File Upload (MinIO)


Grafikarsa menggunakan MinIO sebagai object storage dengan strategi Pre-
signed URL untuk upload file secara efisien dan aman.

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 �
�<�������������������� �
� � �

Supported Upload Types

Type Purpose Max Size Allowed Formats


avatar User profile 2 MB jpg, jpeg, png, webp
picture
banner User profile 2 MB jpg, jpeg, png, webp
banner
thumbnail Portfolio 5 MB jpg, jpeg, png, webp
thumbnail
Image in
portfolio_image 5 MB jpg, jpeg, png, webp
content block

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:

Field Type Required Description


upload_type string Yes avatar, banner,
thumbnail,
portfolio_image
filename string Yes Nama file asli
content_type string Yes MIME type file
file_size integer Yes Ukuran file dalam bytes
portfolio_id UUID Conditional Wajib jika thumbnail
atau portfolio_image
target_user_id UUID No (Admin Only) ID user
target untuk upload asset

Success Response (200):


{
"data": {
"upload_id": "upload-uuid-123",
"presigned_url": "[Link]
"object_key": "avatars/550e8400-e29b-41d4-a716-446655440000/[Link]",
"expires_in": 900,
"method": "PUT",
"headers": {
"Content-Type": "image/jpeg"
}
}
}

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
}
]
}
}

Client-Side Upload to MinIO


Setelah mendapat presigned URL, client upload langsung ke MinIO:
JavaScript Example:
async function uploadToMinIO(presignedData, file) {
const response = await fetch(presignedData.presigned_url, {
method: [Link],
headers: [Link],
body: file
});

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."
}
}

MinIO Bucket Structure


grafikarsa/
��� avatars/{user_id}/{uuid}.{ext}
��� banners/{user_id}/{uuid}.{ext}
��� thumbnails/{portfolio_id}/{uuid}.{ext}
��� portfolio-images/{portfolio_id}/{uuid}.{ext}

8. Social - Follow
POST /users/{username}/follow
Follow user.
Authentication: Required
Path Parameters:

Parameter Type Required Description


username string Yes Username user yang akan di-follow

Success Response (200):


{
"data": {
"is_following": true,
"follower_count": 151
},
"message": "Berhasil follow john_doe"
}
Error Responses:
400 Bad Request - Tidak bisa follow diri sendiri:
{
"error": {
"code": "CANNOT_FOLLOW_SELF",
"message": "Tidak bisa follow diri sendiri"
}
}

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:

Parameter Type Required Description


username string Yes Username user yang akan di-unfollow

Success Response (200):


{
"data": {
"is_following": false,
"follower_count": 150
},
"message": "Berhasil unfollow john_doe"
}
Error Responses:
400 Bad Request - Belum follow:
{
"error": {
"code": "NOT_FOLLOWING",
"message": "Anda belum follow user ini"

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"data": {
"is_liked": true,
"like_count": 46
},
"message": "Portfolio berhasil di-like"
}
Error Responses:
409 Conflict - Sudah like:
{
"error": {
"code": "ALREADY_LIKED",
"message": "Anda sudah like portfolio ini"
}
}
404 Not Found:

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"data": {
"is_liked": false,
"like_count": 45
},
"message": "Like berhasil dihapus"
}
Error Responses:
400 Bad Request - Belum like:
{
"error": {
"code": "NOT_LIKED",
"message": "Anda belum like portfolio ini"
}
}

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:

Parameter Type Required Description


q string Yes Query pencarian (nama, username, bio)
major_id UUID No Filter jurusan
class_id UUID No Filter kelas
role string No Filter role: student, alumni, admin
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman (default: 20, max: 50)

Success Response (200):


{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",
"avatar_url": "[Link]
"bio": "Siswa RPL yang suka coding",
"role": "student",
"class_name": "XII-RPL-A",
"major_name": "Rekayasa Perangkat Lunak"
}
],
"meta": {
"current_page": 1,
"per_page": 20,

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:

Parameter Type Required Description


q string Yes Query pencarian (judul, nama user)
tag_ids string No Filter tags (comma-separated UUID)
major_id UUID No Filter jurusan pembuat
class_id UUID No Filter kelas pembuat
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman (default: 20, max: 50)

Success Response (200): Sama dengan GET /portfolios


Error Responses:
422 Unprocessable Entity - Query terlalu pendek:
{
"error": {
"code": "VALIDATION_ERROR",

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:

Parameter Type Required Description


page integer No Halaman (default: 1)
limit integer No Jumlah per halaman (default: 20, max: 50)

Success Response (200):


{
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"thumbnail_url": "[Link]
"published_at": "2026-02-09T10:00:00Z",
"like_count": 45,
"is_liked": false,
"user": {
"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"
},

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.

12. Admin - Majors


Note: Semua endpoint admin memerlukan role admin.

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:

Parameter Type Required Description


id UUID Yes ID jurusan

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:

Parameter Type Required Description


id UUID Yes ID jurusan

Success Response (200):


{
"message": "Jurusan berhasil dihapus"
}
Error Responses:
409 Conflict - Jurusan masih digunakan:
{
"error": {
"code": "MAJOR_IN_USE",
"message": "Jurusan tidak bisa dihapus karena masih digunakan oleh kelas atau user"
}
}
404 Not Found:
{
"error": {
"code": "MAJOR_NOT_FOUND",
"message": "Jurusan tidak ditemukan"
}
}

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:

Parameter Type Required Description


id UUID Yes ID tahun ajaran

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

Success Response (200):


{
"message": "Tahun ajaran berhasil dihapus"
}
Error Responses:
409 Conflict - Tahun ajaran masih digunakan:
{
"error": {
"code": "ACADEMIC_YEAR_IN_USE",
"message": "Tahun ajaran tidak bisa dihapus karena masih memiliki kelas"
}
}
400 Bad Request - Tahun ajaran aktif tidak bisa dihapus:
{
"error": {
"code": "CANNOT_DELETE_ACTIVE_YEAR",
"message": "Tahun ajaran yang aktif tidak bisa dihapus"
}
}

14. Admin - Classes


GET /admin/classes
Daftar semua kelas.
Authentication: Required (admin)
Query Parameters:

Parameter Type Required Description


academic_year_id UUID No Filter berdasarkan tahun ajaran
major_id UUID No Filter berdasarkan jurusan
grade_level integer No Filter berdasarkan tingkat: 10, 11, 12

Success Response (200):

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:

Parameter Type Required Description


id UUID Yes ID kelas

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

Success Response (200):


{
"message": "Kelas berhasil dihapus"
}
Error Responses:
409 Conflict - Kelas masih memiliki siswa:
{
"error": {
"code": "CLASS_HAS_STUDENTS",
"message": "Kelas tidak bisa dihapus karena masih memiliki siswa"
}
}

{ “error”: { “code”: “CLASS_HAS_STUDENTS”, “message”: “Kelas tidak


bisa dihapus karena masih memiliki siswa” } }

`404 Not Found`:


```json
{
"error": {
"code": "CLASS_NOT_FOUND",
"message": "Kelas tidak ditemukan"
}
}

15. Admin - Tags


GET /admin/tags
Daftar semua tags.
Authentication: Required (admin)
Query Parameters:

Parameter Type Required Description


search string No Cari berdasarkan nama tag

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:

Parameter Type Required Description


id UUID Yes ID tag

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:

Parameter Type Required Description


id UUID Yes ID tag

Success Response (200):


{
"message": "Tag berhasil dihapus"
}
Error Responses:
409 Conflict - Tag masih digunakan:
{
"error": {
"code": "TAG_IN_USE",
"message": "Tag tidak bisa dihapus karena masih digunakan oleh portfolio"
}
}
404 Not Found:

65
{
"error": {
"code": "TAG_NOT_FOUND",
"message": "Tag tidak ditemukan"
}
}

16. Admin - Users


GET /admin/users
Daftar semua user (untuk admin panel).
Authentication: Required (admin)
Query Parameters:

Parameter Type Required Description


search string No Cari berdasarkan
nama, username,
email, NISN, NIS
role string No Filter role: student,
alumni, admin
status string No Filter status: active,
graduated,
dropped_out,
inactive
major_id UUID No Filter jurusan
class_id UUID No Filter kelas
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman
(default: 20, max: 100)

Success Response (200):


{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"email": "john@[Link]",
"name": "John Doe",
"role": "student",
"status": "active",
"nisn": "0098115881",

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:

Parameter Type Required Description


id UUID Yes ID user

Success Response (200):


{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"email": "john@[Link]",
"name": "John Doe",
"bio": "Siswa RPL yang suka coding",
"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",

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:

Parameter Type Required Description


id UUID Yes ID user

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:

Parameter Type Required Description


id UUID Yes ID user

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:

Parameter Type Required Description


id UUID Yes ID user

Success Response (200):


{
"message": "User berhasil dihapus"
}
Error Responses:
400 Bad Request - Tidak bisa hapus diri sendiri:
{
"error": {
"code": "CANNOT_DELETE_SELF",
"message": "Tidak bisa menghapus akun sendiri"
}
}
404 Not Found:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User tidak ditemukan"
}
}

17. Admin - Portfolios


GET /admin/portfolios
Daftar semua portfolio (untuk admin panel, termasuk semua status).
Authentication: Required (admin)

74
Query Parameters:

Parameter Type Required Description


search string No Cari berdasarkan
judul atau nama user
status string No Filter: draft,
pending_review,
rejected, published,
archived
user_id UUID No Filter berdasarkan
user
major_ids string No Filter berdasarkan
jurusan pembuat
(comma-separated
UUID)
class_ids string No Filter berdasarkan
kelas pembuat
(comma-separated
UUID)
tag_ids string No Filter berdasarkan
tags
(comma-separated
UUID)
page integer No Halaman (default: 1)
limit integer No Jumlah per halaman
(default: 20, max: 100)

Success Response (200):


{
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"title": "Website Portfolio Pribadi",
"slug": "website-portfolio-pribadi",
"thumbnail_url": "[Link]
"status": "published",
"published_at": "2026-02-01T10:00:00Z",
"created_at": "2026-01-25T08:00:00Z",
"updated_at": "2026-02-01T09:30:00Z",
"like_count": 45,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "john_doe",
"name": "John Doe",

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200): Sama dengan GET /portfolios/id/{id}

PATCH /admin/portfolios/{id}
Update portfolio (oleh admin).
Authentication: Required (admin)
Path Parameters:

Parameter Type Required Description


id UUID Yes ID portfolio

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:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"message": "Portfolio berhasil dihapus"
}
Error Responses:
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}

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"
}

18. Admin - Moderation


GET /admin/moderation/portfolios
Daftar portfolio yang menunggu review (status: pending_review).
Authentication: Required (admin)
Query Parameters:

Parameter Type Required Description


search string No Cari berdasarkan
judul atau nama user
major_ids string No Filter berdasarkan
jurusan pembuat
(comma-separated
UUID)
class_ids string No Filter berdasarkan
kelas pembuat
(comma-separated
UUID)
tag_ids string No Filter berdasarkan
tags
(comma-separated
UUID)
sort string No Sorting: created_at
(oldest first),
-created_at (newest
first, default)
page integer No Halaman (default: 1)

80
Parameter Type Required Description
limit integer No Jumlah per halaman
(default: 20, max: 100)

Success Response (200):


{
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440005",
"title": "Aplikasi Mobile E-Commerce",
"slug": "aplikasi-mobile-e-commerce",
"thumbnail_url": "[Link]
"status": "pending_review",
"submitted_at": "2026-02-08T14:30:00Z",
"created_at": "2026-02-05T10:00:00Z",
"updated_at": "2026-02-08T14:30:00Z",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440005",
"username": "alice_dev",
"name": "Alice Developer",
"role": "student",
"class_name": "XI-RPL-B",
"major_name": "Rekayasa Perangkat Lunak"
},
"tags": [
{
"id": "tag-uuid-2",
"name": "Mobile App"
},
{
"id": "tag-uuid-3",
"name": "UI/UX Design"
}
]
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 2,
"total_count": 35
}
}

81
POST /admin/moderation/portfolios/{id}/approve
Setujui portfolio (ubah status dari pending_review ke published).
Authentication: Required (admin)
Path Parameters:

Parameter Type Required Description


id UUID Yes ID portfolio

Success Response (200):


{
"data": {
"id": "880e8400-e29b-41d4-a716-446655440005",
"status": "published",
"published_at": "2026-02-09T11:00:00Z",
"admin_review_note": null,
"updated_at": "2026-02-09T11:00:00Z"
},
"message": "Portfolio berhasil disetujui dan dipublikasikan"
}
Error Responses:
400 Bad Request - Status tidak valid:
{
"error": {
"code": "INVALID_STATUS",
"message": "Hanya portfolio dengan status pending_review yang bisa disetujui"
}
}
404 Not Found:
{
"error": {
"code": "PORTFOLIO_NOT_FOUND",
"message": "Portfolio tidak ditemukan"
}
}

82
POST /admin/moderation/portfolios/{id}/reject
Tolak portfolio (ubah status dari pending_review ke rejected).
Authentication: Required (admin)
Path Parameters:

Parameter Type Required Description


id UUID Yes ID portfolio

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

You might also like