diff --git a/AGENTS.md b/AGENTS.md index 383842a..62a5340 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,9 +12,15 @@ Fondasi utama skema database: table User: -- id String @id -- name String @unique +- id String @id @default(uuid()) +- username String @unique +- name String +- password String - photo String? +- role String @default("user") // "user" or "superadmin" +- is_active Boolean @default(true) +- created_at DateTime @default(now()) +- updated_at DateTime @updatedAt - orders Order[] @relation("CreatedOrders") - purchases Submission[] @@ -74,15 +80,12 @@ table SubmissionItem: # 2. CORE AUTHENTICATION FLOW -- DILARANG menggunakan NextAuth/library auth eksternal. -- Setiap kali web diakses, jalankan pengecekan: - 1. Cek `localStorage` untuk `user_id`. - 2. Jika belum ada, generate UUID baru, simpan ke `localStorage`. - 3. Lakukan verifikasi ke database/server action apakah `user_id` tersebut sudah terdaftar di tabel `User`. - 4. Jika belum terdaftar, wajib munculkan Modal Profile yang tidak bisa ditutup. - 5. Modal meminta input "Nama" (Wajib) dan "Foto Profile" (Opsional). - 6. Validasi: Nama harus UNIK di tabel User. Setelah valid, simpan ke database menggunakan `user_id` sebagai `id`. - 7. Jika `user_id` sudah terdaftar, lewati modal dan muat data user secara mulus. +- DILARANG menggunakan NextAuth/library auth eksternal. Buat custom authentication menggunakan JWT (atau hash-based token) yang disimpan di HTTP-only cookies atau localStorage. +- Fitur Token: Tanpa waktu expired (no expiration) dan mendukung multi-device login. +- Terdapat halaman `/login` dan `/register` untuk pengguna. +- Default role saat registrasi adalah `user`. +- Harus ada seeder untuk membuat akun superadmin default: Username: `superadmin` (atau spesifik), Password: `titipin123!`, Role: `superadmin`. +- Middleware/Route Protection: Hanya user yang sudah login yang bisa mengakses halaman utama aplikasi. Halaman manajemen user hanya bisa diakses oleh role `superadmin`. # 3. MENU NAVIGATION ORDER @@ -92,7 +95,8 @@ Urutan navigasi menu utama pada aplikasi adalah sebagai berikut: 2. Jasa Order Saya 3. Pesanan Saya 4. Laporan -5. Profile +5. User Management (Hanya Superadmin) +6. Profile # 4. PAGE SPECIFICATIONS & STEP-BY-STEP FLOW @@ -148,10 +152,22 @@ Urutan navigasi menu utama pada aplikasi adalah sebagai berikut: - Visualisasi berupa Bar Chart (Grafik) dan Data Grid (Tabel) yang diurutkan secara proporsional. - Mendukung filter Bulan dan Tahun berbasis server-side. -## Step 6: Halaman Profile (/profile) +## Step 6: Halaman User Management (/users) - KHUSUS SUPERADMIN + +- Hanya muncul jika current user memiliki role `superadmin`. +- Menampilkan tabel daftar user dengan pagination (default 10 per halaman). +- Fitur Superadmin: + - Membuat data user baru. + - Mengedit password user lain. + - Activate dan Deactivate user. + - Menghapus user. +- Proteksi Data Superadmin: Data superadmin tidak bisa dihapus atau di-deactivate (terkunci), hanya bisa ganti password. + +## Step 7: Halaman Profile (/profile) - Form untuk mengedit Nama dan Foto Profile milik current user. -- Validasi nama unik tetap berlaku saat proses update. +- Terdapat form Ganti Password yang mewajibkan input: Password Lama, Password Baru, dan Konfirmasi Password Baru. +- Validasi nama dan verifikasi password lama tetap berlaku saat proses update. # 6. WORKFLOW EXECUTION diff --git a/README.md b/README.md index 8f7c77e..957c770 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # TitipIn - Sistem Titip Pesanan -Aplikasi web modern berbasis Next.js untuk mempermudah sistem "jasa titip" (jastip) atau pesanan bersama. Aplikasi ini berjalan dengan arsitektur serverless (Server Actions) dan menggunakan sistem autentikasi stateless (berbasis UUID di LocalStorage). +Aplikasi web modern berbasis Next.js untuk mempermudah sistem "jasa titip" (jastip) atau pesanan bersama. Aplikasi ini berjalan dengan arsitektur serverless (Server Actions) dan menggunakan sistem autentikasi **stateless berbasis JWT yang disimpan secara aman di HTTP-only Cookies**. Dilengkapi dengan Role-Based Access Control (RBAC) untuk manajemen akses pengguna dan Superadmin. ## Kebutuhan Sistem (Prerequisites) Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak berikut: - **Node.js**: Versi 18.x atau lebih baru (direkomendasikan versi LTS terbaru). -- **npm**: (biasanya sudah termasuk saat instalasi Node.js). +- **Yarn / npm**: (Disarankan menggunakan Yarn). - **PostgreSQL**: Database server berjalan (versi 13 atau lebih baru direkomendasikan). ## Instalasi @@ -13,12 +13,12 @@ Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak 2. Buka terminal/command prompt. 3. Jalankan perintah instalasi dependensi: ```bash - npm install + yarn install ``` - *Catatan: Pastikan dependensi Prisma terinstal pada versi stabil (contoh: `@prisma/client@5`).* + *(atau `npm install` jika tidak menggunakan yarn).* ## Pengaturan Environment -Buat sebuah file bernama `.env` di root folder proyek Anda. Konfigurasi kredensial database PostgreSQL Anda seperti format berikut: +Buat sebuah file bernama `.env` di root folder proyek Anda. Konfigurasi kredensial database PostgreSQL Anda beserta kunci rahasia JWT seperti format berikut: ```env DATABASE_HOST="172.10.10.2" @@ -27,26 +27,33 @@ DATABASE_USER="postgres" DATABASE_PASSWORD="eigen3m!" DATABASE_NAME="testing" DATABASE_URL="postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}?schema=public" + +# Kunci rahasia untuk enkripsi sesi JWT +JWT_SECRET="isi-dengan-kunci-rahasia-acak-disini" ``` -*(Sesuaikan IP, port, dan kredensial lain jika server database Anda berbeda).* +*(Sesuaikan IP, port, dan kredensial database jika server database Anda berbeda).* ## Eksekusi Migrasi Database -Aplikasi menggunakan **Prisma ORM** untuk berinteraksi dengan database. Jalankan perintah terminal berikut secara berurutan untuk menyinkronkan skema dan mengaktifkan client: +Aplikasi menggunakan **Prisma ORM** untuk berinteraksi dengan database. Jalankan perintah terminal berikut secara berurutan untuk menyinkronkan skema dan mengisi (seed) data awal: 1. **Generate Prisma Client**: Membangun tipe TypeScript dan library client. ```bash npx prisma generate ``` -2. **Push Skema ke Database**: Membuat struktur tabel-tabel di database (User, Order, Submission, dll.) sesuai dengan `prisma/schema.prisma`. +2. **Push Skema ke Database**: Membuat struktur tabel-tabel di database sesuai dengan `prisma/schema.prisma`. ```bash npx prisma db push ``` +3. **Seed Database**: Mengisi database dengan akun Superadmin bawaan. + ```bash + npx prisma db seed + ``` ## Menjalankan Aplikasi Setelah database siap, Anda bisa menjalankan aplikasi di tahap pengembangan lokal (development mode): ```bash -npm run dev +yarn dev ``` Aplikasi secara bawaan dapat diakses melalui browser dengan URL: @@ -55,9 +62,10 @@ Aplikasi secara bawaan dapat diakses melalui browser dengan URL: --- ### Alur Singkat Aplikasi: -- Akses pertama kali, sistem akan _generate_ UUID unik untuk *device* Anda dan menyimpannya di `localStorage`. -- Aplikasi akan menampilkan Modal Wajib untuk mendaftarkan **Nama** dan opsional **Foto Profil**. +- **Autentikasi:** Akses aplikasi harus melalui halaman login. Jika belum memiliki akun, pengguna dapat mendaftar (register) yang otomatis akan memberikan *role* sebagai `user`. +- **Akun Superadmin:** Secara default, sistem telah menyiapkan akun *Superadmin* (Username: `superadmin`, Password: `titipin123!`). Superadmin memiliki akses eksklusif ke halaman **User Management** untuk mengelola, menonaktifkan, atau menghapus pengguna lain. - Anda dapat mulai: - Membuat Jasa Titip di menu **Jasa Order Saya**. - Mengikuti Jasa Titip teman di menu **Open Order**. - Mengelola pembayaran & melihat rincian di halaman detail. + - Memantau laporan omzet & pengeluaran di menu **Laporan**. diff --git a/package.json b/package.json index bf4d979..c6825e9 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "@base-ui/react": "^1.7.0", "@hookform/resolvers": "^5.9.1", "@prisma/client": "^5.22.0", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", + "jose": "^6.2.10", "lucide-react": "^1.34.0", "next": "16.3.3", "prisma": "^5.22.0", @@ -32,6 +34,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^3.0.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -39,6 +42,11 @@ "eslint": "^9", "eslint-config-next": "16.3.3", "tailwindcss": "^4", - "typescript": "^5" + "ts-node": "^10.9.2", + "tsx": "^4.23.12", + "typescript": "^7.0.2" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1390e9c..f9fa417 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,9 +11,15 @@ datasource db { } model User { - id String @id - name String @unique + id String @id @default(uuid()) + username String @unique + name String + password String photo String? + role String @default("user") + is_active Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt orders Order[] @relation("CreatedOrders") purchases Submission[] } @@ -28,13 +34,17 @@ model Order { creator User @relation("CreatedOrders", fields: [creator_id], references: [id]) available_items AvailableItem[] submissions Submission[] + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } model AvailableItem { id String @id @default(uuid()) order_id String - name String - order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) + name String + order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } model Submission { @@ -46,6 +56,8 @@ model Submission { order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade) items SubmissionItem[] + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } model SubmissionItem { @@ -55,4 +67,6 @@ model SubmissionItem { qty Int @default(1) is_custom Boolean @default(false) submission Submission @relation(fields: [submission_id], references: [id], onDelete: Cascade) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..c0b29da --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,35 @@ +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcryptjs' + +const prisma = new PrismaClient() + +async function main() { + const superadminUsername = 'superadmin' + const superadminPassword = 'titipin123!' + + const hashedPassword = await bcrypt.hash(superadminPassword, 10) + + const superadmin = await prisma.user.upsert({ + where: { username: superadminUsername }, + update: {}, + create: { + username: superadminUsername, + name: 'Super Admin', + password: hashedPassword, + role: 'superadmin', + is_active: true, + }, + }) + + console.log({ superadmin }) +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..9866903 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,9 @@ +import { AppLayout } from "@/components/AppLayout"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/my-orders/[id]/page.tsx b/src/app/(app)/my-orders/[id]/page.tsx similarity index 98% rename from src/app/my-orders/[id]/page.tsx rename to src/app/(app)/my-orders/[id]/page.tsx index a96572d..65d74d3 100644 --- a/src/app/my-orders/[id]/page.tsx +++ b/src/app/(app)/my-orders/[id]/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' -import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder } from '@/app/actions' +import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser } from '@/app/actions' import { Card, CardContent } from '@/components/ui/card' import { Button, buttonVariants } from '@/components/ui/button' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' @@ -47,9 +47,14 @@ export default function OrderDetailPage() { const [searchQuery, setSearchQuery] = useState('') useEffect(() => { - const id = localStorage.getItem('user_id') - setUserId(id) - if (orderId) loadOrder() + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + } + if (orderId) loadOrder() + } + init() }, [orderId]) const loadOrder = async () => { @@ -476,13 +481,13 @@ function SubmissionRow({ isClosed: boolean onUpdate: () => void }) { - const [bill, setBill] = useState(sub.bill || '') + const [bill, setBill] = useState(sub.bill ?? '') const [status, setStatus] = useState(sub.payment_status || 'BELUM_BAYAR') const [saving, setSaving] = useState(false) const handleSave = async () => { setSaving(true) - await updateSubmissionPayment(sub.id, bill ? parseInt(bill) : null, status) + await updateSubmissionPayment(sub.id, bill !== '' ? parseInt(bill as string) : null, status) onUpdate() setSaving(false) } diff --git a/src/app/my-orders/page.tsx b/src/app/(app)/my-orders/page.tsx similarity index 99% rename from src/app/my-orders/page.tsx rename to src/app/(app)/my-orders/page.tsx index e747c9b..ed74848 100644 --- a/src/app/my-orders/page.tsx +++ b/src/app/(app)/my-orders/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { getMyOrders, createOrder, updateOrder, duplicateOrder, updateOrderStatus, deleteOrder } from '@/app/actions' +import { getMyOrders, createOrder, updateOrder, duplicateOrder, updateOrderStatus, deleteOrder, getSessionUser } from '@/app/actions' import { Button, buttonVariants } from '@/components/ui/button' import { cn } from '@/lib/utils' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' @@ -46,11 +46,14 @@ export default function MyOrdersPage() { const ITEMS_PER_PAGE = 5 useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) { - setUserId(id) - loadOrders(id) + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + loadOrders(user.id) } + } + init() }, []) const loadOrders = async (id: string) => { diff --git a/src/app/my-purchases/page.tsx b/src/app/(app)/my-purchases/page.tsx similarity index 99% rename from src/app/my-purchases/page.tsx rename to src/app/(app)/my-purchases/page.tsx index 991b0a8..23a0acd 100644 --- a/src/app/my-purchases/page.tsx +++ b/src/app/(app)/my-purchases/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { getMyPurchases, submitOrder, getUserSubmission } from '@/app/actions' +import { getMyPurchases, submitOrder, getUserSubmission, getSessionUser } from '@/app/actions' import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' import { Button, buttonVariants } from '@/components/ui/button' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' @@ -38,11 +38,14 @@ export default function MyPurchasesPage() { const ITEMS_PER_PAGE = 5 useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) { - setUserId(id) - loadPurchases(id) + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + loadPurchases(user.id) } + } + init() }, []) const loadPurchases = async (id: string) => { diff --git a/src/app/page.tsx b/src/app/(app)/page.tsx similarity index 98% rename from src/app/page.tsx rename to src/app/(app)/page.tsx index 42815b1..debe785 100644 --- a/src/app/page.tsx +++ b/src/app/(app)/page.tsx @@ -1,4 +1,4 @@ -import { getAvailableOrders } from './actions' +import { getAvailableOrders } from '@/app/actions' import { OrderCard } from '@/components/OrderCard' import { Sparkles, Plus, Store } from 'lucide-react' import Link from 'next/link' diff --git a/src/app/(app)/profile/page.tsx b/src/app/(app)/profile/page.tsx new file mode 100644 index 0000000..5ac8228 --- /dev/null +++ b/src/app/(app)/profile/page.tsx @@ -0,0 +1,280 @@ +'use client' + +import { useEffect, useState } from 'react' +import { getSessionUser, updateProfile, changePassword } from '@/app/actions' +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Loader2, Save, CheckCircle, ShieldCheck, UserCircle, KeyRound, LockKeyhole } from 'lucide-react' + +export default function ProfilePage() { + const [user, setUser] = useState(null) + const [name, setName] = useState('') + const [photo, setPhoto] = useState('') + const [loading, setLoading] = useState(true) + + // Profile state + const [savingProfile, setSavingProfile] = useState(false) + const [profileError, setProfileError] = useState('') + const [profileSuccess, setProfileSuccess] = useState(false) + + // Password state + const [oldPassword, setOldPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [savingPassword, setSavingPassword] = useState(false) + const [passwordError, setPasswordError] = useState('') + const [passwordSuccess, setPasswordSuccess] = useState(false) + const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) + + useEffect(() => { + loadUser() + }, []) + + const loadUser = async () => { + setLoading(true) + const sessionUser = await getSessionUser() + if (sessionUser) { + setUser(sessionUser) + setName(sessionUser.name) + setPhoto(sessionUser.photo || '') + } + setLoading(false) + } + + const handleSaveProfile = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) { + setProfileError('Nama wajib diisi') + return + } + + setProfileError('') + setProfileSuccess(false) + setSavingProfile(true) + + const res = await updateProfile(user.id, name.trim(), photo.trim() || null) + + if (res.success) { + setUser(res.user) + setProfileSuccess(true) + setTimeout(() => setProfileSuccess(false), 3000) + } else { + setProfileError(res.error || 'Terjadi kesalahan') + } + setSavingProfile(false) + } + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault() + setPasswordError('') + setPasswordSuccess(false) + + if (!oldPassword || !newPassword || !confirmPassword) { + setPasswordError('Semua field wajib diisi') + return + } + if (newPassword !== confirmPassword) { + setPasswordError('Konfirmasi password tidak cocok') + return + } + if (newPassword.length < 6) { + setPasswordError('Password baru minimal 6 karakter') + return + } + + setSavingPassword(true) + const res = await changePassword(user.id, oldPassword, newPassword) + if (res.success) { + setPasswordSuccess(true) + setOldPassword('') + setNewPassword('') + setConfirmPassword('') + setTimeout(() => { + setPasswordSuccess(false) + setIsPasswordModalOpen(false) + }, 2000) + } else { + setPasswordError(res.error || 'Gagal mengubah password') + } + setSavingPassword(false) + } + + if (loading) { + return ( +
+ + Memuat profil... +
+ ) + } + + if (!user) { + return ( +
+ Anda belum login. +
+ ) + } + + return ( +
+
+ + {/* Left 2 Cols: Forms */} +
+ + {/* PROFILE FORM */} + +
+
+

Informasi Akun

+

Ubah nama tampilan dan foto profil Anda.

+
+ + {user.role} + +
+ +
+ +
+ +
+ {photo ? ( + {name} + ) : ( +
+ {name ? name.charAt(0).toUpperCase() : } +
+ )} +
+

Pratinjau Avatar

+

Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil.

+
+
+
+ +
+ + setName(e.target.value)} placeholder="Contoh: Budi Santoso" className="h-11 rounded-xl" /> +
+ +
+ + setPhoto(e.target.value)} placeholder="https://..." className="h-11 rounded-xl" /> +
+ + {profileError && ( +
{profileError}
+ )} + {profileSuccess && ( +
+ Profil berhasil diperbarui! +
+ )} +
+ +
+ +
+
+
+ + +
+ + {/* Right 1 Col: Account Details */} +
+ +
+ + Detail Akun +
+ +
+
+ Username +
{user.username}
+
+
+ Role Akses +
{user.role}
+
+
+ + {/* PASSWORD CHANGE MODAL BUTTON */} +
+ + + + Ganti Password + + + + Ganti Password +

