diff --git a/bun.lockb b/bun.lockb index 68d46c85..a862a5a4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2cfb90fb..065dc1c1 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "add": "^2.0.6", "adm-zip": "^0.5.16", "animate.css": "^4.1.1", + "async-mutex": "^0.5.0", "bcryptjs": "^3.0.2", "bun": "^1.2.2", "chart.js": "^4.4.8", diff --git a/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts b/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts index 33774f75..5ee80718 100644 --- a/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts +++ b/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts @@ -60,3 +60,12 @@ export async function seedDataPerpustakaan() { } console.log("✅ Data perpustakaan seeded successfully"); } +if (import.meta.main) { + seedDataPerpustakaan() + .then(() => { + console.log("seed data perpustakaan success"); + }) + .catch((err) => { + console.log("gagal seed data perpustakaan", JSON.stringify(err)); + }); +} \ No newline at end of file diff --git a/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json b/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json index b49b5298..abfe70cd 100644 --- a/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json +++ b/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json @@ -138,5 +138,61 @@ "deskripsi": "

Cerita edukatif yang mengenalkan sains kepada anak dengan bahasa sederhana

", "kategoriId": "cmkqb11mc000104jibqh7bdzu", "imageName": "G0iELZb2DhQDCCP5OdzJR-desktop.webp" + }, + { + "id": "cml7fq776000104jscnj58sgm", + "judul": "Pedagogy of the Oppressed", + "deskripsi": "

Klasik pemikiran pendidikan kritis; menggali hubungan guru-murid dan peran pendidikan dalam pembebasan sosial

", + "kategoriId": "cmkqb11mc000104jibq97bdzu", + "imageName": "pendidikan-1.webp" + }, + { + "id": "cml7fqurm000204js5p60hkym", + "judul": "The Courage to Teach", + "deskripsi": "

Tentang refleksi diri seorang pendidik; cocok untuk pengajar yang ingin lebih dari sekedar “metode mengajar”

", + "kategoriId": "cmkqb11mc000104jibq97bdzu", + "imageName": "pendidikan-2.webp" + }, + { + "id": "cml7fqurm000204js5p60hkzn", + "judul": "A Brief History of Time", + "deskripsi": "

Penjelasan kosmologi yang terkenal dunia; sains kompleks dibahas dengan bahasa yang bisa dinikmati pembaca umum

", + "kategoriId": "cmkqb11mc000104jibqa7bdzu", + "imageName": "ilmiah-1.webp" + }, + { + "id": "cml7fqurm000204js5p60hkao", + "judul": "The Selfish Gene", + "deskripsi": "

Membawa perspektif baru tentang evolusi melalui “gen” sebagai unit seleksi

", + "kategoriId": "cmkqb11mc000104jibqa7bdzu", + "imageName": "ilmiah-2.webp" + }, + { + "id": "cml7fx09c000304jshams3xbg", + "judul": "A Little Life", + "deskripsi": "

Novel yang menggambarkan hidup seorang remaja yang mengalami kehidupan yang sangat sulit

", + "kategoriId": "cmkqb11mc000104jibqb7bdzu", + "imageName": "drama-1.webp" + }, + { + "id": "cml7fx09c000304jshams3xch", + "judul": "Death of a Salesman", + "deskripsi": "

Drama teater klasik Amerika tentang harapan, keluarga, dan realitas hidup.

", + "kategoriId": "cmkqb11mc000104jibqb7bdzu", + "imageName": "drama-2.webp" + }, + { + "id": "cml7fx09c000304jshams3xdi", + "judul": "How Not to Die", + "deskripsi": "

Panduan berbasis penelitian tentang pola makan untuk mencegah dan menangani penyakit.

", + "kategoriId": "cmkqb11mc000104jibqg7bdzu", + "imageName": "kesehatan-1.webp" + }, + { + "id": "cml7fx09c000304jshams3xej", + "judul": "The Body Keeps the Score", + "deskripsi": "

Fokus pada trauma, otak & tubuh; penting untuk memahami kesehatan mental secara mendalam.

