diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 034834d..f0343c3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -230,10 +230,17 @@ Alur data mengikuti pola yang sama: **UI (`app/`) → server actions (`features/ ### Trust & organizer (`server/services/trust.service.ts`) -- **Verified:** kolom `User.isVerified` (default false; set manual / seed / admin ke depan). +- **Verified Organizer:** dihitung dari `OrganizerVerification.status === "APPROVED"` (lihat `lib/trust.ts → isVerifiedOrganizer()`). Tidak lagi pakai `User.isVerified`. - **Trip leader:** heuristik `jumlah trip dibuat ≥ TRIP_LEADER_MIN_TRIPS` (`lib/trust.ts`). - **Jumlah trip dibuat & rating organizer:** dihitung agregat dari DB (rating = rata-rata `TripReview` pada semua trip sang organizer). +### Verifikasi organizer (KYC ringan) + +- Model `OrganizerVerification` (1-1 ke `User`) menyimpan KTP (nama, NIK unik, tanggal lahir, alamat), URL foto KTP & selfie, data rekening bank, dan status `PENDING` / `APPROVED` / `REJECTED` + audit reviewer. +- Alur: user submit di `/verify` (`features/organizer/`) → admin review di `/admin/verifications` → setujui/tolak. +- **Gate trip berbayar:** `createTripAction` menolak `price > 0` jika user belum `APPROVED` (`organizerService.isApproved`). +- **Akses admin:** `lib/admin.ts → isAdminEmail()` membaca `ADMIN_EMAILS` (env, comma-separated). + --- # 🧠 Final Principle diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..9820bcc --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,175 @@ +# 🔒 Privacy Policy (Kebijakan Privasi) SeTrip + +**Terakhir diperbarui: 2026-04-27** + +SeTrip menghargai privasi Anda. Kebijakan Privasi ini menjelaskan bagaimana kami mengumpulkan, menggunakan, dan melindungi informasi Anda saat menggunakan platform SeTrip. + +Dengan menggunakan SeTrip, Anda menyetujui praktik yang dijelaskan dalam Kebijakan Privasi ini. + +--- + +# 1. Informasi yang Kami Kumpulkan + +Kami dapat mengumpulkan informasi berikut: + +## a. Informasi Akun + +- Nama +- Email +- Nomor telepon +- Password (disimpan dalam bentuk terenkripsi) + +## b. Informasi Profil + +- Foto profil +- Deskripsi diri +- Riwayat trip + +## c. Informasi Transaksi + +- Data booking trip +- Status pembayaran +- Riwayat aktivitas + +## d. Informasi Teknis + +- Alamat IP +- Browser +- Perangkat yang digunakan +- Log aktivitas + +--- + +# 2. Cara Kami Menggunakan Informasi + +Kami menggunakan informasi Anda untuk: + +- Membuat dan mengelola akun +- Menghubungkan pengguna dengan organizer +- Memproses booking dan aktivitas trip +- Meningkatkan layanan dan pengalaman pengguna +- Mengirim notifikasi terkait aktivitas +- Mencegah penipuan dan penyalahgunaan + +--- + +# 3. Pembagian Informasi + +Kami tidak menjual data pribadi Anda. + +Namun, kami dapat membagikan informasi dalam kondisi berikut: + +## a. Dengan Organizer + +- Informasi dasar seperti nama dan kontak dapat dibagikan kepada organizer untuk keperluan trip + +## b. Dengan Penyedia Layanan + +- Untuk kebutuhan teknis (hosting, analytics, dll) + +## c. Kewajiban Hukum + +- Jika diminta oleh hukum atau otoritas berwenang + +--- + +# 4. Keamanan Data + +Kami berusaha melindungi data Anda dengan: + +- Enkripsi password +- Pembatasan akses data +- Sistem keamanan standar industri + +Namun, tidak ada sistem yang 100% aman. + +--- + +# 5. Penyimpanan Data + +Kami menyimpan data Anda selama: + +- Akun Anda aktif +- Dibutuhkan untuk keperluan layanan + +Data dapat dihapus atas permintaan pengguna, kecuali diwajibkan oleh hukum untuk disimpan. + +--- + +# 6. Hak Pengguna + +Anda memiliki hak untuk: + +- Mengakses data pribadi Anda +- Memperbarui informasi +- Menghapus akun +- Menarik persetujuan + +--- + +# 7. Cookie & Tracking + +SeTrip dapat menggunakan: + +- Cookie +- Teknologi pelacakan sederhana + +Untuk: + +- Menyimpan sesi login +- Meningkatkan pengalaman pengguna + +--- + +# 8. Layanan Pihak Ketiga + +SeTrip dapat menggunakan layanan pihak ketiga seperti: + +- Hosting +- Analytics +- Payment gateway (di masa depan) + +Kami tidak bertanggung jawab atas kebijakan privasi pihak ketiga tersebut. + +--- + +# 9. Perlindungan terhadap Penipuan + +Kami dapat menggunakan data untuk: + +- Mendeteksi aktivitas mencurigakan +- Mencegah penipuan +- Melindungi pengguna lain + +--- + +# 10. Perubahan Kebijakan Privasi + +SeTrip dapat memperbarui Kebijakan Privasi ini sewaktu-waktu. + +Pengguna disarankan untuk: + +- Membaca secara berkala +- Memahami perubahan yang berlaku + +--- + +# 11. Kontak + +Jika Anda memiliki pertanyaan mengenai Kebijakan Privasi ini, silakan hubungi: + +Email: [support@setrip.com](mailto:support@setrip.com) + +--- + +# ✅ Persetujuan + +Dengan menggunakan SeTrip, Anda menyatakan bahwa: + +- Telah membaca +- Memahami +- Menyetujui Kebijakan Privasi ini + +--- + +**End of Document** diff --git a/README.md b/README.md index d1c6062..2b3adc0 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tid Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini. +**Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, selfie, dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip. + ### 3. Peserta: mencari trip 1. **Beranda** (`/`) dan **Open Trip** (`/trips`) menampilkan trip **OPEN** dengan tanggal berangkat yang masih relevan (`tripService.getOpenTrips`). @@ -95,7 +97,8 @@ Alur ini menggambarkan satu peserta dari pertama kali mendaftar sampai pembayara |--------|-------------| | Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer | | Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual | -| Organizer (kepercayaan) | `User.isVerified`; agregat rating & jumlah trip dibuat dihitung dari data ulasan & trip | +| Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, selfie, rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. | +| Persetujuan T&C / Privasi | `User.acceptedTermsAndPrivacy` + `User.acceptedAt`, dicentang saat registrasi (link ke `/terms` & `/privacy`). | ## Menjalankan secara lokal diff --git a/TERMS.md b/TERMS.md new file mode 100644 index 0000000..8dde10f --- /dev/null +++ b/TERMS.md @@ -0,0 +1,197 @@ +# 📜 Terms & Conditions (Syarat & Ketentuan) SeTrip + +**Terakhir diperbarui: 2026-04-27** + +Selamat datang di SeTrip. Dengan mengakses atau menggunakan platform SeTrip, Anda menyetujui untuk terikat oleh Syarat & Ketentuan berikut. + +--- + +# 1. Definisi + +Dalam dokumen ini: + +- **SeTrip**: Platform yang menghubungkan pengguna dengan penyelenggara trip. +- **Pengguna (User)**: Individu yang menggunakan aplikasi SeTrip. +- **Organizer (Penyelenggara)**: Pengguna yang membuat dan mengelola trip. +- **Trip**: Kegiatan perjalanan yang dibuat oleh organizer. +- **Platform**: Website atau aplikasi SeTrip. + +--- + +# 2. Peran SeTrip + +SeTrip bertindak sebagai **platform perantara** yang menghubungkan pengguna dan organizer. + +SeTrip: + +- **Bukan penyelenggara trip** +- **Tidak terlibat langsung dalam pelaksanaan perjalanan** +- **Tidak bertanggung jawab atas kegiatan selama trip berlangsung** + +--- + +# 3. Penggunaan Platform + +Dengan menggunakan SeTrip, Anda menyatakan bahwa: + +- Berusia minimal 18 tahun atau memiliki izin dari wali +- Memberikan informasi yang benar dan akurat +- Tidak menggunakan platform untuk: + - Penipuan + - Aktivitas ilegal + - Penyebaran informasi palsu + +--- + +# 4. Akun Pengguna + +- Pengguna bertanggung jawab atas keamanan akun masing-masing +- Dilarang membagikan akun kepada pihak lain +- SeTrip berhak menangguhkan atau menghapus akun jika terjadi pelanggaran + +--- + +# 5. Trip & Booking + +- Organizer bertanggung jawab atas seluruh informasi trip +- Pengguna wajib membaca detail trip sebelum melakukan join +- Dengan melakukan join trip, pengguna menyetujui seluruh ketentuan trip yang dibuat oleh organizer + +--- + +# 6. Pembayaran + +- Pembayaran dilakukan sesuai metode yang tersedia di platform +- Dalam fase awal, pembayaran dapat dilakukan langsung kepada organizer +- SeTrip tidak menjamin keamanan transaksi yang dilakukan di luar platform + +--- + +# 7. Pembatalan & Refund + +- Kebijakan pembatalan ditentukan oleh organizer +- SeTrip tidak bertanggung jawab atas refund yang tidak diberikan oleh organizer +- Pengguna disarankan untuk memahami kebijakan sebelum melakukan pembayaran + +--- + +# 8. Tanggung Jawab Organizer + +Organizer wajib: + +- Memberikan informasi trip yang jelas dan akurat +- Menjalankan trip sesuai deskripsi +- Bertanggung jawab atas keselamatan peserta selama trip + +--- + +# 9. Risiko Perjalanan + +Pengguna memahami bahwa aktivitas perjalanan, terutama kegiatan outdoor, memiliki risiko termasuk namun tidak terbatas pada: + +- Cedera +- Kecelakaan +- Cuaca ekstrem +- Kondisi tak terduga lainnya + +Dengan mengikuti trip, pengguna menyatakan: + +> Mengikuti kegiatan secara sadar dan bertanggung jawab atas risiko pribadi + +--- + +# 10. Batasan Tanggung Jawab + +SeTrip tidak bertanggung jawab atas: + +- Kerugian finansial +- Cedera atau kecelakaan +- Kegagalan pelaksanaan trip +- Tindakan organizer atau pengguna lain + +--- + +# 11. Larangan Transaksi di Luar Platform + +Pengguna disarankan untuk tidak melakukan transaksi di luar platform. + +SeTrip tidak bertanggung jawab atas: + +- Penipuan +- Kerugian +- Masalah lain yang terjadi akibat transaksi di luar sistem SeTrip + +--- + +# 12. Sistem Review + +- Pengguna dapat memberikan review setelah trip +- Review harus jujur dan tidak mengandung unsur fitnah +- SeTrip berhak menghapus review yang melanggar + +--- + +# 13. Penangguhan & Penghentian Akun + +SeTrip berhak untuk: + +- Menangguhkan akun +- Menghapus akun +- Membatasi akses + +Jika pengguna: + +- Melanggar ketentuan +- Terindikasi melakukan penipuan +- Menyalahgunakan platform + +--- + +# 14. Perubahan Layanan + +SeTrip dapat: + +- Mengubah fitur +- Menghentikan layanan +- Menambahkan kebijakan baru + +Tanpa pemberitahuan sebelumnya + +--- + +# 15. Perubahan Syarat & Ketentuan + +SeTrip dapat memperbarui Syarat & Ketentuan ini kapan saja. + +Pengguna disarankan untuk: + +- Membaca secara berkala +- Memahami perubahan yang berlaku + +--- + +# 16. Hukum yang Berlaku + +Syarat & Ketentuan ini diatur oleh hukum yang berlaku di Republik Indonesia. + +--- + +# 17. Kontak + +Jika Anda memiliki pertanyaan, silakan hubungi: + +Email: [support@setrip.com](mailto:support@setrip.com) + +--- + +# ✅ Persetujuan + +Dengan menggunakan SeTrip, Anda menyatakan bahwa: + +- Telah membaca +- Memahami +- Menyetujui seluruh isi Syarat & Ketentuan ini + +--- + +**End of Document** diff --git a/app/admin/verifications/layout.tsx b/app/admin/verifications/layout.tsx new file mode 100644 index 0000000..de7a0c3 --- /dev/null +++ b/app/admin/verifications/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Admin · Review Verifikasi Organizer", + description: "Halaman admin untuk meninjau pengajuan verifikasi organizer.", + alternates: { canonical: "/admin/verifications" }, + robots: { index: false, follow: false }, +}; + +export default function AdminVerificationsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/app/admin/verifications/page.tsx b/app/admin/verifications/page.tsx new file mode 100644 index 0000000..7bf57fa --- /dev/null +++ b/app/admin/verifications/page.tsx @@ -0,0 +1,79 @@ +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { isAdminEmail } from "@/lib/admin"; +import { organizerRepo } from "@/server/repositories/organizer.repo"; +import { ReviewCard } from "@/features/organizer/components/review-card"; + +type Tab = "PENDING" | "APPROVED" | "REJECTED"; + +interface PageProps { + searchParams: Promise<{ tab?: string }>; +} + +export default async function AdminVerificationsPage({ searchParams }: PageProps) { + const session = await getServerSession(authOptions); + if (!session?.user) redirect("/login?callbackUrl=/admin/verifications"); + if (!isAdminEmail(session.user.email)) { + return ( +
+

+ Halaman ini hanya untuk admin SeTrip. +

+
+ ); + } + + const params = await searchParams; + const tab: Tab = + params.tab === "APPROVED" || params.tab === "REJECTED" ? params.tab : "PENDING"; + + const items = await organizerRepo.listByStatus(tab); + + const tabs: { key: Tab; label: string }[] = [ + { key: "PENDING", label: "Pending" }, + { key: "APPROVED", label: "Disetujui" }, + { key: "REJECTED", label: "Ditolak" }, + ]; + + return ( +
+
+

+ Review Verifikasi Organizer +

+

+ Periksa data KTP, selfie, dan rekening sebelum menyetujui. +

+
+ +
+ {tabs.map((t) => ( + + {t.label} + + ))} +
+ + {items.length === 0 ? ( +
+

Tidak ada data.

+
+ ) : ( +
+ {items.map((v) => ( + + ))} +
+ )} +
+ ); +} diff --git a/app/create-trip/page.tsx b/app/create-trip/page.tsx index 6aeccd4..bb3331b 100644 --- a/app/create-trip/page.tsx +++ b/app/create-trip/page.tsx @@ -1,44 +1,11 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; import Link from "next/link"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { createTripAction } from "@/features/trip/actions"; -import { ImageUrlInput } from "@/features/trip/components/image-url-input"; -import { formatLocalCalendarYmd } from "@/lib/trip-dates"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { organizerService } from "@/server/services/organizer.service"; +import { CreateTripForm } from "@/features/trip/components/create-trip-form"; -const SAMPLE_MOUNTAINS = [ - { name: "Gunung Papandayan", location: "Garut, Jawa Barat" }, - { name: "Gunung Ciremai", location: "Kuningan, Jawa Barat" }, - { name: "Gunung Pangrango", location: "Bogor/Cianjur, Jawa Barat" }, - { name: "Gunung Gede", location: "Bogor/Cianjur, Jawa Barat" }, - { name: "Gunung Tangkuban Parahu", location: "Bandung, Jawa Barat" }, - { name: "Gunung Bukit Tunggul", location: "Bandung, Jawa Barat" }, - { name: "Gunung Malabar", location: "Bandung, Jawa Barat" }, - { name: "Gunung Guntur", location: "Garut, Jawa Barat" }, -]; - -function formatRupiahInput(value: string): string { - const num = value.replace(/\D/g, ""); - return num.replace(/\B(?=(\d{3})+(?!\d))/g, "."); -} - -function parseRupiahInput(value: string): string { - return value.replace(/\./g, ""); -} - -export default function CreateTripPage() { - const { data: session } = useSession(); - const router = useRouter(); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); - const [priceDisplay, setPriceDisplay] = useState(""); +export default async function CreateTripPage() { + const session = await getServerSession(authOptions); if (!session?.user) { return ( @@ -51,7 +18,7 @@ export default function CreateTripPage() { Kamu harus login untuk membuat trip.

Login @@ -61,69 +28,8 @@ export default function CreateTripPage() { ); } - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setError(""); - - if (!startDate) { - setError("Tanggal berangkat harus diisi"); - return; - } - - setLoading(true); - - const formData = new FormData(e.currentTarget); - // Tanggal dari picker → string tanggal untuk server action - formData.set("date", formatLocalCalendarYmd(startDate)); - if (endDate) { - const startYmd = formatLocalCalendarYmd(startDate); - const endYmd = formatLocalCalendarYmd(endDate); - // Satu hari: tanggal pulang sama dengan berangkat → jangan kirim endDate (trip 1 hari) - if (endYmd !== startYmd) { - formData.set("endDate", endYmd); - } - } - // Set raw price number - formData.set("price", parseRupiahInput(priceDisplay)); - - const result = await createTripAction(formData); - - setLoading(false); - - if (result.error) { - setError(result.error); - } else if (result.tripId) { - router.push(`/trips/${result.tripId}`); - } - } - - function handleMountainSelect(e: React.ChangeEvent) { - const selected = SAMPLE_MOUNTAINS.find((m) => m.name === e.target.value); - if (selected) { - const form = e.target.form; - if (form) { - const mountainInput = form.elements.namedItem( - "mountain" - ) as HTMLInputElement; - const locationInput = form.elements.namedItem( - "location" - ) as HTMLInputElement; - mountainInput.value = selected.name; - locationInput.value = selected.location; - } - } - } - - function handleDateChange(dates: [Date | null, Date | null]) { - const [start, end] = dates; - setStartDate(start); - setEndDate(end); - } - - function handlePriceChange(e: React.ChangeEvent) { - const raw = e.target.value.replace(/\D/g, ""); - setPriceDisplay(raw ? formatRupiahInput(raw) : ""); - } + const verification = await organizerService.getStatusForUser(session.user.id); + const isVerifiedOrganizer = verification?.status === "APPROVED"; return (
@@ -134,243 +40,54 @@ export default function CreateTripPage() {

-
- {error && ( -
- {error} -
- )} + {!isVerifiedOrganizer && ( + + )} -
- {/* Mountain Quick Picker */} -
- - -
+ +
+ ); +} -
- - -
+function VerificationBanner({ + status, +}: { + status: "PENDING" | "APPROVED" | "REJECTED" | null; +}) { + if (status === "PENDING") { + return ( +
+

+ ⏳ Verifikasi sedang diproses +

+

+ Pengajuan verifikasi-mu masih ditinjau admin. Sementara menunggu, kamu + masih bisa membuat trip gratis (harga 0). +

+
+ ); + } -
-
- - -
-
- - -
-
- -
- -