Lindungi akun Anda dengan mengubah password secara berkala.

+
+ +
+
+
+
+ +
+ + setOldPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Masukkan password lama" /> +
+
+
+ +
+ + setNewPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Minimal 6 karakter" /> +
+
+
+ +
+ + setConfirmPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Ketik ulang password baru" /> +
+
+
+ + {passwordError && ( +
{passwordError}
+ )} + {passwordSuccess && ( +
+ Password berhasil diubah! +
+ )} +
+ +
+ + +
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/app/reports/page.tsx b/src/app/(app)/reports/page.tsx similarity index 97% rename from src/app/reports/page.tsx rename to src/app/(app)/reports/page.tsx index 0545ae0..65fa46a 100644 --- a/src/app/reports/page.tsx +++ b/src/app/(app)/reports/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { getCreatorReport, getSubmittorReport } from '@/app/actions' +import { getCreatorReport, getSubmittorReport, getSessionUser } from '@/app/actions' import { Card } from '@/components/ui/card' import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react' import { cn } from '@/lib/utils' @@ -51,7 +51,7 @@ function getIntervalFromFilter(type: 'WEEK' | 'MONTH', value: string): { start: export default function ReportsPage() { const [activeTab, setActiveTab] = useState<'SUBMITTOR' | 'CREATOR'>('SUBMITTOR') - const [creatorTab, setCreatorTab] = useState<'BY_ORDER' | 'BY_PERSON'>('BY_ORDER') + const [creatorTab, setCreatorTab] = useState<'BY_ORDER' | 'BY_PERSON'>('BY_PERSON') const [filterType, setFilterType] = useState<'WEEK' | 'MONTH'>('WEEK') const [filterValue, setFilterValue] = useState(getWeekValue(new Date())) @@ -62,10 +62,12 @@ export default function ReportsPage() { const [creatorData, setCreatorData] = useState([]) const [loading, setLoading] = useState(true) - // Initial load: get userId useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) setUserId(id) + const init = async () => { + const user = await getSessionUser() + if (user?.id) setUserId(user.id) + } + init() }, []) // Re-fetch every time userId, filterType, or filterValue changes @@ -261,7 +263,7 @@ export default function ReportsPage() { {/* Sub-Tabs */}
- {(['BY_ORDER', 'BY_PERSON'] as const).map(tab => ( + {([ 'BY_PERSON','BY_ORDER',] as const).map(tab => ( +
+ + +
+ + + + + + + + + + + + {loading ? ( + + + + ) : users.length === 0 ? ( + + + + ) : ( + users.map(u => ( + + + + + + + + )) + )} + +
PenggunaRoleStatusTerdaftarAksi
+ +
Belum ada pengguna lain.
+
+ {u.photo ? ( + {u.name} + ) : ( +
+ {u.name.charAt(0)} +
+ )} +
+
{u.name}
+
@{u.username}
+
+
+
+ {u.role === 'superadmin' ? ( + + Superadmin + + ) : ( + + User + + )} + + {u.is_active ? ( + + Aktif + + ) : ( + + Nonaktif + + )} + + {new Date(u.created_at).toLocaleDateString('id-ID')} + +
+ + + {u.role !== 'superadmin' && ( + <> + + + + )} +
+
+
+ + {totalPages > 1 && ( +
+ Halaman {page} dari {totalPages} +
+ + +
+
+ )} +
+ + {/* CREATE MODAL */} + + +
+ Tambah User Baru +
+
+
+ + setName(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan nama lengkap" /> +
+
+ + setUsername(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan username unik" /> +
+
+ + setPassword(e.target.value)} className="h-11 rounded-xl" placeholder="Buat kata sandi (min. 6 karakter)" /> +
+
+ +
+ + +
+
+
+ + +
+
+
+
+ + {/* RESET PASSWORD MODAL */} + + +
+ Ubah Password +

Ubah password untuk user @{selectedUser?.username}

+
+
+
+ + setNewPass(e.target.value)} className="h-11 rounded-xl" placeholder="Minimal 6 karakter" /> +
+
+ + +
+
+
+
+ + ) +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..821e1d8 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,7 @@ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..1d0e540 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,153 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { loginUser } from '@/app/actions' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { LogIn, KeyRound, User, Loader2, Eye, EyeOff } from 'lucide-react' + +export default function LoginPage() { + const router = useRouter() + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [showPassword, setShowPassword] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setLoading(true) + setError('') + + const formData = new FormData(e.currentTarget) + const username = (formData.get('username') as string).trim() + const password = formData.get('password') as string + + if (!username || !password) { + setError('Username dan Password wajib diisi') + setLoading(false) + return + } + + const res = await loginUser(username, password) + if (res.success) { + window.location.href = '/' // Force hard reload to update context/middleware + } else { + setError(res.error || 'Gagal login') + setLoading(false) + } + } + + return ( +
+ {/* Elegant Ambient Background */} +
+
+
+
+ +
+ + {/* Left Side: Branding (Hidden on Mobile) */} +
+
+
+
+
+ +
+
+ +
+

+ Selamat Datang
di TitipIn +

+

+ Platform modern dan terpercaya untuk mengelola pesanan jasa titip Anda dengan rapi, efisien, dan transparan. +

+
+ +
+
+
A
+
B
+
C
+
+

Bergabung dengan ribuan
pengguna lainnya.

+
+
+ + {/* Right Side: Form */} +
+
+
+ +
+

TitipIn

+
+ +
+

Masuk ke Akun

+

Silakan masukkan kredensial Anda untuk melanjutkan

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ + +
+ +

+ Belum punya akun?{' '} + + Daftar di sini + +

+
+
+
+ ) +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx new file mode 100644 index 0000000..9d2e3a4 --- /dev/null +++ b/src/app/(auth)/register/page.tsx @@ -0,0 +1,201 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { registerUser } from '@/app/actions' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { UserPlus, KeyRound, User, Loader2, BadgeCheck, Eye, EyeOff } from 'lucide-react' + +export default function RegisterPage() { + const router = useRouter() + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [showConfirm, setShowConfirm] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setLoading(true) + setError('') + + const formData = new FormData(e.currentTarget) + const name = formData.get('name') as string + const username = (formData.get('username') as string).trim() + const password = formData.get('password') as string + const confirm = formData.get('confirm') as string + + if (!name || !username || !password || !confirm) { + setError('Semua field wajib diisi') + setLoading(false) + return + } + + if (password !== confirm) { + setError('Password dan Konfirmasi Password tidak cocok') + setLoading(false) + return + } + + if (password.length < 6) { + setError('Password minimal 6 karakter') + setLoading(false) + return + } + + const res = await registerUser(name, username, password) + if (res.success) { + window.location.href = '/' // Force hard reload to update context/middleware + } else { + setError(res.error || 'Gagal mendaftar') + setLoading(false) + } + } + + return ( +
+ {/* Elegant Ambient Background */} +
+
+
+
+ +
+ + {/* Left Side: Branding (Hidden on Mobile) */} +
+
+
+
+
+ +
+
+ +
+

+ Mulai Perjalanan
Anda di TitipIn +

+

+ Buat akun secara gratis dan nikmati kemudahan mengelola PO jasa titip tanpa ribet. +

+
+ +
+
+
+
+
+
+

Aman, Cepat, dan
Mudah digunakan.

+
+
+ + {/* Right Side: Form */} +
+
+
+ +
+

Daftar TitipIn

+
+ +
+

Buat Akun Baru

+

Lengkapi form di bawah untuk membuat akun baru

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + + +
+
+ + +
+ +

+ Sudah punya akun?{' '} + + Masuk di sini + +

+
+
+
+ ) +} diff --git a/src/app/actions.ts b/src/app/actions.ts index b840374..c24ab58 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -3,38 +3,204 @@ import prisma from '@/lib/prisma' import { revalidatePath } from 'next/cache' -// === USER ACTIONS === -export async function checkUser(id: string) { - return await prisma.user.findUnique({ where: { id } }) +import { hashPassword, verifyPassword, encrypt, decrypt } from '@/lib/auth' +import { cookies } from 'next/headers' + +// === AUTHENTICATION ACTIONS === +export async function loginUser(username: string, password: string) { + try { + const user = await prisma.user.findUnique({ where: { username } }) + if (!user) return { success: false, error: 'Username tidak ditemukan.' } + if (!user.is_active) return { success: false, error: 'Akun Anda dinonaktifkan.' } + + const isMatch = await verifyPassword(password, user.password) + if (!isMatch) return { success: false, error: 'Password salah.' } + + const sessionData = { + id: user.id, + username: user.username, + name: user.name, + role: user.role, + photo: user.photo + } + + const sessionToken = await encrypt(sessionData); + + (await cookies()).set('session', sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/' + }) + + return { success: true, user: sessionData } + } catch (e) { + console.error("Login Error:", e) + return { success: false, error: 'Terjadi kesalahan saat login.' } + } } -export async function registerUser(id: string, name: string, photo?: string) { +export async function registerUser(name: string, username: string, password: string) { try { + const hashedPassword = await hashPassword(password) const user = await prisma.user.create({ - data: { id, name, photo } + data: { name, username, password: hashedPassword } }) - return { success: true, user } + + const sessionData = { + id: user.id, + username: user.username, + name: user.name, + role: user.role, + photo: user.photo + } + + const sessionToken = await encrypt(sessionData); + + (await cookies()).set('session', sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/' + }) + + return { success: true, user: sessionData } } catch (error: any) { - if (error.code === 'P2002') return { success: false, error: 'Nama sudah digunakan.' } + console.error("Register Error:", error) + if (error.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' } return { success: false, error: 'Terjadi kesalahan.' } } } -export async function updateProfile(id: string, name: string, photo?: string) { +export async function logoutUser() { + (await cookies()).delete('session') + return { success: true } +} + +export async function getSessionUser() { + const cookie = (await cookies()).get('session')?.value + if (!cookie) return null + return await decrypt(cookie) +} + +export async function updateProfile(id: string, name: string, photo?: string | null) { try { const user = await prisma.user.update({ where: { id }, - data: { name, photo } + data: { name, photo: photo === undefined ? undefined : (photo || null) } }) + + // Update session cookie with new data + const sessionData = { + id: user.id, + username: user.username, + name: user.name, + role: user.role, + photo: user.photo + } + const sessionToken = await encrypt(sessionData); + (await cookies()).set('session', sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/' + }) + revalidatePath('/') revalidatePath('/profile') - return { success: true, user } + return { success: true, user: sessionData } } catch (error: any) { - if (error.code === 'P2002') return { success: false, error: 'Nama sudah digunakan.' } return { success: false, error: 'Gagal memperbarui profil.' } } } +export async function changePassword(id: string, oldPass: string, newPass: string) { + try { + const user = await prisma.user.findUnique({ where: { id } }) + if (!user) return { success: false, error: 'User tidak ditemukan.' } + + const isMatch = await verifyPassword(oldPass, user.password) + if (!isMatch) return { success: false, error: 'Password lama salah.' } + + const hashed = await hashPassword(newPass) + await prisma.user.update({ + where: { id }, + data: { password: hashed } + }) + + return { success: true } + } catch (e) { + return { success: false, error: 'Gagal mengganti password.' } + } +} + +// === USER MANAGEMENT (SUPERADMIN) === +export async function getUsers(page = 1, limit = 10) { + const skip = (page - 1) * limit + const [users, total] = await Promise.all([ + prisma.user.findMany({ skip, take: limit, orderBy: { created_at: 'desc' } }), + prisma.user.count() + ]) + return { users, total, totalPages: Math.ceil(total / limit) } +} + +export async function createUserByAdmin(name: string, username: string, password: string, role: string) { + try { + const hashedPassword = await hashPassword(password) + await prisma.user.create({ + data: { name, username, password: hashedPassword, role } + }) + revalidatePath('/users') + return { success: true } + } catch (e: any) { + if (e.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' } + return { success: false, error: 'Gagal membuat user.' } + } +} + +export async function toggleUserActive(id: string, is_active: boolean) { + try { + const user = await prisma.user.findUnique({ where: { id } }) + if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dinonaktifkan.' } + + await prisma.user.update({ + where: { id }, + data: { is_active } + }) + revalidatePath('/users') + return { success: true } + } catch (e) { + return { success: false, error: 'Gagal mengupdate status user.' } + } +} + +export async function deleteUser(id: string) { + try { + const user = await prisma.user.findUnique({ where: { id } }) + if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dihapus.' } + + await prisma.user.delete({ where: { id } }) + revalidatePath('/users') + return { success: true } + } catch (e) { + return { success: false, error: 'Gagal menghapus user.' } + } +} + +export async function resetUserPassword(id: string, newPass: string) { + try { + const hashed = await hashPassword(newPass) + await prisma.user.update({ + where: { id }, + data: { password: hashed } + }) + revalidatePath('/users') + return { success: true } + } catch (e) { + return { success: false, error: 'Gagal mereset password.' } + } +} + // === ORDER ACTIONS (DASHBOARD & CREATE) === export async function getAvailableOrders() { const startOfDay = new Date() @@ -91,6 +257,7 @@ export async function createOrder(data: { revalidatePath('/my-orders') return { success: true, order } } catch (error) { + console.error("Create Order Error:", error) return { success: false, error: 'Gagal membuat order.' } } } @@ -225,7 +392,7 @@ export async function updateSubmissionPayment(submission_id: string, bill: numbe revalidatePath(`/reports`) return { success: true } } catch (e) { - return { success: false, error: 'Gagal menyimpan tagihan.' } + console.error(e); return { success: false, error: 'Gagal menyimpan tagihan.' } } } diff --git a/src/app/globals.css b/src/app/globals.css index c908b21..4423cda 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -115,4 +115,7 @@ html { font-family: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif; } -} \ No newline at end of file +} +button { + cursor: pointer; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9d20f71..6b6b4c6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import { Roboto, Open_Sans } from "next/font/google"; import "./globals.css"; -import { AuthProvider } from "@/components/AuthProvider"; -import { AppLayout } from "@/components/AppLayout"; + const roboto = Roboto({ weight: ["300", "400", "500", "700", "900"], @@ -37,11 +36,7 @@ export default function RootLayout({ suppressHydrationWarning className="min-h-full flex flex-col bg-[#F4F6FB] dark:bg-[#0B0F19] font-sans antialiased" > - - - {children} - - + {children} ); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx deleted file mode 100644 index b8109b5..0000000 --- a/src/app/profile/page.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { checkUser, updateProfile } from '@/app/actions' -import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Loader2, UserCircle, Save, CheckCircle, Upload, ShieldCheck, Sparkles, Image as ImageIcon } from 'lucide-react' - -export default function ProfilePage() { - const [userId, setUserId] = useState(null) - const [name, setName] = useState('') - const [photo, setPhoto] = useState('') - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - const [error, setError] = useState('') - const [success, setSuccess] = useState(false) - - useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) { - setUserId(id) - loadProfile(id) - } - }, []) - - const loadProfile = async (id: string) => { - setLoading(true) - const user = await checkUser(id) - if (user) { - setName(user.name) - setPhoto(user.photo || '') - } - setLoading(false) - } - - const handleSave = async (e: React.FormEvent) => { - e.preventDefault() - if (!name.trim()) { - setError('Nama wajib diisi') - return - } - - setError('') - setSuccess(false) - setSaving(true) - - const res = await updateProfile(userId!, name.trim(), photo.trim() || undefined) - - if (res.success) { - setSuccess(true) - setTimeout(() => setSuccess(false), 3000) - } else { - setError(res.error || 'Terjadi kesalahan') - } - setSaving(false) - } - - if (loading) { - return ( -
- - Memuat profil... -
- ) - } - - return ( -
-
- {/* Left 2 Cols: Main Profile Form */} -
- -
-
-

- Informasi Akun -

-

- Ubah nama tampilan dan foto avatar yang terlihat oleh teman-teman. -

-
- - Stateless Auth - -
- -
- - {/* Photo Preview & Dashed Container */} -
- -
- {photo ? ( - {name} - ) : ( -
- {name ? name.charAt(0).toUpperCase() : } -
- )} -
-

- Pratinjau Avatar -

-

- Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil. -

-
-
-
- - {/* Form Inputs */} -
- - setName(e.target.value)} - placeholder="Contoh: Budi Santoso" - className="h-11 rounded-xl border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1] font-semibold" - /> -

Nama ini akan tercantum di setiap PO yang Anda buat atau ikuti.

-
- -
- - setPhoto(e.target.value)} - placeholder="https://images.unsplash.com/photo-..." - className="h-11 rounded-xl border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1] text-xs font-mono" - /> -
- - {error && ( -
- {error} -
- )} - - {success && ( -
- - Profil Anda berhasil diperbarui! -
- )} -
- - {/* Card Footer with Dedicated Container & Padding */} -
- -
-
-
-
- - {/* Right 1 Col: Device & ID Details */} -
- -
- - Identitas Stateless -
- -

- Akun Anda tersimpan secara lokal pada peramban ini menggunakan Device ID yang unik di bawah ini. -

- -
-
- Unique User UUID - -
-
- {userId} -
-
- -
-
- - Tips Penggunaan -
-

- Jangan bersihkan data peramban (localStorage) jika Anda ingin tetap login dengan ID yang sama. -

-
-
-
-
-
- ) -} diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx deleted file mode 100644 index 08f097e..0000000 --- a/src/components/AuthProvider.tsx +++ /dev/null @@ -1,157 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { v4 as uuidv4 } from 'uuid' -import { checkUser, registerUser } from '@/app/actions' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Button } from '@/components/ui/button' -import { Loader2, Sparkles, User, Image as ImageIcon } from 'lucide-react' - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [isVerified, setIsVerified] = useState(false) - const [showModal, setShowModal] = useState(false) - const [userId, setUserId] = useState(null) - const [name, setName] = useState('') - const [photo, setPhoto] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - - useEffect(() => { - async function initAuth() { - let id = localStorage.getItem('user_id') - if (!id) { - id = uuidv4() - localStorage.setItem('user_id', id) - } - setUserId(id) - - const user = await checkUser(id) - if (user) { - setIsVerified(true) - } else { - setShowModal(true) - } - setLoading(false) - } - initAuth() - }, []) - - const handleRegister = async (e: React.FormEvent) => { - e.preventDefault() - if (!name.trim()) { - setError('Nama wajib diisi') - return - } - - setError('') - setSaving(true) - const res = await registerUser(userId!, name.trim(), photo.trim() || undefined) - - if (res.success) { - setShowModal(false) - setIsVerified(true) - } else { - setError(res.error || 'Terjadi kesalahan') - } - setSaving(false) - } - - const handleOpenChange = (open: boolean) => { - if (!open && !isVerified) { - setShowModal(true) - } - } - - if (loading) { - return ( -
-
- -
-
- - Memuat sesi Anda... -
-
- ) - } - - return ( - <> - {isVerified && children} - - -
-
-
- -
- - Selamat Datang di TitipIn - - - Daftarkan nama Anda untuk mulai membuka atau menitip pesanan bersama teman. - -
- -
-
- -
- setName(e.target.value)} - placeholder="Contoh: Budi Santoso" - className="rounded-xl h-11 border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]" - autoComplete="off" - /> -
-