", + "kategoriId": "cmkqb11mc000104jibqg7bdzu", + "imageName": "kesehatan-2.webp" } ] diff --git a/prisma/lib/get_images.ts b/prisma/lib/get_images.ts index fc7812b9..dc5421ae 100644 --- a/prisma/lib/get_images.ts +++ b/prisma/lib/get_images.ts @@ -1,3 +1,5 @@ +import { getValidAuthToken } from "../../src/lib/seafile-auth-service"; + type DirItem = { type: "file" | "dir"; name: string; @@ -5,7 +7,6 @@ type DirItem = { size?: number; }; -const TOKEN = process.env.SEAFILE_TOKEN!; const REPO_ID = process.env.SEAFILE_REPO_ID!; // ⛔ PENTING: RELATIVE PATH (tanpa slash depan) @@ -13,11 +14,12 @@ const DIR_TARGET = "asset-web"; const BASE_URL = process.env.SEAFILE_URL; -const headers = { - Authorization: `Token ${TOKEN}`, -}; - async function getDirItems(): Promise { + const token = await getValidAuthToken(); + const headers = { + Authorization: `Token ${token}`, + }; + const res = await fetch(`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`, { headers, }); @@ -30,6 +32,11 @@ async function getDirItems(): Promise { } async function getDownloadUrl(filePath: string): Promise { + const token = await getValidAuthToken(); + const headers = { + Authorization: `Token ${token}`, + }; + const res = await fetch( `${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`, { headers }, diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts index 3b51b2d0..52e71e27 100644 --- a/prisma/seed_assets.ts +++ b/prisma/seed_assets.ts @@ -38,12 +38,12 @@ export default async function seedAssets() { console.log("🎉 Image seeding completed"); } -// if (import.meta.main) { -// seedAssets() -// .then(() => { -// console.log("seed assets success"); -// }) -// .catch((err) => { -// console.log("gagal seed assets", JSON.stringify(err)); -// }); -// } +if (import.meta.main) { + seedAssets() + .then(() => { + console.log("seed assets success"); + }) + .catch((err) => { + console.log("gagal seed assets", JSON.stringify(err)); + }); +} diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx index 5cc017f7..eced7c77 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx @@ -19,19 +19,27 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) { const searchQuery = searchParams.get('search') || ''; const router = useTransitionRouter() + // Convert kebab-case back to original category name format + // This reverses the transformation done in layoutTabs: item.name.toLowerCase().replace(/\s+/g, '-') + const convertKebabCaseToOriginal = (kebabStr: string): string => { + // Replace hyphens with spaces + return kebabStr.replace(/-/g, ' '); + }; + const decodedKategoriBuku = decodeURIComponent(kategoriBuku); + const originalKategoriName = convertKebabCaseToOriginal(decodedKategoriBuku); const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => { try { setIsLoading(true); - const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku; + const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : originalKategoriName; await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter); setCurrentPage(page); setTotalPages(state.dataPerpustakaan.findMany.totalPages); } finally { setIsLoading(false); } - }, [state.dataPerpustakaan.findMany, decodedKategoriBuku]); + }, [state.dataPerpustakaan.findMany, originalKategoriName, decodedKategoriBuku]); useShallowEffect(() => { loadData(searchQuery); diff --git a/src/app/darmasaba/_com/main-page/landing-page/index.tsx b/src/app/darmasaba/_com/main-page/landing-page/index.tsx index 63655d68..7b205b5d 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx @@ -123,7 +123,7 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess let workHoursMessage = ""; if (["Senin", "Selasa", "Rabu", "Kamis"].includes(day)) { - workHoursMessage = "07:30 - 15:10"; + workHoursMessage = "07:30 - 15:30"; } else if (day === "Jumat") { workHoursMessage = "07:30 - 12:00"; } diff --git a/src/lib/seafile-auth-service.ts b/src/lib/seafile-auth-service.ts new file mode 100644 index 00000000..0c9eeecd --- /dev/null +++ b/src/lib/seafile-auth-service.ts @@ -0,0 +1,109 @@ +import { Mutex } from 'async-mutex'; + +// Store the token and its expiration time +let authToken: string | null = null; +let tokenExpirationTime: number | null = null; + +// Mutex to prevent multiple simultaneous token refresh attempts +const mutex = new Mutex(); + +// Function to authenticate with Seafile and get a new token +async function authenticateWithSeafile(): Promise<{token: string, expirationTime: number}> { + // First, check if we have a static token as fallback + const staticToken = process.env.SEAFILE_TOKEN; + if (staticToken) { + console.log("Using static SEAFILE_TOKEN from environment variables"); + // For static tokens, we'll set a conservative expiration (e.g., 6 days) to force periodic refresh attempts + const conservativeExpiration = Date.now() + (6 * 24 * 60 * 60 * 1000); // 6 days from now + return { + token: staticToken, + expirationTime: conservativeExpiration + }; + } + + // Otherwise, use username/password to get a new token + const username = process.env.SEAFILE_USERNAME; + const password = process.env.SEAFILE_PASSWORD; + const baseUrl = process.env.SEAFILE_URL; + + if (!username || !password || !baseUrl) { + throw new Error('Missing required Seafile environment variables (either SEAFILE_TOKEN or SEAFILE_USERNAME/SEAFILE_PASSWORD/SEAFILE_URL)'); + } + + const response = await fetch(`${baseUrl}/api2/auth-token/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + username, + password, + // Note: Seafile tokens typically last 7 days by default, but we'll refresh earlier + }).toString(), + }); + + if (!response.ok) { + throw new Error(`Authentication failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + // Calculate expiration time (set to refresh 1 hour before actual expiration) + // Seafile tokens typically last 7 days (604800 seconds), so we refresh after 6 days and 23 hours (601200 seconds) + const refreshTokenThreshold = 601200; // 6 days and 23 hours in seconds + const expirationTime = Date.now() + (refreshTokenThreshold * 1000); + + return { + token: data.token, + expirationTime + }; +} + +// Function to get a valid authentication token +export async function getValidAuthToken(): Promise { + // Check if we have a valid token that hasn't expired + if (authToken && tokenExpirationTime && Date.now() < tokenExpirationTime) { + return authToken; + } + + // Acquire lock to prevent multiple simultaneous refresh attempts + return mutex.runExclusive(async () => { + // Double-check after acquiring the lock + if (authToken && tokenExpirationTime && Date.now() < tokenExpirationTime) { + return authToken; + } + + // Get a new token + const { token, expirationTime } = await authenticateWithSeafile(); + + // Update the stored token and expiration time + authToken = token; + tokenExpirationTime = expirationTime; + + console.log('New Seafile token acquired and cached'); + + return token; + }); +} + +// Function to force refresh the token (useful for manual refresh or testing) +export async function refreshAuthToken(): Promise { + return mutex.runExclusive(async () => { + const { token, expirationTime } = await authenticateWithSeafile(); + + // Update the stored token and expiration time + authToken = token; + tokenExpirationTime = expirationTime; + + console.log('Seafile token refreshed'); + + return token; + }); +} + +// Function to check if the token is still valid +export function isTokenValid(): boolean { + return authToken !== null && + tokenExpirationTime !== null && + Date.now() < tokenExpirationTime; +} \ No newline at end of file