Marketplace multi-peran bertema vintage - menghubungkan Buyer, Seller, Driver, dan Admin dalam satu ekosistem, dari katalog publik sampai pengiriman dan penanganan pesanan telat secara otomatis.
Dibuat untuk COMPFEST 18 Software Engineering Academy Technical Challenge. Seluruh 7 level (100 poin) telah diimplementasikan.
- Arsitektur & Tech Stack
- Struktur Proyek
- Setup & Instalasi
- Environment Variables
- Akun Demo
- Aturan Bisnis
- Keamanan
- Dokumentasi API
- Panduan Testing End-to-End
- Status Penyelesaian Level
- Deployment
| Bagian | Teknologi |
|---|---|
| Frontend | React 19 + Vite + Tailwind CSS v4 + Framer Motion + React Router v7 |
| Backend | Node.js + Express |
| Database | PostgreSQL (via Neon) + Prisma ORM |
| Autentikasi | JWT disimpan di httpOnly cookie + bcrypt untuk hash password |
| Validasi | Zod (frontend & backend) |
| Keamanan | Helmet, express-rate-limit, sanitize-html, CORS terbatas |
| Dokumentasi API | Swagger UI (OpenAPI 3.0), tersaji di /api/docs |
Frontend dan backend adalah dua aplikasi terpisah (seapedia-vintage/ dan seapedia-api/) yang berkomunikasi lewat REST API.
SEAPEDIA/
├── seapedia-api/ Backend (Express + Prisma)
│ ├── prisma/
│ │ ├── schema.prisma Skema database lengkap
│ │ └── seed.js Data demo (akun, toko, produk, voucher, promo)
│ └── src/
│ ├── constants/enums.js Satu sumber kebenaran untuk semua aturan bisnis
│ ├── middleware/ requireAuth, requireRole, errorHandler
│ ├── modules/ Satu folder per domain (auth, product, order, dst)
│ ├── docs/openapi.js Spesifikasi Swagger
│ └── app.js
│
├── seapedia-vintage/ Frontend (React + Vite)
│ └── src/
│ ├── api/ Klien API per domain
│ ├── context/ AuthContext, CartContext
│ ├── components/ Komponen reusable (UI, ProtectedRoute, ErrorBoundary)
│ ├── constants/ orderStatus.js - status & label bersama
│ └── pages/ Dikelompokkan per peran (buyer/, seller/, driver/, admin/)
│
└── DEPLOYMENT.md Panduan deploy ke Render + Vercel + Neon
Butuh Node.js ≥ 18.
- Daftar di neon.tech, buat project baru.
- Salin connection string Postgres yang diberikan.
cd seapedia-api
cp .env.example .env
# Buka .env, isi DATABASE_URL dengan connection string dari Neon,
# dan JWT_SECRET dengan string acak (generate: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))")
npm install
npm run prisma:migrate -- --name init
npm run seed
npm run devServer berjalan di http://localhost:4000. Cek http://localhost:4000/api/health.
cd seapedia-vintage
cp .env.example .env
npm install
npm run devBuka http://localhost:5173.
Akun Admin tidak bisa didaftarkan lewat form register publik (sengaja dibatasi - lihat Aturan Bisnis). Cara membuat admin baru:
- Cara termudah: pakai akun demo
admin_akardari seed data (lihat tabel di bawah). - Cara manual: jalankan
npx prisma studiodi folderseapedia-api, buka tabelUser, buat user baru lalu tambahkan baris di tabelUserRoledenganrole = "ADMIN"mengarah ke user tersebut.
seapedia-api/.env
| Variabel | Contoh | Keterangan |
|---|---|---|
PORT |
4000 |
Port server backend |
NODE_ENV |
development |
production saat deploy |
DATABASE_URL |
postgresql://... |
Connection string Postgres (Neon) |
JWT_SECRET |
string acak ≥32 karakter | Kunci penandatanganan token sesi |
JWT_EXPIRES_IN |
7d |
Masa berlaku sesi login |
CLIENT_ORIGIN |
http://localhost:5173 |
Origin frontend yang diizinkan CORS |
ADMIN_EMAIL / ADMIN_USERNAME / ADMIN_PASSWORD |
- | Dipakai seed.js untuk akun admin pertama |
seapedia-vintage/.env
| Variabel | Contoh | Keterangan |
|---|---|---|
VITE_API_URL |
http://localhost:4000/api |
Base URL backend |
Seluruhnya dibuat oleh npm run seed. Password sama untuk memudahkan demo.
| Username | Password | Role | Catatan |
|---|---|---|---|
admin_akar |
Admin123! |
Admin | Akses dashboard monitoring & voucher/promo |
toko_hutan |
Seller123! |
Seller | Toko "Toko Hutan Akar" (kategori tumbuhan) |
kios_waktu |
Seller123! |
Seller | Toko "Kios Waktu" (elektronik & arloji vintage) |
pustaka_lama |
Seller123! |
Seller | Toko "Pustaka Lama" (buku & alat tulis) |
pembeli_akar |
Buyer123! |
Buyer | Saldo awal Rp500.000 |
kurir_akar |
Driver123! |
Driver | - |
pengembara_akar |
Pengembara123! |
Buyer + Seller + Driver | Untuk demo alur pemilihan role aktif |
Voucher & Promo demo (Level 4): kode AKAR10 (diskon 10%, maks Rp20rb), HEMAT25K (potongan tetap Rp25rb), PROMO_AWAL (diskon 15%, maks Rp30rb) - bisa langsung dipakai saat checkout tanpa setup tambahan.
Bagian ini mendokumentasikan seluruh keputusan desain yang PDF tugas izinkan untuk ditentukan sendiri.
Satu cart hanya boleh berisi produk dari satu toko. Kalau buyer mencoba menambah produk dari toko lain saat cart sudah terisi, backend menolak dengan kode error STORE_CONFLICT dan frontend menampilkan modal konfirmasi "Kosongkan keranjang untuk melanjutkan?" - bukan sekadar pesan error. Implementasi: seapedia-api/src/modules/cart/cart.service.js, UI: seapedia-vintage/src/components/AddToCartControl.jsx.
- Voucher dan Promo tidak dapat digabung dalam satu checkout - buyer memilih salah satu.
- Urutan perhitungan (lihat
seapedia-api/src/modules/order/order.pricing.js):
subtotal = Σ(harga × qty)
discountAmount = nilai diskon dari voucher ATAU promo (pilih salah satu)
taxableBase = subtotal - discountAmount
ppnAmount = floor(taxableBase × 12%) ← ongkir TIDAK dikenai PPN
deliveryFee = flat per metode pengiriman
totalAmount = taxableBase + ppnAmount + deliveryFee
- Ongkir flat: Instant Rp25.000, Next Day Rp15.000, Regular Rp9.000.
- Voucher wajib punya
expiresAt+usageLimit; Promo wajib punyaexpiresAt(tanpa batas pemakaian, berlaku untuk semua buyer selama belum kedaluwarsa).
- Job pengiriman baru muncul untuk Driver setelah Seller memproses order (status
Menunggu Kurir). - Satu order hanya bisa diambil oleh satu Driver (dijamin lewat conditional update di database, aman dari race condition dua driver klik bersamaan).
- Satu Driver hanya boleh punya satu job aktif pada satu waktu - harus menyelesaikan job berjalan sebelum mengambil job baru.
- Earning Driver = 80% dari ongkir pesanan (20% sisanya dianggap fee platform). Dikreditkan otomatis ke wallet Driver saat job dikonfirmasi selesai.
| Metode Pengiriman | Batas Waktu (SLA) |
|---|---|
| Instant | 1 hari |
| Next Day | 2 hari |
| Regular | 4 hari |
SLA dihitung sejak Seller memproses order (bukan sejak checkout), disimpan di kolom Order.overdueDeadline. Sistem memakai waktu virtual (tabel SystemClock, bukan waktu asli komputer) supaya bisa disimulasikan kapan saja tanpa menunggu hari sungguhan berlalu.
Cara simulasi: Login sebagai Admin → Dashboard Admin → masukkan jumlah hari → klik "Majukan Waktu". Aksi ini otomatis:
- Memajukan
SystemClock.virtualNow. - Mencari semua order yang
overdueDeadline-nya sudah lewat dan masih berstatusMenunggu Kurir/Sedang Dikirim. - Memindahkan order tersebut ke status
Dikembalikan. - Mengembalikan
totalAmountpenuh ke wallet Buyer (tercatat sebagai transaksiREFUND). - Membalik pendapatan Seller yang sudah tercatat (transaksi
SELLER_INCOME_REVERSAL) - supaya laporan pendapatan Seller tetap akurat. - Mengembalikan stok produk sesuai kuantitas tiap item.
Seluruh proses berjalan dalam transaksi database, dengan pengecekan status ganda (idempotency guard) untuk mencegah satu order diproses refund dua kali.
- Satu username non-admin boleh memiliki lebih dari satu role (Buyer/Seller/Driver) sekaligus.
- Setelah login, kalau user punya >1 role non-admin, sistem meminta pemilihan role aktif sebelum masuk ke dashboard privat manapun.
- Otorisasi selalu memakai role aktif saat itu, bukan daftar seluruh role yang dimiliki - kalau role aktif adalah Buyer, endpoint Seller tetap ditolak meski username itu juga terdaftar sebagai Seller.
- Admin adalah akun privileged terpisah: tidak bisa didaftarkan lewat form publik, dan tidak ditawari fitur "tambah role" seperti akun non-admin.
Ringkasan implementasi untuk tiap area yang diminta di Level 7:
| Area | Implementasi |
|---|---|
| SQL Injection | Seluruh akses database memakai Prisma ORM (parameterized query otomatis). Tidak ada satu pun $queryRaw/$executeRaw atau string SQL manual di codebase. |
| XSS | Komentar ulasan publik disanitasi dengan sanitize-html (seluruh tag HTML dibuang) sebelum disimpan ke database - bukan hanya saat ditampilkan. React juga meng-escape output secara default; dangerouslySetInnerHTML tidak dipakai sama sekali di frontend. |
| Validasi Input | Setiap endpoint yang menerima body/query memvalidasi dengan skema Zod sebelum diproses; error dikembalikan dalam format konsisten { field, message }. |
| Password | Di-hash dengan bcrypt (10 salt rounds), tidak pernah disimpan atau dikembalikan dalam bentuk plain text. |
| Sesi | JWT disimpan di cookie httpOnly (tidak bisa dibaca lewat JavaScript browser, tahan dari pencurian token via XSS), secure + sameSite=none otomatis aktif di production. Masa berlaku 7 hari (JWT_EXPIRES_IN). Logout menghapus cookie di server. |
| Session Expiry Handling | Interceptor axios di frontend mendeteksi 401 di tengah pemakaian, membersihkan state login, dan mengarahkan user ke halaman login dengan pesan jelas - bukan error mentah di halaman acak. |
| RBAC (Role-Based Access Control) | Middleware requireAuth + requireRole diterapkan di setiap endpoint privat (diaudit satu per satu, lihat daftar lengkap di kode). Role diverifikasi dari payload JWT yang sudah ditandatangani server, bukan dari data yang dikirim client - frontend tidak pernah dipercaya sebagai sumber kebenaran otorisasi. |
| Ownership Check | Semua resource milik user (produk, alamat, item cart, order, delivery job) diverifikasi kepemilikannya di service layer sebelum diizinkan diubah/dihapus - bukan hanya mengandalkan role. |
| Rate Limiting | Limiter global (300 req/15 menit) di seluruh API, plus limiter lebih ketat khusus endpoint sensitif: login/register (20 req/15 menit) dan submit ulasan (10 req/jam) untuk mencegah brute force dan spam. |
| Security Headers | Helmet aktif secara default (mis. X-Content-Type-Options, HSTS). |
| CORS | Dibatasi ke origin frontend spesifik (CLIENT_ORIGIN), bukan wildcard *, dengan credentials: true untuk cookie sesi. |
| Error Handling | Error tak terduga di backend tidak pernah membocorkan detail internal ke client (pesan digeneralisir), tapi tetap di-log penuh di server untuk debugging. React ErrorBoundary mencegah satu error UI menjatuhkan seluruh aplikasi. |
# Tes anti-XSS: kirim script tag di komentar ulasan
curl -X POST http://localhost:4000/api/reviews \
-H "Content-Type: application/json" \
-d '{"name":"Tester","rating":5,"comment":"<script>alert(1)</script>Halo"}'
# Hasil yang benar: field "comment" pada response HANYA berisi "Halo",
# tag <script> sudah dibuang sebelum masuk database.
# Tes anti-SQLi: masukkan payload SQL-like di username login
curl -X POST http://localhost:4000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin_akar OR 1=1--","password":"apa saja"}'
# Hasil yang benar: "Username atau kata sandi salah" (401) - payload
# diperlakukan sebagai string biasa oleh Prisma, tidak memengaruhi query.Dokumentasi interaktif (Swagger UI) tersedia di:
http://localhost:4000/api/docs
Mencakup seluruh ~50 endpoint dari Level 1-6, lengkap dengan contoh request body dan skema response. Bisa langsung dicoba dari browser lewat tombol "Try it out" (kirim request nyata ke backend lokal kamu).
Urutan ini menguji seluruh alur utama dari Level 1 sampai Level 6 dalam satu sesi demo:
- Guest - buka
/, jelajahi katalog produk lintas kategori (tumbuhan, elektronik vintage, buku), buka detail salah satu produk, submit ulasan publik tanpa login. - Registrasi & Login - daftar akun baru sebagai Buyer, logout, login lagi memakai akun demo multi-role
pengembara_akar/Pengembara123!- perhatikan layar pemilihan role muncul karena akun ini punya Buyer+Seller+Driver. - Seller (
toko_hutan/Seller123!) - Dashboard → Kelola Produk → tambah 1 produk baru dengan stok terbatas. - Buyer (
pembeli_akar/Buyer123!) - Dashboard → Dompet → top up saldo → Alamat → tambah alamat → cari produk dari Seller di atas → tambah ke keranjang → Checkout → coba kode voucherAKAR10→ selesaikan pembayaran. Perhatikan status awal order: Sedang Dikemas. - Seller lagi - Dashboard → Pesanan Masuk → klik Proses Pesanan (status → Menunggu Kurir, job otomatis muncul untuk Driver).
- Driver (
kurir_akar/Driver123!) - Dashboard → Papan Tugas → ambil job yang baru muncul (status order → Sedang Dikirim) → buka detail job → Konfirmasi Selesai (status order → Pesanan Selesai, earning masuk wallet Driver). - Cek Laporan - buka Laporan Belanja (Buyer) dan Laporan Pendapatan (Seller), pastikan angkanya konsisten dengan transaksi di atas.
- Admin (
admin_akar/Admin123!) - Dashboard Admin → jelajahi tab monitoring (Users/Stores/Products/Orders/Deliveries) → buat 1 order baru sampai status "Menunggu Kurir" tapi jangan diambil Driver → masukkan angka hari lebih dari SLA metode pengirimannya (Instant=1, Next Day=2, Regular=4) di kotak "Majukan Waktu" → klik tombol → order tersebut otomatis pindah ke Dikembalikan, saldo Buyer ter-refund, dan hasilnya tampil langsung di layar. - Kelola Diskon - dari Dashboard Admin, buka Voucher/Promo, buat kode baru, pastikan langsung bisa dipakai di checkout Buyer.
| Level | Fitur | Status |
|---|---|---|
| 1 | Marketplace publik, autentikasi, role awareness, ulasan | ✅ Selesai |
| 2 | Manajemen toko & produk Seller | ✅ Selesai |
| 3 | Wallet, alamat, cart, checkout Buyer | ✅ Selesai |
| 4 | Voucher/Promo, proses order Seller, laporan | ✅ Selesai |
| 5 | Alur kerja Driver (cari/ambil/selesaikan job) | ✅ Selesai |
| 6 | Monitoring Admin & auto-refund overdue | ✅ Selesai |
| 7 | Security hardening & dokumentasi akhir | ✅ Selesai |
Lihat DEPLOYMENT.md untuk panduan lengkap deploy ke Render (backend) + Vercel (frontend) + Neon (database), termasuk semua environment variable yang perlu diatur di masing-masing platform.