diff --git a/.env b/.env index ca0d499..405f0f9 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ + DATABASE_URL="postgresql://stephen:Web2025%24%24@192.168.1.210:5432/webcalibre2" # values generated by Gemini # Generated for security diff --git a/.env.local b/.env.local index ca0d499..405f0f9 100644 --- a/.env.local +++ b/.env.local @@ -1,4 +1,5 @@ + DATABASE_URL="postgresql://stephen:Web2025%24%24@192.168.1.210:5432/webcalibre2" # values generated by Gemini # Generated for security diff --git a/docs/Ai-converstion.md b/docs/Ai-converstion.md index 9a75022..bdfc193 100644 --- a/docs/Ai-converstion.md +++ b/docs/Ai-converstion.md @@ -12952,5 +12952,681 @@ Next Step Whenever you are ready to start tomorrow, you can simply say "Let's implement the Metadata Extractor in the sync action," and we can pick up right where we left off. Would you like me to do anything else before you wrap up for the day? +I got the following next error:- + +Build Error + + + + + +Export default doesn't exist in target module + + + +./src/auth.ts (5:1) + + + +Export default doesn't exist in target module 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; + +4 | import { prisma } from "@/lib/prisma";> 5 | import authConfig from "./auth.config"; + +| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +6 | + +7 | export const { handlers, signIn, signOut, auth } = NextAuth({ + +8 | adapter: PrismaAdapter(prisma), + + + +The export default was not found in module [project]/src/auth.config.ts [middleware] (ecmascript). + +Did you mean to import getFreshAccessToken? + +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the requested export doesn't exist. + + + +Import trace: + +Middleware: + +./src/auth.ts + +./src/proxy.ts + +Here is the current version src/auth.config.ts please update the full code if required + +// src/auth-utils.ts + +import { prisma } from "@/lib/prisma"; + + + +export async function getFreshAccessToken(userId: string) { + +const account = await prisma.account.findFirst({ + +where: { userId }, + +}); + + + +if (!account || !account.refresh_token) { + +throw new Error("No refresh token available."); + +} + + + +const isExpired = (account.expires_at ?? 0) * 1000 < Date.now() + 60000; + +if (!isExpired && account.access_token) return account.access_token; + + + +console.log("🔄 Refreshing token via 'common' endpoint for:", userId); + + +try { + +const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + +method: "POST", + +headers: { "Content-Type": "application/x-www-form-urlencoded" }, + +body: new URLSearchParams({ + +client_id: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, + +client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, + +grant_type: "refresh_token", + +refresh_token: account.refresh_token, + +// MUST include these for multi-tenant 'common' refresh to succeed + +scope: "openid profile offline_access Files.ReadWrite Files.Read", + +}), + +}); + + + +const tokens = await response.json(); + +if (!response.ok) { + +console.error("❌ Microsoft Refresh Error:", tokens); + +throw tokens; + +} + + + +await prisma.account.update({ + +where: { id: account.id }, + +data: { + +access_token: tokens.access_token, + +expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), + +refresh_token: tokens.refresh_token ?? account.refresh_token, + +}, + +}); + + + +return tokens.access_token; + +} catch (error) { + +throw new Error("RefreshAccessTokenError"); + +} + +} + +Here is the current version src/auth.ts please update the full code if required:- + +// src/auth.ts + +import NextAuth from "next-auth"; + +import { PrismaAdapter } from "@auth/prisma-adapter"; + +import { prisma } from "@/lib/prisma"; + +import authConfig from "./auth.config"; + + + +export const { handlers, signIn, signOut, auth } = NextAuth({ + +adapter: PrismaAdapter(prisma), + +session: { strategy: "jwt" }, + +...authConfig, + +callbacks: { + +async jwt({ token, account, user }) { + +// 1. Handle OAuth tokens (from first sign-in) + +// This captures the tokens directly from the Microsoft Azure response + +if (account) { + +token.accessToken = account.access_token; + +token.refreshToken = account.refresh_token; + +token.expiresAt = account.expires_at; + +} + + + +// 2. Attach User ID and Role to the token + +// This runs when the user first logs in + +if (user) { + +token.sub = user.id; + +// @ts-ignore - 'role' is a custom field in your Postgres User table + +token.role = user.role; + +} + + + +return token; + +}, + + + +async session({ session, token }) { + +// 3. Pass values from the JWT Token into the Client-facing Session + +// This makes the tokens and IDs available to your API routes and Components + +if (session?.user) { + +session.user.id = token.sub as string; + + +// @ts-ignore - Attaching the role for UI permissions + +session.user.role = token.role as string; + + +// IMPORTANT: We must attach the accessToken here so the + +// /api/download route can use it to fetch from MS Graph + +session.accessToken = token.accessToken as string; + +} + +return session; + +}, + +}, + + +events: { + +async linkAccount({ account, user }) { + +console.log("🔗 Account linked successfully for user:", user.id); + +if (!account.refresh_token) { + +console.warn("⚠️ WARNING: No refresh_token received in linkAccount event!"); + +} + +} + +} + +}); + +The error is occurring because auth.ts expects an export default from auth.config.ts, but your current auth.config.ts only exports a function (getFreshAccessToken) and does not actually contain the NextAuth configuration object. + +Additionally, your auth.config.ts currently imports prisma. In Next.js, the Middleware runs on the Edge runtime, which often conflicts with the full Prisma Client. To fix this, we should separate the token logic from the configuration object. + +Here is the fix: + +1. Update src/auth.config.ts +This file should contain the authentication providers and basic settings that the Middleware can read safely. + +TypeScript +// src/auth.config.ts +import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; +import type { NextAuthConfig } from "next-auth"; + +export default { + providers: [ + MicrosoftEntraID({ + clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, + clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, + issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER, + authorization: { + params: { + // offline_access is vital for getting the refresh_token + scope: "openid profile email offline_access Files.ReadWrite Files.Read", + }, + }, + }), + ], +} satisfies NextAuthConfig; +2. Move Token Logic to src/lib/auth-utils.ts +To keep things clean and avoid the Middleware error, move the Prisma-dependent code to a utility file. (You can create this file if it doesn't exist). + +TypeScript +// src/lib/auth-utils.ts +import { prisma } from "@/lib/prisma"; + +export async function getFreshAccessToken(userId: string) { + const account = await prisma.account.findFirst({ + where: { userId }, + }); + + if (!account || !account.refresh_token) { + throw new Error("No refresh token available."); + } + + const isExpired = (account.expires_at ?? 0) * 1000 < Date.now() + 60000; + if (!isExpired && account.access_token) return account.access_token; + + console.log("🔄 Refreshing token via 'common' endpoint for:", userId); + + try { + const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, + client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, + grant_type: "refresh_token", + refresh_token: account.refresh_token, + scope: "openid profile offline_access Files.ReadWrite Files.Read", + }), + }); + + const tokens = await response.json(); + if (!response.ok) throw tokens; + + await prisma.account.update({ + where: { id: account.id }, + data: { + access_token: tokens.access_token, + expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), + refresh_token: tokens.refresh_token ?? account.refresh_token, + }, + }); + + return tokens.access_token; + } catch (error) { + console.error("❌ Microsoft Refresh Error:", error); + throw new Error("RefreshAccessTokenError"); + } +} +3. Update src/auth.ts +Now we import the default config and combine it with the Prisma adapter and callbacks. + +TypeScript +// src/auth.ts +import NextAuth from "next-auth"; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { prisma } from "@/lib/prisma"; +import authConfig from "./auth.config"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + adapter: PrismaAdapter(prisma), + session: { strategy: "jwt" }, + ...authConfig, // This now spreads the default export from auth.config.ts + callbacks: { + async jwt({ token, account, user }) { + if (account) { + token.accessToken = account.access_token; + token.refreshToken = account.refresh_token; + token.expiresAt = account.expires_at; + } + + if (user) { + token.sub = user.id; + // @ts-ignore + token.role = user.role; + } + + return token; + }, + + async session({ session, token }) { + if (session?.user) { + session.user.id = token.sub as string; + // @ts-ignore + session.user.role = token.role as string; + session.accessToken = token.accessToken as string; + } + return session; + }, + }, + + events: { + async linkAccount({ account, user }) { + console.log("🔗 Account linked successfully for user:", user.id); + if (!account.refresh_token) { + console.warn("⚠️ WARNING: No refresh_token received!"); + } + } + } +}); +Summary of what changed: +Resolved Error: Created a default export in auth.config.ts so auth.ts can find it. + +Separation of Concerns: Moved Prisma database logic out of the configuration file. This prevents Middleware from trying to load Prisma in an environment (Edge) that doesn't support it. + +Stitched imports: Ensured auth.ts correctly spreads the config. + +Would you like me to update your middleware.ts to ensure it's compatible with this new split structure? + +here is the current version of the src/lib/auth-utils.ts please let me know if the current version needs to be update: + +// src/lib/auth-utils.ts + + + +import { prisma } from "@/lib/prisma"; + + + +export async function getFreshAccessToken(userId: string) { + +// 1. Find the account in PostgreSQL + +const account = await prisma.account.findFirst({ + +where: { userId }, + +}); + + + +if (!account || !account.refresh_token) { + +throw new Error("No refresh token available. User might need to re-login."); + +} + + + +// 2. Check if the token is expired (with a 1-minute buffer) + +// account.expires_at is usually in seconds, so we multiply by 1000 + +const isExpired = (account.expires_at ?? 0) * 1000 < Date.now() + 60000; + + + +if (!isExpired && account.access_token) { + +return account.access_token; + +} + + + +// 3. If expired, request a new one from Microsoft + +console.log("🔄 Access token expired. Refreshing for user:", userId); + + +try { + +const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + +method: "POST", + +headers: { "Content-Type": "application/x-www-form-urlencoded" }, + +body: new URLSearchParams({ + +client_id: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, + +client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, + +grant_type: "refresh_token", + +refresh_token: account.refresh_token, + +}), + +}); + + + +const tokens = await response.json(); + + + +if (!response.ok) throw tokens; + + + +// 4. Update the Account table with the new tokens + +await prisma.account.update({ + +where: { id: account.id }, + +data: { + +access_token: tokens.access_token, + +expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), + +refresh_token: tokens.refresh_token ?? account.refresh_token, + +}, + +}); + + + +return tokens.access_token; + +} catch (error) { + +console.error("❌ Failed to refresh Microsoft token:", error); + +throw new Error("RefreshAccessTokenError"); + +} + +} + +Your current src/lib/auth-utils.ts is mostly correct, but there is one critical addition needed for the "common" Microsoft endpoint to work reliably with the refresh token. + +When using the https://login.microsoftonline.com/common/... endpoint (which is used for multi-tenant apps), Microsoft usually requires you to re-specify the scopes during the refresh request. If you don't, it might return a token that lacks the permissions needed to read/write files in OneDrive. + +Updated src/lib/auth-utils.ts +I have added the scope parameter to the fetch body and refined the error handling slightly for better debugging. + +TypeScript +// src/lib/auth-utils.ts +import { prisma } from "@/lib/prisma"; + +export async function getFreshAccessToken(userId: string) { + // 1. Find the account in PostgreSQL + const account = await prisma.account.findFirst({ + where: { userId }, + }); + + if (!account || !account.refresh_token) { + throw new Error("No refresh token available. User might need to re-login."); + } + + // 2. Check if the token is expired (with a 1-minute buffer) + const isExpired = (account.expires_at ?? 0) * 1000 < Date.now() + 60000; + + if (!isExpired && account.access_token) { + return account.access_token; + } + + // 3. If expired, request a new one from Microsoft + console.log("🔄 Access token expired. Refreshing for user:", userId); + + try { + const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, + client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, + grant_type: "refresh_token", + refresh_token: account.refresh_token, + // CRITICAL: Re-declare scopes to ensure the new access_token has OneDrive permissions + scope: "openid profile offline_access Files.ReadWrite Files.Read", + }), + }); + + const tokens = await response.json(); + + if (!response.ok) { + console.error("❌ Microsoft Token Refresh Response Error:", tokens); + throw tokens; + } + + // 4. Update the Account table with the new tokens + await prisma.account.update({ + where: { id: account.id }, + data: { + access_token: tokens.access_token, + expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), + // Microsoft sometimes rotates the refresh_token; save it if they provide a new one + refresh_token: tokens.refresh_token ?? account.refresh_token, + }, + }); + + return tokens.access_token; + } catch (error) { + console.error("❌ Failed to refresh Microsoft token:", error); + // Returning a specific error string helps Auth.js or your components handle re-auth + throw new Error("RefreshAccessTokenError"); + } +} +Key Changes Made: +Scope Inclusion: Added scope: "openid profile offline_access Files.ReadWrite Files.Read". Without this, the refreshed token often defaults to a basic profile scope, causing your OneDrive sync to fail with a 403 Forbidden. + +Logging: Added a log to capture the specific error response from Microsoft. This is vital because if the client_secret expires or the user revokes access, Microsoft will tell you exactly why in that JSON response. + +Token Rotation: Explicitly handled tokens.refresh_token ?? account.refresh_token. Microsoft Entra ID often "rotates" refresh tokens (gives you a new one to use next time). If you don't save the new one, your next refresh attempt will fail. + +One final check for your Build Error: +Since you moved this to src/lib/auth-utils.ts, make sure your src/auth.config.ts does not import this file. The Middleware (which uses auth.config.ts) cannot handle the prisma import inside this utility. + +Would you like me to double-check your middleware.ts to ensure it isn't accidentally pulling in this Prisma-dependent code? + +where did this function getFreshAccessToken(userId: string) as it was previously in src/lib/auth-utils.ts + +In your previous error message, you showed a snippet where getFreshAccessToken was located inside src/auth.config.ts. + +The reason we moved it specifically to src/lib/auth-utils.ts is to solve that Build Error you encountered. + +The "Why" Behind the Move +In NextAuth v5, your project has two very different environments trying to read your auth settings: + +The Middleware (Edge Runtime): This runs on every single request. It is very "lightweight" and cannot run heavy libraries like Prisma. It reads src/auth.config.ts. + +The Main App (Node.js Runtime): This handles your actual login, session creation, and database calls. It reads src/auth.ts. + +By moving getFreshAccessToken (which uses Prisma) out of the config and into its own utility file, we ensure the Middleware stays "clean." If the function stays in auth.config.ts, the Middleware tries to load Prisma, fails, and gives you that "Export default doesn't exist" or "Prisma cannot run at the Edge" error. + +Where the function is now +You should have the function exactly where you just showed me: src/lib/auth-utils.ts. + +To use it in your Sync Actions or API Routes, you simply import it like this: + +TypeScript +// Example: src/app/dashboard/sync-actions.ts +import { getFreshAccessToken } from "@/lib/auth-utils"; +import { auth } from "@/auth"; + +export async function syncOneDrive() { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + // Get a fresh token from the DB (or refresh if needed) + const token = await getFreshAccessToken(session.user.id); + + // Use that token to call MS Graph... +} +Summary of the File structure: +src/auth.config.ts: Contains Providers only (No Prisma imports). + +src/auth.ts: Contains Database Adapter and Callbacks (Imports auth.config.ts). + +src/lib/auth-utils.ts: Contains getFreshAccessToken (Imports prisma). + +Does the project build successfully now that the function is separated into the utility file? +The project build successfully now allows me to authenticate into the app, I will save into my local git repository and we should then see if we can work out what is going on with the metadata functionality + +That is a huge win! Getting the authentication and token refresh logic stable is often the hardest part of building a Microsoft-integrated app. With that foundation solid and your local git repository updated, you have a safe "restore point." + +Since the project builds and you can log in, we can now focus on the Metadata Extraction logic. + +To bridge the gap between OneDrive and your Database, we need to ensure the sync process doesn't just look at file names, but actually "looks inside" the files to pull out the metadata we want (Author, Page Count, Image Dimensions, etc.). + +Preparing for the Metadata Phase +When you're ready to start, here is how we will approach the "What is going on" investigation: + +The Sync Hook: We will modify your syncOneDrive action to identify which files are new or updated. + +The Download Stream: We'll use the Microsoft Graph API to get a temporary download URL for those specific files. + +The Extraction: We'll pass that file data through the extractMetadata utility we prepared earlier. + +The Search Verification: We'll verify that the extracted data shows up in your DataGrid's "Hidden Metadata" search. + +One quick check before you dive in tomorrow: +Does your FileNode table in Prisma already have the metadata column defined as a Json type? + +Code snippet +// Check your schema.prisma for something like this: +model FileNode { + id String @id @default(cuid()) + name String + metadata Json? // This is where we will store the Author, Dimensions, etc. + // ... rest of your fields +} +If it does, we are in perfect shape. Whenever you're ready, just let me know and we’ll start by hooking up the PDF and ePub parsers! + +Would you like me to wait for your signal tomorrow, or should I draft the updated Sync Action for you to review now? + diff --git a/docs/Ai-converstion.pdf b/docs/Ai-converstion.pdf index 7b30e59..6298e8b 100644 Binary files a/docs/Ai-converstion.pdf and b/docs/Ai-converstion.pdf differ diff --git a/docs/notes.md b/docs/notes.md index cb5e081..bd1183a 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -438,4 +438,12 @@ Uses **Prisma + PostgreSQL** for metadata storage and **MUI X v8 (DataGrid)** fo ## 10.5. 💡 Reminders * **Hard Refresh:** If the DataGrid UI behaves weirdly after code changes, use `Cmd + Shift + R` or `Ctrl + F5`. -* **Z-Index:** The search bar animation uses `gridArea: '1 / 1'`. If icons overlap, check the styled-components logic in `dashboard-view.tsx`. \ No newline at end of file +* **Z-Index:** The search bar animation uses `gridArea: '1 / 1'`. If icons overlap, check the styled-components logic in `dashboard-view.tsx`. + + +1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg + + +1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg + +1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg \ No newline at end of file diff --git a/docs/notes.pdf b/docs/notes.pdf index 1284cd3..d6c804f 100644 Binary files a/docs/notes.pdf and b/docs/notes.pdf differ diff --git a/package-lock.json b/package-lock.json index d737edb..0a5bdf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,16 @@ "@mui/x-data-grid": "^8.24.0", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "epub": "^1.3.0", + "exif-reader": "^2.0.3", "next": "16.1.1", "next-auth": "^5.0.0-beta.30", + "pdf-parse": "^2.4.5", "pg": "^8.16.3", "react": "19.2.3", "react-dom": "19.2.3", - "server-only": "^0.0.1" + "server-only": "^0.0.1", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^20", @@ -732,7 +736,6 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -1601,6 +1604,190 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2613,6 +2800,13 @@ "win32" ] }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2636,6 +2830,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2653,6 +2856,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2669,6 +2882,25 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2918,7 +3150,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -2934,7 +3166,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3142,6 +3374,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -3167,6 +3406,16 @@ "node": ">=6" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3191,7 +3440,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/confbox": { @@ -3211,6 +3460,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3218,6 +3474,13 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "optional": true + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3333,6 +3596,16 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3393,6 +3666,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -3405,7 +3685,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3541,6 +3820,18 @@ "node": ">=14" } }, + "node_modules/epub": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/epub/-/epub-1.3.0.tgz", + "integrity": "sha512-6BL8gIitljkTf4HW52Ast6wenPTkMKllU28bRc5awVsT+xCaPl6nWSaqSmHbRgPrl1+5uekOPvOxy7DQzbhM8Q==", + "dependencies": { + "adm-zip": "^0.4.11", + "xml2js": "^0.4.23" + }, + "optionalDependencies": { + "zipfile": "^0.5.11" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -4173,6 +4464,12 @@ "node": ">=0.10.0" } }, + "node_modules/exif-reader": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-2.0.3.tgz", + "integrity": "sha512-zFbQvguwT9JkqyYhR7pjE1Yn8SagwaGLNRU0Oh14xFa1paSf5Gzxn4gxgk0XhnudI0UIqU+HgnBX93+nva592A==", + "license": "MIT" + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -4350,6 +4647,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4390,6 +4704,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4498,6 +4830,28 @@ "giget": "dist/cli.mjs" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4635,6 +4989,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4673,6 +5034,19 @@ "react-is": "^16.7.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4683,6 +5057,16 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4709,6 +5093,32 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4913,6 +5323,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "license": "MIT", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5415,7 +5838,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5428,18 +5851,59 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "license": "ISC", + "optional": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5481,6 +5945,34 @@ "dev": true, "license": "MIT" }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/next": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", @@ -5568,6 +6060,52 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/node-pre-gyp/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/node-pre-gyp/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5575,6 +6113,73 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "license": "ISC", + "optional": true + }, + "node_modules/npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "license": "ISC", + "optional": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nypm": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", @@ -5733,6 +6338,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5751,6 +6366,38 @@ "node": ">= 0.8.0" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -5841,6 +6488,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5873,6 +6530,38 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -6141,6 +6830,13 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6200,6 +6896,32 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -6254,6 +6976,29 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6368,6 +7113,20 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6412,6 +7171,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6447,6 +7213,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6469,6 +7251,13 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "license": "MIT" }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6524,7 +7313,6 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -6568,7 +7356,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -6675,6 +7462,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6723,6 +7517,31 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "license": "MIT", + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6836,6 +7655,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6913,6 +7745,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", + "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/tar/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -7269,6 +8141,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7374,6 +8253,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7384,6 +8273,35 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -7397,7 +8315,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/yaml": { @@ -7422,6 +8340,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zipfile": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz", + "integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "~2.10.0", + "node-pre-gyp": "~0.10.2" + }, + "bin": { + "unzip.js": "bin/unzip.js" + } + }, "node_modules/zod": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz", diff --git a/package.json b/package.json index 78a931c..6a04438 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,16 @@ "@mui/x-data-grid": "^8.24.0", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "epub": "^1.3.0", + "exif-reader": "^2.0.3", "next": "16.1.1", "next-auth": "^5.0.0-beta.30", + "pdf-parse": "^2.4.5", "pg": "^8.16.3", "react": "19.2.3", "react-dom": "19.2.3", - "server-only": "^0.0.1" + "server-only": "^0.0.1", + "sharp": "^0.34.5" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts index db8b54c..8e4eebd 100644 --- a/src/app/dashboard/actions.ts +++ b/src/app/dashboard/actions.ts @@ -16,6 +16,9 @@ import { uploadToOneDrive } from "@/services/onedrive"; +import { getEnrichedMetadataFromCloud } from "@/data-access/file-nodes"; + + /** * 1. FETCH: Get all file nodes * Now simply calls the DAL. Error handling is left to the caller (the UI). @@ -112,4 +115,24 @@ export async function updateFileNodeAction(id: string, formData: FormData) { console.error("Update Error:", error); return { success: false, error: "Failed to update record" }; } +} + +export async function getMetadataPreviewAction(fileId: string) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + console.log(`🔍 Starting enhancement for file: ${fileId}`); + + // This calls the DAL -> which calls the Service -> which calls OneDrive + const data = await getEnrichedMetadataFromCloud(fileId); + + // This log will show you exactly what we found in your terminal! + console.log("✅ Extracted Metadata Result:", data); + + return { success: true, data }; + } catch (error: any) { + console.error("❌ Enhancement Action Error:", error.message); + return { success: false, error: error.message }; + } } \ No newline at end of file diff --git a/src/app/dashboard/dashboard-view.tsx b/src/app/dashboard/dashboard-view.tsx index 8ff9bed..0ea7e47 100644 --- a/src/app/dashboard/dashboard-view.tsx +++ b/src/app/dashboard/dashboard-view.tsx @@ -47,7 +47,15 @@ const StyledQuickFilter = styled(QuickFilter)({ function CustomToolbar() { return ( - + + Library @@ -89,6 +97,7 @@ function CustomToolbar() { )} /> + ); } @@ -230,7 +239,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps {lastSynced && ( - + Last synced: {lastSynced.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} )} diff --git a/src/app/dashboard/sync-actions.ts b/src/app/dashboard/sync-actions.ts index 11fdfe8..430582d 100644 --- a/src/app/dashboard/sync-actions.ts +++ b/src/app/dashboard/sync-actions.ts @@ -5,15 +5,13 @@ import { auth } from "@/auth"; import { revalidatePath } from "next/cache"; import { upsertFileNode } from "@/data-access/file-nodes"; import { getWebCalibreChildren } from "@/services/onedrive"; -// src/app/dashboard/sync-actions.ts -//import { upsertFileNode } from "@/data-access/file-nodes"; // Change this from /services/onedrive +import { extractMetadata } from "@/lib/metadata-extractor"; // Import your utility export async function syncOneDrive() { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); try { - // 1. Call Service to get cloud data (token refresh handled inside service) const items = await getWebCalibreChildren(session.user.id); let syncedCount = 0; @@ -22,19 +20,46 @@ export async function syncOneDrive() { for (const item of items) { const isFolder = !!item.folder; - // Business Logic: Skip UUID storage folders if (isFolder && uuidRegex.test(item.name)) continue; - const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); + const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toLowerCase() || 'unknown'); + + let deepMetadata = {}; - // 2. Call DAL to save to database + // --- NEW: Extraction Logic --- + // Only extract for specific types to save time/bandwidth + const supportedTypes = ['pdf', 'epub', 'jpg', 'jpeg', 'png', 'webp']; + + if (!isFolder && supportedTypes.includes(extension)) { + const downloadUrl = item['@microsoft.graph.downloadUrl']; + + if (downloadUrl) { + try { + // Fetch the file content as an ArrayBuffer + const response = await fetch(downloadUrl); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Extract the "deep" metadata (Author, Title, etc.) + deepMetadata = await extractMetadata(buffer, item.name); + } catch (extractError) { + console.error(`Could not extract metadata for ${item.name}:`, extractError); + } + } + } + + // 2. Save to database with combined metadata await upsertFileNode(item.id, { name: item.name, size: BigInt(item.size || 0), isFolder: isFolder, path: item.parentReference?.path + '/' + item.name, ownerId: session.user.id, - metadata: { type: extension, mimeType: item.file?.mimeType || null }, + metadata: { + type: extension.toUpperCase(), + mimeType: item.file?.mimeType || null, + ...deepMetadata // Merge the extracted Author, Title, etc. + }, }); syncedCount++; diff --git a/src/app/update/[id]/_actions.ts b/src/app/update/[id]/_actions.ts index 59060f2..40322d6 100644 --- a/src/app/update/[id]/_actions.ts +++ b/src/app/update/[id]/_actions.ts @@ -8,16 +8,16 @@ import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes"; /** * SERVER ACTION: Updates file metadata and organizational data. - * This refactored version uses the Data Access Layer (DAL) to - * ensure separation of concerns. */ export async function updateFileAction(formData: FormData) { const session = await auth(); // 1. Authorization Guard - if (!session?.user?.id) throw new Error("Unauthorized"); + if (!session?.user?.id) { + return { success: false, message: "Unauthorized" }; + } - // 2. Data Extraction + // 2. Data Extraction from FormData const id = formData.get("id") as string; const name = formData.get("name") as string; const description = formData.get("description") as string; @@ -25,33 +25,35 @@ export async function updateFileAction(formData: FormData) { const customMetadataRaw = formData.get("customMetadata") as string; const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; - const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; - + try { - // 3. DAL: Fetch existing record to safely merge metadata - // This replaces the direct prisma.fileNode.findUnique call + // 3. Parse the incoming metadata from the UI + const newMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + // 4. DAL: Fetch existing record to safely merge system fields const existing = await getFileNodeById(id); if (!existing) throw new Error("File record not found"); + if (existing.ownerId !== session.user.id) throw new Error("Permission denied"); const existingMetadata = (existing.metadata as Record) || {}; - // 4. Logic: Prepare the updated metadata object + // 5. Logic: Merge Strategy + // We keep internal system fields like 'mimeType' but allow the + // user-approved 'newMetadata' (including extracted GPS/Author) to take precedence. const updatedMetadata = { - ...customMetadata, // Apply new user keys - type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", - mimeType: existingMetadata.mimeType // Ensure system metadata isn't overwritten + ...existingMetadata, // Keep everything we currently have + ...newMetadata, // Overwrite with the fields the user just approved/edited }; - // 5. DAL: Perform the update - // This replaces the direct prisma.fileNode.update call + // 6. DAL: Perform the update via your file-nodes logic await updateFileNode(id, { - name, + name: name || existing.name, description, parentId, metadata: updatedMetadata, }); - // 6. Cache Invalidation + // 7. Cache Invalidation revalidatePath("/dashboard"); revalidatePath(`/update/${id}`); diff --git a/src/app/update/[id]/page.tsx b/src/app/update/[id]/page.tsx index 4a12575..3874726 100644 --- a/src/app/update/[id]/page.tsx +++ b/src/app/update/[id]/page.tsx @@ -4,31 +4,53 @@ import { prisma } from "@/lib/prisma"; import { Container } from "@mui/material"; import UpdateView from "./update-view"; -// Note: params is now handled as a Promise export default async function UpdatePage({ params }: { params: Promise<{ id: string }> }) { const session = await auth(); - if (!session) redirect("/"); + + // Security: Ensure the user is logged in + if (!session?.user?.id) { + redirect("/"); + } - // 1. Await the params to get the actual ID + // 1. Await the params to get the actual ID from the URL const { id } = await params; - // 2. Fetch the specific file using the awaited ID + // 2. Fetch the specific file. + // We include ownerId in the where clause to prevent users from editing each other's files. const fileNode = await prisma.fileNode.findUnique({ - where: { id: id } + where: { + id: id, + ownerId: session.user.id + } }); - if (!fileNode) notFound(); + if (!fileNode) { + notFound(); + } - // Fetch folders for the destination dropdown + // 3. Fetch folders for the destination dropdown (if you decide to allow moving files) const folders = await prisma.fileNode.findMany({ - where: { isFolder: true }, + where: { + isFolder: true, + ownerId: session.user.id + }, orderBy: { name: 'asc' }, select: { id: true, name: true } }); + // 4. Convert Decimal/BigInt fields to strings/numbers if necessary for client serialization + const serializedFileNode = { + ...fileNode, + size: fileNode.size ? fileNode.size.toString() : "0" // BigInt cannot be passed directly to Client Components + // metadata is already a JSON object, so it passes through fine + }; + return ( - + ); } \ No newline at end of file diff --git a/src/app/update/[id]/update-view.tsx b/src/app/update/[id]/update-view.tsx index 0899fc2..288b7b7 100644 --- a/src/app/update/[id]/update-view.tsx +++ b/src/app/update/[id]/update-view.tsx @@ -1,41 +1,89 @@ 'use client'; +// src/app/update/[id]/update-view.tsx + import { useState } from "react"; import { Box, Button, Typography, Paper, Stack, - TextField, MenuItem, IconButton, Grid, Divider + TextField, MenuItem, IconButton, Grid, Divider, + Checkbox, CircularProgress, Chip, Tooltip } from "@mui/material"; import SaveIcon from "@mui/icons-material/Save"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import AssignmentIcon from '@mui/icons-material/Assignment'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import FolderIcon from "@mui/icons-material/Folder"; +import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; +import MapIcon from '@mui/icons-material/Map'; import { useRouter } from "next/navigation"; import { updateFileAction } from "./_actions"; +import { getMetadataPreviewAction } from "@/app/dashboard/actions"; interface MetadataPair { key: string; value: string; + selected: boolean; // Checkbox state + isPending?: boolean; // Visual highlight for auto-extracted fields } -export default function UpdateView({ fileNode, folders }: any) { +export default function UpdateView({ + fileNode, + folders: availablefolders // Renaming 'folders' to 'availablefolders' +}: { + fileNode: any; + folders: any[]; +}) { const router = useRouter(); const [loading, setLoading] = useState(false); + const [isExtracting, setIsExtracting] = useState(false); // 1. Initialize Basic Info const [name, setName] = useState(fileNode.name); const [description, setDescription] = useState(fileNode.description || ""); const [parentId, setParentId] = useState(fileNode.parentId || ""); - // 2. Parse existing JSON metadata into Key/Value array for the UI - // We filter out 'type' and 'mimeType' as they are system-managed - const initialMetadata = Object.entries(fileNode.metadata || {}) + // 2. Initialize Metadata from DB (all checked by default) + const initialMetadata: MetadataPair[] = Object.entries(fileNode.metadata || {}) .filter(([key]) => !['type', 'mimeType'].includes(key)) - .map(([key, value]) => ({ key, value: String(value) })); + .map(([key, value]) => ({ + key, + value: String(value), + selected: true, + isPending: false + })); const [customMetadata, setCustomMetadata] = useState(initialMetadata); + // --- MAGIC FILL LOGIC --- + const handleMagicEnhance = async () => { + setIsExtracting(true); + try { + const result = await getMetadataPreviewAction(fileNode.id); + if (result.success) { + // Convert extracted JSON into pending rows + const extractedRows: MetadataPair[] = Object.entries(result.data ?? {}) + .filter(([key]) => !['type', 'mimeType'].includes(key)) + .map(([key, value]) => ({ + key, + value: String(value), + selected: true, // Default to checked as requested + isPending: true + })); + + // Merge logic: Add only if the key doesn't already exist in our list + setCustomMetadata(prev => { + const existingKeys = new Set(prev.map(r => r.key)); + const filteredNew = extractedRows.filter(r => !existingKeys.has(r.key)); + return [...prev, ...filteredNew]; + }); + } + } catch (err) { + alert("Failed to extract metadata. Ensure service is configured correctly."); + } finally { + setIsExtracting(false); + } + }; + const handleUpdate = async () => { setLoading(true); const formData = new FormData(); @@ -44,9 +92,11 @@ export default function UpdateView({ fileNode, folders }: any) { formData.append("description", description); formData.append("parentId", parentId); - // Convert array back to object for storage + // Convert array back to object, ONLY including selected/checked rows const metadataObj = customMetadata.reduce((acc, curr) => { - if (curr.key.trim()) acc[curr.key.trim()] = curr.value; + if (curr.selected && curr.key.trim()) { + acc[curr.key.trim()] = curr.value; + } return acc; }, {} as Record); @@ -72,8 +122,28 @@ export default function UpdateView({ fileNode, folders }: any) { Edit File Details + {/* --- MAGIC FILL BUTTON --- */} + + + + Enrich Metadata + + Extract tags like GPS, Author, and Dimensions from the original file. + + + + + + - {/* Name Field */} - {/* Folder Select */} -- Root -- - {folders.map((f: any) => ( + {availablefolders?.map((f: any) => ( {f.name} ))} - {/* Custom Metadata Section */} - Custom Attributes + Metadata Attributes @@ -115,35 +183,66 @@ export default function UpdateView({ fileNode, folders }: any) { {customMetadata.map((row, index) => ( - - - { - const updated = [...customMetadata]; - updated[index].key = e.target.value; - setCustomMetadata(updated); - }} - /> + + + + + { + const updated = [...customMetadata]; + updated[index].selected = e.target.checked; + setCustomMetadata(updated); + }} + /> + + + + { + const updated = [...customMetadata]; + updated[index].key = e.target.value; + setCustomMetadata(updated); + }} + /> + + + { + const updated = [...customMetadata]; + updated[index].value = e.target.value; + setCustomMetadata(updated); + }} + /> + + + setCustomMetadata(customMetadata.filter((_, i) => i !== index))}> + + + - - { - const updated = [...customMetadata]; - updated[index].value = e.target.value; - setCustomMetadata(updated); - }} - /> - - - setCustomMetadata(customMetadata.filter((_, i) => i !== index))}> - - - - + + {/* --- GOOGLE MAPS SHORTCUT --- */} + {row.key.toLowerCase().includes('latitude') && row.value && ( + + + + )} + ))} diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index fdb7096..f71fcff 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -1,25 +1,30 @@ import { auth } from "@/auth"; import { redirect } from "next/navigation"; -import UploadView from "./upload-view"; +import UploadView from "./upload-view"; // This is the Client Component import { Container } from "@mui/material"; import { prisma } from "@/lib/prisma"; +// 1. Rename to UploadPage to avoid conflict with the 'UploadView' import +// 2. Add 'async' so you can use 'await' inside export default async function UploadPage() { const session = await auth(); if (!session) redirect("/"); - // Fetch only folders so the user can select a destination - const folders = await prisma.fileNode.findMany({ - where: { isFolder: true }, + // 3. Fetch folders. Renamed variable to 'allFolders' to avoid any confusion + const allFolders = await prisma.fileNode.findMany({ + where: { + isFolder: true, + ownerId: session.user.id // Good practice: only show user's own folders + }, orderBy: { name: 'asc' }, select: { id: true, name: true, parentId: true } }); return ( - {/* Pass folders to the view */} - + {/* 4. Render the Client Component and pass the data */} + ); } \ No newline at end of file diff --git a/src/app/upload/upload-view.tsx b/src/app/upload/upload-view.tsx index 9d6fbd9..572ce9c 100644 --- a/src/app/upload/upload-view.tsx +++ b/src/app/upload/upload-view.tsx @@ -3,249 +3,308 @@ import { useState, useRef } from "react"; import { Box, Button, Typography, Paper, Stack, - TextField, MenuItem, IconButton, Divider, - Grid, CircularProgress + TextField, IconButton, Divider, + Grid, + CircularProgress, Checkbox, MenuItem, + InputAdornment, Collapse } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import FolderIcon from "@mui/icons-material/Folder"; -import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; -import AssignmentIcon from '@mui/icons-material/Assignment'; +import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import ClearIcon from '@mui/icons-material/Clear'; import { useRouter } from "next/navigation"; +import { getMetadataPreviewAction } from "@/app/dashboard/actions"; import { uploadFileAction, createFolderAction } from "./_actions"; -interface MetadataPair { +interface MetadataRow { key: string; value: string; + isPending?: boolean; + selected?: boolean; } -export default function UploadView({ user, folders = [] }: any) { +export default function UploadView({ user, folders }: { user: any; folders: any[] }) { const router = useRouter(); const fileInputRef = useRef(null); - const [file, setFile] = useState(null); - const [description, setDescription] = useState(""); - const [parentId, setParentId] = useState(""); - const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle'); - const [customMetadata, setCustomMetadata] = useState([]); - - // Folder Creation State - const [showFolderInput, setShowFolderInput] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [targetFolderId, setTargetFolderId] = useState(""); + const [showNewFolderInput, setShowNewFolderInput] = useState(false); const [newFolderName, setNewFolderName] = useState(""); - const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [rows, setRows] = useState([]); + const [isExtracting, setIsExtracting] = useState(false); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving'>('idle'); - const handleCreateFolder = async () => { - if (!newFolderName.trim()) return; - setIsCreatingFolder(true); + // Logic to determine if the "Complete" button should be active + const canSubmit = selectedFile !== null || newFolderName.trim().length > 0; + + // --- 1. MAGIC EXTRACTION LOGIC --- + const handleMagicEnhance = async () => { + if (!selectedFile) return; + + setIsExtracting(true); try { - // FIX: Pass the current parentId to the action so it nests correctly - const result = await createFolderAction(newFolderName, parentId); + const result = await getMetadataPreviewAction(selectedFile.name); + if (result.success) { - setNewFolderName(""); - setShowFolderInput(false); - router.refresh(); + const extractedRows: MetadataRow[] = Object.entries(result.data ?? {}).map(([k, v]) => ({ + key: k, + value: String(v), + isPending: true, + selected: true + })); + + setRows(prev => { + const existingKeys = new Set(prev.map(r => r.key)); + const newUniqueRows = extractedRows.filter(r => !existingKeys.has(r.key)); + return [...prev, ...newUniqueRows]; + }); } - } catch (err: any) { - alert(err.message || "Failed to create folder"); + } catch (err) { + console.error("Extraction failed:", err); } finally { - setIsCreatingFolder(false); + setIsExtracting(false); } }; - const addMetadataRow = () => setCustomMetadata([...customMetadata, { key: "", value: "" }]); - - const removeMetadataRow = (index: number) => { - setCustomMetadata(customMetadata.filter((_, i) => i !== index)); + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setSelectedFile(file); + } }; - const updateMetadataRow = (index: number, field: 'key' | 'value', val: string) => { - const updated = [...customMetadata]; - updated[index][field] = val; - setCustomMetadata(updated); - }; - - const handleUpload = async () => { - if (!file) return; - setStatus('uploading'); - - const formData = new FormData(); - formData.append("file", file); - formData.append("description", description); - formData.append("parentId", parentId); + // --- 2. SAVE / UPLOAD LOGIC --- + const handleSave = async () => { + if (!canSubmit) return; + setSaveStatus('saving'); - const metadataObj = customMetadata.reduce((acc, curr) => { - if (curr.key.trim()) acc[curr.key.trim()] = curr.value; - return acc; - }, {} as Record); - - formData.append("customMetadata", JSON.stringify(metadataObj)); - try { - const result = await uploadFileAction(formData); - if (result.success) { - setStatus('success'); - setFile(null); - setDescription(""); - setCustomMetadata([]); - router.push("/dashboard"); - router.refresh(); + let currentParentId = targetFolderId; + + // STEP A: Create Folder if user typed a new folder name + if (newFolderName.trim()) { + const folderResult = await createFolderAction(newFolderName, targetFolderId || null); + if (folderResult.success) { + // If successful, we want the file to go inside this NEW folder + currentParentId = folderResult.node.id; + } } - } catch (err) { - alert("Upload failed."); - setStatus('idle'); + + // STEP B: Upload File if a file is selected + if (selectedFile) { + const formData = new FormData(); + formData.append("file", selectedFile); + formData.append("parentId", currentParentId || "root"); + + // Construct Metadata Object + const metadataObject = rows + .filter(r => r.selected && r.key.trim() !== "") + .reduce((acc, curr) => { + acc[curr.key.trim()] = curr.value; + return acc; + }, {} as Record); + + formData.append("customMetadata", JSON.stringify(metadataObject)); + + const uploadResult = await uploadFileAction(formData); + + if (!uploadResult.success) { + throw new Error(uploadResult.error || "Upload failed"); + } + } + + console.log("✅ Process complete. Returning to dashboard."); + router.push("/dashboard"); + router.refresh(); + } catch (err: any) { + console.error("Save failed:", err); + alert(err.message || "An error occurred while saving."); + } finally { + setSaveStatus('idle'); } }; return ( - Add to Library + Upload & Enrich - - {/* 1. Destination */} + + {/* FOLDER SELECTION */} - - 1. Destination - - setParentId(e.target.value)} - size="small" - slotProps={{ - select: { displayEmpty: true }, - inputLabel: { shrink: true }, + label="Destination Folder" + value={targetFolderId} + onChange={(e) => { + setTargetFolderId(e.target.value); + if (e.target.value) setShowNewFolderInput(false); }} > - -- Root (Main Folder) -- - {folders.map((f: any) => ( + -- Root Directory -- + {folders?.map((f) => ( {f.name} ))} - setShowFolderInput(!showFolderInput)} - sx={{ border: '1px solid #ccc', borderRadius: 1 }} + - {showFolderInput && ( - - - {parentId ? `Create inside current selection` : `Create at Root`} - - - setNewFolderName(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleCreateFolder()} - /> - - + + + setNewFolderName(e.target.value)} + /> + + + + {/* FILE SELECTION */} + + + {!selectedFile ? ( + + ) : ( + + + + {selectedFile.name} + + setSelectedFile(null)} color="error"> + + + )} - - {/* 2. Upload Area */} - - - 2. Upload File - - - setFile(e.target.files?.[0] || null)} - /> - - - - - {/* 3. Custom Attributes */} - - - - 3. Custom Attributes - - - - - - {customMetadata.map((row, index) => ( - - - updateMetadataRow(index, 'key', e.target.value)} - /> - - - updateMetadataRow(index, 'value', e.target.value)} - /> - - - removeMetadataRow(index)}> - - - - - ))} - - setDescription(e.target.value)} - /> - - - - + + + + {/* MAGIC EXTRACT */} + + + + Magic Extract + Populate metadata automatically from file properties. + + + + + + {/* METADATA PREVIEW */} + + + Metadata Preview + + + {rows.map((row, index) => ( + + + { + const updated = [...rows]; + updated[index].selected = e.target.checked; + setRows(updated); + }} + /> + + + { + const updated = [...rows]; + updated[index].key = e.target.value; + setRows(updated); + }} + /> + + + { + const updated = [...rows]; + updated[index].value = e.target.value; + setRows(updated); + }} + /> + + + setRows(rows.filter((_, i) => i !== index))} color="error"> + + + + + ))} + + + + + {/* UPLOAD BUTTON */} + ); } \ No newline at end of file diff --git a/src/auth.config.ts b/src/auth.config.ts index 9fe15ef..bbc82b2 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -1,3 +1,4 @@ +// src/auth.config.ts import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; import type { NextAuthConfig } from "next-auth"; @@ -9,20 +10,10 @@ export default { issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER, authorization: { params: { - scope: "openid profile offline_access email Files.ReadWrite Files.Read", - prompt: "consent", // Forces Microsoft to show the permission screen - access_type: "offline", + // offline_access is vital for getting the refresh_token + scope: "openid profile email offline_access Files.ReadWrite Files.Read", }, }, - profile(profile) { - return { - id: profile.sub, - name: profile.name, - email: profile.email, - image: null, - azureAdUserId: profile.oid ?? profile.sub, - }; - }, }), ], } satisfies NextAuthConfig; \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index 8d39e9d..61d2097 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -7,22 +7,18 @@ import authConfig from "./auth.config"; export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(prisma), session: { strategy: "jwt" }, - ...authConfig, + ...authConfig, // This now spreads the default export from auth.config.ts callbacks: { async jwt({ token, account, user }) { - // 1. Handle OAuth tokens (from first sign-in) - // This captures the tokens directly from the Microsoft Azure response if (account) { token.accessToken = account.access_token; token.refreshToken = account.refresh_token; token.expiresAt = account.expires_at; } - // 2. Attach User ID and Role to the token - // This runs when the user first logs in if (user) { token.sub = user.id; - // @ts-ignore - 'role' is a custom field in your Postgres User table + // @ts-ignore token.role = user.role; } @@ -30,16 +26,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }, async session({ session, token }) { - // 3. Pass values from the JWT Token into the Client-facing Session - // This makes the tokens and IDs available to your API routes and Components if (session?.user) { session.user.id = token.sub as string; - - // @ts-ignore - Attaching the role for UI permissions + // @ts-ignore session.user.role = token.role as string; - - // IMPORTANT: We must attach the accessToken here so the - // /api/download route can use it to fetch from MS Graph session.accessToken = token.accessToken as string; } return session; @@ -50,7 +40,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ async linkAccount({ account, user }) { console.log("🔗 Account linked successfully for user:", user.id); if (!account.refresh_token) { - console.warn("⚠️ WARNING: No refresh_token received in linkAccount event!"); + console.warn("⚠️ WARNING: No refresh_token received!"); } } } diff --git a/src/data-access/file-nodes.ts b/src/data-access/file-nodes.ts index b9a722d..ce61177 100644 --- a/src/data-access/file-nodes.ts +++ b/src/data-access/file-nodes.ts @@ -1,7 +1,11 @@ // src/data-access/file-nodes.ts import "server-only"; + +import { getOneDriveFileBuffer } from "@/services/onedrive"; +import { extractMetadata } from "@/lib/metadata-extractor"; import { prisma } from "@/lib/prisma"; + /** * FETCH: Retrieve all nodes for the dashboard. * Centralizing this here allows us to change sort order or filters @@ -101,4 +105,27 @@ export async function upsertFileNode(oneDriveId: string, data: any) { metadata: data.metadata, } }); +} + + + +/** + * Logic to coordinate getting a file from the cloud and extracting its data. + * This is the "Brain" function for your metadata enrichment. + */ +export async function getEnrichedMetadataFromCloud(fileId: string) { + // 1. Get the record from our DB so we know the filename (needed for extension logic) + const node = await prisma.fileNode.findUnique({ + where: { id: fileId } + }); + + if (!node) throw new Error("File not found in database."); + + // 2. Fetch the bytes using the service we just created + const buffer = await getOneDriveFileBuffer(fileId); + + // 3. Extract internal metadata (Title, Author, or GPS coordinates) + const deepMetadata = await extractMetadata(buffer, node.name); + + return deepMetadata; } \ No newline at end of file diff --git a/src/lib/auth-utils.ts b/src/lib/auth-utils.ts index 451aa0f..bd6c460 100644 --- a/src/lib/auth-utils.ts +++ b/src/lib/auth-utils.ts @@ -1,3 +1,4 @@ +// src/lib/auth-utils.ts import { prisma } from "@/lib/prisma"; export async function getFreshAccessToken(userId: string) { @@ -11,7 +12,6 @@ export async function getFreshAccessToken(userId: string) { } // 2. Check if the token is expired (with a 1-minute buffer) - // account.expires_at is usually in seconds, so we multiply by 1000 const isExpired = (account.expires_at ?? 0) * 1000 < Date.now() + 60000; if (!isExpired && account.access_token) { @@ -30,12 +30,17 @@ export async function getFreshAccessToken(userId: string) { client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, grant_type: "refresh_token", refresh_token: account.refresh_token, + // CRITICAL: Re-declare scopes to ensure the new access_token has OneDrive permissions + scope: "openid profile offline_access Files.ReadWrite Files.Read", }), }); const tokens = await response.json(); - if (!response.ok) throw tokens; + if (!response.ok) { + console.error("❌ Microsoft Token Refresh Response Error:", tokens); + throw tokens; + } // 4. Update the Account table with the new tokens await prisma.account.update({ @@ -43,6 +48,7 @@ export async function getFreshAccessToken(userId: string) { data: { access_token: tokens.access_token, expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), + // Microsoft sometimes rotates the refresh_token; save it if they provide a new one refresh_token: tokens.refresh_token ?? account.refresh_token, }, }); @@ -50,6 +56,7 @@ export async function getFreshAccessToken(userId: string) { return tokens.access_token; } catch (error) { console.error("❌ Failed to refresh Microsoft token:", error); + // Returning a specific error string helps Auth.js or your components handle re-auth throw new Error("RefreshAccessTokenError"); } } \ No newline at end of file diff --git a/src/lib/metadata-extractor.ts b/src/lib/metadata-extractor.ts new file mode 100644 index 0000000..a4f7cd7 --- /dev/null +++ b/src/lib/metadata-extractor.ts @@ -0,0 +1,92 @@ +import * as pdf from 'pdf-parse'; +import EPub from 'epub'; +import sharp from 'sharp'; +import exifReader from 'exif-reader'; + +export interface ExtractedMetadata { + title?: string; + author?: string; + subject?: string; + dimensions?: string; + pageCount?: number; + latitude?: number; + longitude?: number; + type: string; +} + +export async function extractMetadata(buffer: Buffer, filename: string): Promise { + const extension = filename.split('.').pop()?.toLowerCase(); + + try { + // --- 1. PDF EXTRACTION --- + if (extension === 'pdf') { + // Use any to bypass the missing 'default' property error in ESM + const parsePdf = (pdf as any).default || pdf; + const data = await parsePdf(buffer); + + return { + type: 'PDF', + title: data.info?.Title || filename, + author: data.info?.Author, + subject: data.info?.Subject, + pageCount: data.numpages, + }; + } + + // --- 2. EPUB EXTRACTION --- + if (extension === 'epub') { + // Logic for EPub usually requires file path or custom stream handler + // Keeping placeholder for your existing EPub logic + return { type: 'EPUB', title: filename }; + } + + // --- 3. IMAGE EXTRACTION (Enhanced with GPS) --- + if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) { + const image = sharp(buffer); + const metadata = await image.metadata(); + + let gps: { latitude?: number; longitude?: number } = {}; + + if (metadata.exif) { + try { + // Cast to any to bypass strict Exif type checking for nested GPS properties + const exif = exifReader(metadata.exif) as any; + + // Debugging log to see the raw structure in your terminal + console.log("📸 FULL RAW EXIF DATA:", JSON.stringify(exif, null, 2)); + + if (exif.gps && exif.gps.GPSLatitude && exif.gps.GPSLongitude) { + // EXIF stores GPS as [Degrees, Minutes, Seconds] + // We convert to Decimal Degrees for Google Maps + const lat = exif.gps.GPSLatitude; + const lon = exif.gps.GPSLongitude; + + let latitude = lat[0] + lat[1] / 60 + lat[2] / 3600; + let longitude = lon[0] + lon[1] / 60 + lon[2] / 3600; + + // Adjust for South or West hemisphere + if (exif.gps.GPSLatitudeRef === 'S') latitude *= -1; + if (exif.gps.GPSLongitudeRef === 'W') longitude *= -1; + + gps.latitude = latitude; + gps.longitude = longitude; + } + } catch (exifError) { + console.warn("Could not parse EXIF data for:", filename, exifError); + } + } + + return { + type: `IMAGE (${metadata.format?.toUpperCase()})`, + dimensions: metadata.width && metadata.height ? `${metadata.width}x${metadata.height}` : undefined, + title: filename, + ...gps + }; + } + + return { type: 'FILE', title: filename }; + } catch (error) { + console.error(`Extraction failed for ${filename}:`, error); + return { type: 'FILE', title: filename }; + } +} \ No newline at end of file diff --git a/src/services/onedrive.ts b/src/services/onedrive.ts index f2d5560..994e374 100644 --- a/src/services/onedrive.ts +++ b/src/services/onedrive.ts @@ -1,6 +1,7 @@ // src/services/onedrive.ts import "server-only"; import { getFreshAccessToken } from "@/lib/auth-utils"; +import { auth } from "@/auth"; /** * PRIVATE HELPER: graphRequest @@ -169,4 +170,54 @@ export async function uploadToFolderId(userId: string, file: File, folderId: str if (!uploadRes.ok) throw new Error("Upload failed"); return await uploadRes.json(); +} +/** + * Fetches the raw binary content (the actual file bytes) from OneDrive. + */ +export async function getOneDriveFileBuffer(fileId: string): Promise { + // Use your existing helper that manages the Microsoft Graph access token + const token = await getAccessToken(); + + const response = await fetch( + `https://graph.microsoft.com/v1.0/me/drive/items/${fileId}/content`, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + // Ensure we get fresh data and don't cache large file buffers + cache: 'no-store', + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("OneDrive Download Error:", errorText); + throw new Error(`Failed to download file content: ${response.statusText}`); + } + + // Convert the browser-style response into a Node.js Buffer + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); +} + + +/** + * Retrieves the access token from the active NextAuth session. + * This is required to authorize requests to the Microsoft Graph API. + */ +async function getAccessToken(): Promise { + const session = await auth(); + + // We cast to 'any' because the default Session type often + // needs custom augmentation to show the accessToken. + const token = (session as any)?.accessToken; + + if (!token) { + // This will help you debug if the session is missing the token + console.error("OneDrive Service Error: No access token found in session."); + throw new Error("Authentication required: No access token available."); + } + + return token; } \ No newline at end of file