Nama harus unik agar pembuat PO mudah mengenali Anda.

-
- -
- - setPhoto(e.target.value)} - placeholder="https://images.unsplash.com/..." - className="rounded-xl h-11 border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]" - autoComplete="off" - /> -
- - {error && ( -
- {error} -
- )} - - -
- -
- - ) -} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9fc05be..10e1d75 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -54,6 +54,13 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) { badge: 'Laporan', Icon: BarChart2 } + case '/users': + return { + title: 'Manajemen Pengguna', + subtitle: 'Kelola data pengguna, role, dan status aktif.', + badge: 'Superadmin', + Icon: User + } default: if (pathname.startsWith('/my-orders/')) { return { diff --git a/src/components/OrderCard.tsx b/src/components/OrderCard.tsx index 6c2321d..e0d9adc 100644 --- a/src/components/OrderCard.tsx +++ b/src/components/OrderCard.tsx @@ -7,7 +7,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' -import { submitOrder, getUserSubmission } from '@/app/actions' +import { submitOrder, getUserSubmission, getSessionUser } from '@/app/actions' import { PlusCircle, MinusCircle, User, CheckCircle2, ShoppingBag, Sparkles, Users } from 'lucide-react' import { format } from 'date-fns' @@ -113,11 +113,14 @@ function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => voi const [error, setError] = useState('') useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) { - setUserId(id) - loadExistingSubmission(id) + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + loadExistingSubmission(user.id) } + } + init() }, [order.id]) const loadExistingSubmission = async (uid: string) => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 62ca6b6..1e12e7a 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -4,6 +4,12 @@ import { useEffect, useState } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' import { cn } from '@/lib/utils' +import { + Dialog, + DialogContent, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' import { Store, ClipboardList, @@ -15,7 +21,6 @@ import { Info, BarChart2 } from 'lucide-react' -import { checkUser } from '@/app/actions' const menuItems = [ { name: 'Open Order', href: '/', icon: Store, desc: 'Daftar PO live hari ini' }, @@ -27,22 +32,38 @@ const menuItems = [ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () => void }) { const pathname = usePathname() const [userName, setUserName] = useState('Pengguna') + const [userNameAccount, setUserNameAccount] = useState('Pengguna') const [userPhoto, setUserPhoto] = useState(null) + const [userRole, setUserRole] = useState(null) const [userId, setUserId] = useState(null) + const [isLogoutOpen, setIsLogoutOpen] = useState(false) useEffect(() => { - const id = localStorage.getItem('user_id') - if (id) { - setUserId(id) - checkUser(id).then((u) => { + import('@/app/actions').then(({ getSessionUser }) => { + getSessionUser().then((u) => { if (u) { + setUserId(u.id) setUserName(u.name) + setUserNameAccount(u.username) + setUserRole(u.role) if (u.photo) setUserPhoto(u.photo) } }) - } + }) }, [pathname]) + const handleLogout = async () => { + const { logoutUser } = await import('@/app/actions') + await logoutUser() + window.location.href = '/login' + } + + // Dynamic Menu Items based on role + const finalMenuItems = [...menuItems] + if (userRole === 'superadmin') { + finalMenuItems.push({ name: 'User Management', href: '/users', icon: UserCircle, desc: 'Kelola pengguna' }) + } + return ( <> {/* Mobile Backdrop */} @@ -106,9 +127,16 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
- - {userId ? `ID: ${userId.slice(0, 10)}...` : 'Aktif'} - +
+ {userRole === 'superadmin' && ( + + Admin + + )} + + {userNameAccount ? `@${userNameAccount}` : 'Aktif'} + +
@@ -118,7 +146,7 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () => Navigasi Utama
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b033601..4b6d5a9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "cursor-pointer group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..7c9bde3 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,39 @@ +import { SignJWT, jwtVerify } from 'jose' +import bcrypt from 'bcryptjs' +import { cookies } from 'next/headers' + +const secretKey = process.env.JWT_SECRET +const key = new TextEncoder().encode(secretKey) + +export async function hashPassword(password: string) { + return await bcrypt.hash(password, 10) +} + +export async function verifyPassword(password: string, hash: string) { + return await bcrypt.compare(password, hash) +} + +export async function encrypt(payload: any) { + return await new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .sign(key) // No expiration as requested +} + +export async function decrypt(input: string): Promise { + try { + const { payload } = await jwtVerify(input, key, { + algorithms: ['HS256'], + }) + return payload + } catch (error) { + return null + } +} + +export async function getSession() { + const cookieStore = await cookies() + const session = cookieStore.get('session')?.value + if (!session) return null + return await decrypt(session) +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..803752c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { decrypt } from '@/lib/auth' + +const protectedRoutes = ['/', '/my-orders', '/my-purchases', '/reports', '/profile', '/users'] +const publicRoutes = ['/login', '/register'] + +export async function middleware(request: NextRequest) { + const path = request.nextUrl.pathname + + // Also protect dynamic routes like /my-orders/[id] + const isProtectedRoute = protectedRoutes.includes(path) || path.startsWith('/my-orders/') + const isPublicRoute = publicRoutes.includes(path) + + const cookie = request.cookies.get('session')?.value + const session = cookie ? await decrypt(cookie) : null + + if (isProtectedRoute && !session) { + return NextResponse.redirect(new URL('/login', request.nextUrl)) + } + + if (isPublicRoute && session) { + return NextResponse.redirect(new URL('/', request.nextUrl)) + } + + if (path.startsWith('/users') && session?.role !== 'superadmin') { + return NextResponse.redirect(new URL('/', request.nextUrl)) + } + + return NextResponse.next() +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|icon.tsx|icon).*)'], +} diff --git a/yarn.lock b/yarn.lock index 34ed183..8368acd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -273,6 +273,13 @@ reselect "^5.2.0" use-sync-external-store "^1.6.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@date-fns/tz@^1.4.1": version "1.5.0" resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.5.0.tgz#e9e79b7583f0b1322c53db884a0112551095e3f3" @@ -349,6 +356,136 @@ dependencies: tslib "^2.4.0" +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + "@eslint-community/eslint-utils@4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" @@ -671,16 +808,24 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.6.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" @@ -1001,6 +1146,26 @@ minimatch "^10.0.1" path-browserify "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.13.tgz#56105a9a8c786e8f15e35746879cf2d52275485b" + integrity sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3": version "0.10.3" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" @@ -1008,6 +1173,13 @@ dependencies: tslib "^2.4.0" +"@types/bcryptjs@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-3.0.0.tgz#d7be11653aa82cf17ffee4f3925f1f80cfc1add2" + integrity sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg== + dependencies: + bcryptjs "*" + "@types/d3-array@^3.0.3": version "3.2.2" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c" @@ -1204,6 +1376,106 @@ "@typescript-eslint/types" "8.68.0" eslint-visitor-keys "^5.0.0" +"@typescript/typescript-aix-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6" + integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ== + +"@typescript/typescript-darwin-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235" + integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA== + +"@typescript/typescript-darwin-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4" + integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA== + +"@typescript/typescript-freebsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa" + integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ== + +"@typescript/typescript-freebsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd" + integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw== + +"@typescript/typescript-linux-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f" + integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ== + +"@typescript/typescript-linux-arm@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e" + integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ== + +"@typescript/typescript-linux-loong64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad" + integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ== + +"@typescript/typescript-linux-mips64el@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0" + integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA== + +"@typescript/typescript-linux-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d" + integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA== + +"@typescript/typescript-linux-riscv64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1" + integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ== + +"@typescript/typescript-linux-s390x@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e" + integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw== + +"@typescript/typescript-linux-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2" + integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A== + +"@typescript/typescript-netbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901" + integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA== + +"@typescript/typescript-netbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab" + integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA== + +"@typescript/typescript-openbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf" + integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ== + +"@typescript/typescript-openbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f" + integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg== + +"@typescript/typescript-sunos-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88" + integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g== + +"@typescript/typescript-win32-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7" + integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ== + +"@typescript/typescript-win32-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c" + integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g== + "@unrs/resolver-binding-android-arm-eabi@1.12.2": version "1.12.2" resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz#98a9fee62c01f209747a4ab5855f1ced38a6d03a" @@ -1331,7 +1603,14 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.15.0: +acorn-walk@^8.1.1: + version "8.3.5" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" + integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1: version "8.18.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940" integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== @@ -1392,6 +1671,11 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1547,6 +1831,11 @@ baseline-browser-mapping@^2.11.12, baseline-browser-mapping@^2.9.19: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303" integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw== +bcryptjs@*, bcryptjs@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.3.tgz#4b93d6a398c48bfc9f32ee65d301174a8a8ea56f" + integrity sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g== + body-parser@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437" @@ -1781,6 +2070,11 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -2000,6 +2294,11 @@ detect-libc@^2.0.3, detect-libc@^2.1.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== +diff@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + diff@^8.0.2: version "8.0.4" resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.4.tgz#4f5baf3188b9b2431117b962eb20ba330fadf696" @@ -2229,6 +2528,38 @@ es-toolkit@^1.39.3: resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.51.0.tgz#f1f0fe1944e6405ef5e4d19a92b91c1b65e8b657" integrity sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw== +esbuild@~0.28.0: + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" + escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -2708,7 +3039,7 @@ fs-extra@^11.3.1: jsonfile "^6.0.1" universalify "^2.0.0" -fsevents@2.3.3: +fsevents@2.3.3, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3323,7 +3654,7 @@ jiti@^2.7.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== -jose@^6.1.3: +jose@^6.1.3, jose@^6.2.10: version "6.2.10" resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f" integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g== @@ -3581,6 +3912,11 @@ magic-string@^0.30.21: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -4809,6 +5145,25 @@ ts-morph@^26.0.0: "@ts-morph/common" "~0.27.0" code-block-writer "^13.0.3" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -4833,6 +5188,15 @@ tslib@^2.0.1, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tsx@^4.23.12: + version "4.23.12" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.12.tgz#3a4919591cd9b9e00011b75e596c8ab8db23c09c" + integrity sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q== + dependencies: + esbuild "~0.28.0" + optionalDependencies: + fsevents "~2.3.3" + tw-animate-css@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz#b4a06f68244cba39428aa47e65e6e4c0babc21ee" @@ -4909,10 +5273,31 @@ typescript-eslint@^8.46.0: "@typescript-eslint/typescript-estree" "8.68.0" "@typescript-eslint/utils" "8.68.0" -typescript@^5: - version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== +typescript@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582" + integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA== + optionalDependencies: + "@typescript/typescript-aix-ppc64" "7.0.2" + "@typescript/typescript-darwin-arm64" "7.0.2" + "@typescript/typescript-darwin-x64" "7.0.2" + "@typescript/typescript-freebsd-arm64" "7.0.2" + "@typescript/typescript-freebsd-x64" "7.0.2" + "@typescript/typescript-linux-arm" "7.0.2" + "@typescript/typescript-linux-arm64" "7.0.2" + "@typescript/typescript-linux-loong64" "7.0.2" + "@typescript/typescript-linux-mips64el" "7.0.2" + "@typescript/typescript-linux-ppc64" "7.0.2" + "@typescript/typescript-linux-riscv64" "7.0.2" + "@typescript/typescript-linux-s390x" "7.0.2" + "@typescript/typescript-linux-x64" "7.0.2" + "@typescript/typescript-netbsd-arm64" "7.0.2" + "@typescript/typescript-netbsd-x64" "7.0.2" + "@typescript/typescript-openbsd-arm64" "7.0.2" + "@typescript/typescript-openbsd-x64" "7.0.2" + "@typescript/typescript-sunos-x64" "7.0.2" + "@typescript/typescript-win32-arm64" "7.0.2" + "@typescript/typescript-win32-x64" "7.0.2" unbox-primitive@^1.1.0: version "1.1.0" @@ -5009,6 +5394,11 @@ uuid@^14.0.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.2.tgz#d5ae03e4db0881c87271f8b0b9bd7312d02c799a" integrity sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + validate-npm-package-name@^7.0.1: version "7.0.2" resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz#e57c3d721a4c8bbff454a246e7f7da811559ea0d" @@ -5129,6 +5519,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"