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