Next.js Authentication Made Easy with Microsoft Entra ID
Next.js 16 Middleware DEPRECATED - Authentication In Proxy Or Data Access Layer?
Overview of user consent and how to manage it in Microsoft Entra | Microsoft
Add Tailwind CSS to an Existing Next js Project
How to style Material UI component with Tailwindcss in React project
How to Use Prisma 7 in Next.js
The following is the desire structure
web-calibre/
βββ src/
β βββ app/ # All Routes (Pages & APIs)
β β βββ api/ # API ROUTES
β β β βββ auth/ # NextAuth routes [...nextauth]
β β β βββ upload/ # Custom API for OneDrive sessions
β β βββ dashboard/ # Protected user area
β β βββ layout.tsx # Global Layout
β β βββ page.tsx # Landing Page
β βββ components/ # MUI Components (FileTable, Navbar)
β βββ lib/ # Config (Prisma, Theme, Auth)
β β βββ prisma.ts
β β βββ theme.ts <-- MUI Theme Defined Here
β βββ middleware.ts <-- Protected Routes Logic
βββ prisma/ # DB Schema
βββ .env # Secrets
WebCalibre2 Application (client) ID = 'b549931a-f491-436b-b7bc-d37d8ca3c17e'
Object ID ='0b8256a2-e2ac-472b-896a-4b5845bc32fe'
Directory (tenant) ID = '1c06ce7c-7884-4796-8652-d4c32d75a5d0'
Expires = 1/3/2028
Value='O3p8Q~oMph-0kSLwkvzEJzJdx_iHGOjJjrr5Pa1A'
Secret ID='6a242be1-c711-4cc3-a132-03c1f57993ed'

Since you are using NextAuth.js, the URL must follow a very specific pattern.
For local development: http://localhost:3000/api/auth/callback/azure-ad
For production: https://your-domain.com/api/auth/callback/azure-ad
Note: Replace azure-ad with whatever ID you give the provider in your code. By default, in NextAuth, it is usually azure-ad.
WebCalibre3
| Display name | WebCalibre3 |
|---|---|
| Application (client) ID | a95ef644-cb9d-465c-8dae-f822a78a8ac3 |
| Object ID | 1e1f2bda-661c-4ad0-9aae-82313c603076 |
| Directory (tenant) ID. | 1c06ce7c-7884-4796-8652-d4c32d75a5d0 |
Client credentials
| Description | Expires | Value | Secret ID |
|---|---|---|---|
| WebCalibre3 | 1/5/2028 | VZ48Q~EOsgZcgBI25cc0zys.h9zM7hN9I7DhZdnW | d647cf28-80ed-45fb-a89e-e2bc323a40e9 |

The following command creates the nextjs folder structure
Yes, Next.js has a specific syntax to create a project in your current directory instead of creating a new subfolder.
To do this, navigate to your empty project folder in the terminal and run:
npx create-next-app@latest .
Important Details:
The Dot (.): This tells the installer to use the current directory as the project root.
Naming Conflict: If the folder name contains capital letters (e.g., WebCalibre), the command will fail with an error. Next.js requires the root folder name to be all lowercase and URL-friendly. You must rename your folder to web-calibre before running the command. This caused me rename to all lower case.
3.Empty Folder: The directory must be empty (except for hidden files like .git). If you already have files in there, the installer will stop to prevent overwriting your work.
What happens next? Even though you are using the current directory, the interactive prompt will still ask you:
"What is your project named?" You can just hit Enter to accept the current folder name.
Configuration: It will then ask you about TypeScript, ESLint, Tailwind CSS, the src/ directory, and the App Router.
Based on our previous discussions, I recommend selecting "Yes" for TypeScript, ESLint, Tailwind, the src/ directory, and the App Router to match the professional structure we planned for WebCalibre.
In Next.js, the distinction between .env and .env.local is primarily about security and overriding defaults. While both store key-value pairs, they are handled differently by Git and the Next.js runtime.
The Next.js Loading Order Next.js looks for variables in this specific order (from highest priority to lowest):
DATABASE_URL="postgresql://stephen:Web2025$$@192.168.1.210:5432/webcalibre2"
# Credentials for your Microsoft App (NextAuth)
AZURE_AD_CLIENT_ID="549931a-f491-436b-b7bc-d37d8ca3c17e"
AZURE_AD_CLIENT_SECRET="6a242be1-c711-4cc3-a132-03c1f57993ed"
AZURE_AD_TENANT_ID="1c06ce7c-7884-4796-8652-d4c32d75a5d0"
Summary Comparison Table
| Feature | .env | .env.local |
|---|---|---|
| Purpose | Shared defaults/constants | Private secrets & local overrides |
| Committed to Git? | Yes | No (Added to .gitignore) |
| Sensitive Data? | NEVER | YES (API Keys, Secrets) |
| Scope | All developers/environments | Just your machine |
**Best Practice Tip: ** Since .env.local isn't shared on GitHub, itβs a good idea to create a file named .env.example in your project. This file should contain the names of the keys (e.g., AZURE_AD_CLIENT_SECRET=) but leave the values blank, so other developers know which variables they need to create on their own machines.
Nextjs initializes git by default. But is doesn't add some configs that I do
git config --global push.followTags true
git remote add origin "/Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git"
git remote -v origin /Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git (fetch) origin /Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git (push)
npm run dev
> 124_webcalibre2@0.1.0 dev
> next dev
β² Next.js 16.1.1 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.100:3000
- Environments: .env
β Starting...
β Ready in 648ms
If it working proceed
I ran through the Video:-
Next.js Authentication Made Easy with Microsoft Entra ID
npm install next-auth@beta
Setup Environment The only environment variable that is mandatory is the AUTH_SECRET. This is a random value used by the library to encrypt tokens and email verification hashes. (See Deployment to learn more). You can generate one via the official Auth.js CLI running:
npx auth secret
created .env.local
AUTH_SECRET="7cz3Z4kUI2kB3mdAPo58iioUDLSRJ92X8+boEixvO8k=" # Added by `npx auth`. Read more: https://cli.authjs.dev
# values generated by Gemini
# Generated for security
AUTH_SECRET="7cz3Z4kUI2kB3mdAPo58iioUDLSRJ92X8+boEixvO8k="
# Use 'common' for multi-tenant (Work + Personal) support
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID="common"
# The Application (client) ID
AUTH_MICROSOFT_ENTRA_ID_ID="b549931a-f491-436b-b7bc-d37d8ca3c17e"
# The Client Secret VALUE (ensure no leading '6' or spaces)
AUTH_MICROSOFT_ENTRA_ID_SECRET="O3p8Q~oMph-0kSLwkvzEJzJdx_iHGOjJjrr5Pa1A"
# Required to tell Auth.js to trust your localhost/proxy URL
AUTH_TRUST_HOST=true
DATABASE_URL="postgresql://stephen:Web2025$$@192.168.1.210:5432/webcalibre2"
# Credentials for your Microsoft App (NextAuth)
AZURE_AD_CLIENT_ID="549931a-f491-436b-b7bc-d37d8ca3c17e"
AZURE_AD_CLIENT_SECRET="6a242be1-c711-4cc3-a132-03c1f57993ed"
AZURE_AD_TENANT_ID="1c06ce7c-7884-4796-8652-d4c32d75a5d0"
To get Prisma fully installed and ready to talk to your PostgreSQL database, you need to install the CLI (for migrations) and the Client (for your code to query the database).
Run these three commands in your terminal:
npm install prisma --save-dev
##The Client is used by your Next.js code to talk to the DB
npm install @prisma/client
npm install dotenv-cli --save-dev
Bash
npx prisma init
npx dotenv -e .env.local -- npx prisma generate 4. Apply your Schema to PostgreSQL Finally, run this to actually create the tables in your webcalibre2 database:
npx dotenv -e .env.local -- npx prisma migrate dev --name init_database
To avoid typing that long dotenv command every time, open your package.json and add this to the "scripts" section:
JSON
"scripts": { "db:migrate": "dotenv -e .env.local -- prisma migrate dev", "db:studio": "dotenv -e .env.local -- prisma studio" } Now, you can just run npm run db:migrate whenever you update your schema!
We can authenticate with MS Azure, upload a file and store it on my personal OneDrive. I can see a few problems,
if the file gets stored with its original filename, if I store the same file again it tells that it fail, but in reality it did not fail the it got renamed with 1.pdf added this is not added data base.
We may be better to create a folder and put the file in the folder, to me the logical name would id of the file, but that is automatically create and assigned when the data is stored as a UUID.
We would have to create the UUID ourselves, so that we can create a folder with that specific UUID and store the file in the folder.
The Upload file Page needs to be able to create folder as well select a parent , add a description of either the file
The functionality works may not the best user interface
Projects ID 0371fdfe-a2d1-4f25-b229-804f299bc064 Project-1 ID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1 parentID 0371fdfe-a2d1-4f25-b229-804f299bc064 AS-NZS3000-2018.pdf parentID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1
Now we will go now and add metadata
Now we have the metadata along with some great filters on the data board page
Now we are going to implement an an update of a file or folder stored. 12/1/2026
For download there 2 function we need to implement
down load the file to ~/user/Downloads, this should work for any file type including pdf
if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf
For download there 2 function we need to implement
down load the file to ~/user/Downloads, this should work for any file type including pdf
if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf
So we need to implement 2 new actions, downLoad file and open pdf to read. Does this make sense ?
A Next.js (App Router) dashboard for managing a OneDrive-synced file library. Uses Prisma + PostgreSQL for metadata storage and MUI X v8 (DataGrid) for the frontend.
slots.showToolbar prop on the <DataGrid />.
expanded prop on the <QuickFilter /> component within the custom toolbar to ensure it doesn't collapse.slotProps.textField (not input) to customize the search input in MUI v7/v8.metadata_search) with width: 0.valueGetter to return JSON.stringify(row.metadata).initialState.columns.columnVisibilityModel.sharp and pdf-parse are Node.js-based and will crash if imported into client components.| Command | Purpose |
|---|---|
npx tsc --noEmit |
Run this frequently. Validates types across the entire project. Finds errors your IDE might miss. |
npx prisma generate |
Updates the Prisma Client types after schema changes. |
npx prisma db push |
Pushes schema changes to the PostgreSQL database. |
npm run dev |
Starts the development server. |
src/lib/metadata-extractor.ts.syncOneDrive Server Action.pdf-parse), EPUB (node-epub-utils), Images (sharp).Cmd + Shift + R or Ctrl + F5.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
export async function getMetadataPreviewAction(fileId: string) {
const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized");
console.log(`\n--- π Magic Fill Started for File ID: ${fileId} ---`);
try {
const node = await getFileNodeById(fileId);
if (!node || !node.oneDriveId) throw new Error("File not found");
// 1. Get the token from our utility
const token = await getFreshAccessToken(session.user.id);
// LOG: Just check the length to be sure it's not empty
console.log(`π Token retrieved (Length: ${token.length})`);
console.log(`π‘ Fetching binary for: ${node.name}...`);
// 2. Fetch the content from Microsoft Graph
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Accept': '*/*'
},
}
);
if (!response.ok) {
// If it fails here, we'll see the real reason from Microsoft
const errorText = await response.text();
console.error("β Microsoft Graph Error Response:", errorText);
throw new Error(`OneDrive Download Failed: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
console.log(`π¦ Success! Downloaded ${buffer.length} bytes.`);
// 3. Extract Metadata
const extractedData = await extractMetadata(buffer, node.name);
console.log("β
RAW DATA EXTRACTED:");
console.dir(extractedData, { depth: null, colors: true });
return { success: true, data: extractedData };
} catch (error: any) {
console.error("β Magic Fill Error:", error.message);
return { success: false, error: error.message };
}
}
npm run dev
> 124_webcalibre2@0.1.0 dev
> next dev
β² Next.js 16.1.1 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.100:3000
- Environments: .env.local, .env
- Experiments (use with caution):
Β· serverActions
β Starting...
β Ready in 380ms
β
Prisma 7 connected to PostgreSQL successfully
GET /dashboard 200 in 643ms (compile: 139ms, proxy.ts: 115ms, render: 388ms)
β
Prisma 7 connected to PostgreSQL successfully
GET /update/d3ccd68a-0493-487d-9273-5fa8ff6a6f9a 200 in 329ms (compile: 274ms, proxy.ts: 19ms, render: 37ms)
--- π Magic Fill Started for File ID: d3ccd68a-0493-487d-9273-5fa8ff6a6f9a ---
π Access token expired. Refreshing for user: cc6f11ff-549d-40fa-883f-7886ee6487cf
β
Prisma 7 connected to PostgreSQL successfully
π Token retrieved (Length: 1484)
π‘ Fetching binary for: IMG_2360.jpeg...
π¦ Success! Downloaded 1233596 bytes.
πΈ FULL RAW EXIF DATA: {
"bigEndian": true,
"Image": {
"Make": "Apple",
"Model": "iPhone 14",
"Orientation": 1,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": "18.6.2",
"DateTime": "2025-10-03T08:02:39.000Z",
"HostComputer": "iPhone 14",
"YCbCrPositioning": 1,
"ExifTag": 228,
"GPSTag": 2586
},
"Thumbnail": {
"Compression": 6,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"JPEGInterchangeFormat": 2990,
"JPEGInterchangeFormatLength": 3322
},
"Photo": {
"ExposureTime": 0.025,
"FNumber": 1.5,
"ExposureProgram": 2,
"ISOSpeedRatings": 500,
"ExifVersion": {
"type": "Buffer",
"data": [
48,
50,
51,
50
]
},
"DateTimeOriginal": "2025-10-03T08:02:39.000Z",
"DateTimeDigitized": "2025-10-03T08:02:39.000Z",
"OffsetTime": "+02:00",
"OffsetTimeOriginal": "+02:00",
"OffsetTimeDigitized": "+02:00",
"ComponentsConfiguration": {
"type": "Buffer",
"data": [
1,
2,
3,
0
]
},
"ShutterSpeedValue": 5.327015336217066,
"ApertureValue": 1.1699250021066825,
"BrightnessValue": -0.4198113650227582,
"ExposureBiasValue": 0,
"MeteringMode": 5,
"Flash": 16,
"FocalLength": 5.7,
"SubjectArea": [
2006,
1506,
2213,
1327
],
"MakerNote": {
"type": "Buffer",
"data": [
65,
112,
112,
108,
101,
32,
105,
79,
83,
0,
0,
1,
77,
77,
0,
49,
0,
1,
0,
9,
0,
0,
0,
1,
0,
0,
0,
15,
0,
2,
0,
7,
0,
0,
2,
0,
0,
0,
2,
96,
0,
3,
0,
7,
0,
0,
0,
104,
0,
0,
4,
96,
0,
4,
0,
9,
0,
0,
0,
1,
0,
0,
0,
1,
0,
5,
0,
9,
0,
0,
0,
1,
0,
0,
0,
149,
0,
6,
0,
9,
0,
0,
0,
1,
0,
0,
0,
152,
0,
7,
0,
9,
0,
0,
0,
1,
0,
0,
0,
1,
0,
8,
0,
10,
0,
0,
0,
3,
0,
0,
4,
200,
0,
12,
0,
10,
0,
0,
0,
2,
0,
0,
4,
224,
0,
13,
0,
9,
0,
0,
0,
1,
0,
0,
0,
39,
0,
14,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
16,
0,
9,
0,
0,
0,
1,
0,
0,
0,
1,
0,
20,
0,
9,
0,
0,
0,
1,
0,
0,
0,
12,
0,
22,
0,
7,
0,
0,
0,
72,
0,
0,
4,
240,
0,
23,
0,
16,
0,
0,
0,
1,
0,
0,
5,
56,
0,
25,
0,
9,
0,
0,
0,
1,
0,
0,
32,
2,
0,
26,
0,
2,
0,
0,
0,
6,
0,
0,
5,
64,
0,
31,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
32,
0,
2,
0,
0,
0,
37,
0,
0,
5,
70,
0,
33,
0,
10,
0,
0,
0,
1,
0,
0,
5,
107,
0,
35,
0,
9,
0,
0,
0,
2,
0,
0,
5,
115,
0,
37,
0,
16,
0,
0,
0,
1,
0,
0,
5,
123,
0,
38,
0,
9,
0,
0,
0,
1,
0,
0,
0,
3,
0,
39,
0,
10,
0,
0,
0,
1,
0,
0,
5,
131,
0,
43,
0,
2,
0,
0,
0,
37,
0,
0,
5,
139,
0,
45,
0,
9,
0,
0,
0,
1,
0,
0,
30,
150,
0,
46,
0,
9,
0,
0,
0,
1,
0,
0,
0,
1,
0,
47,
0,
9,
0,
0,
0,
1,
0,
0,
0,
48,
0,
48,
0,
10,
0,
0,
0,
1,
0,
0,
5,
176,
0,
54,
0,
9,
0,
0,
0,
1,
0,
0,
0,
42,
0,
55,
0,
9,
0,
0,
0,
1,
0,
0,
0,
4,
0,
58,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
59,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
60,
0,
9,
0,
0,
0,
1,
0,
0,
0,
4,
0,
63,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
64,
0,
7,
0,
0,
0,
74,
0,
0,
5,
184,
0,
65,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
67,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
68,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
69,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
70,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
74,
0,
9,
0,
0,
0,
1,
0,
0,
0,
2,
0,
77,
0,
10,
0,
0,
0,
1,
0,
0,
6,
2,
0,
78,
0,
7,
0,
0,
0,
121,
0,
0,
6,
10,
0,
79,
0,
7,
0,
0,
0,
43,
0,
0,
6,
131,
0,
82,
0,
9,
0,
0,
0,
1,
0,
0,
0,
11,
0,
83,
0,
9,
0,
0,
0,
1,
0,
0,
0,
1,
0,
85,
0,
9,
0,
0,
0,
1,
0,
0,
0,
0,
0,
88,
0,
9,
0,
0,
0,
1,
0,
0,
7,
3,
0,
0,
0,
0,
239,
1,
205,
1,
169,
1,
135,
1,
103,
1,
73,
1,
45,
1,
19,
1,
251,
0,
228,
0,
207,
0,
188,
0,
172,
0,
158,
0,
146,
0,
136,
0,
54,
2,
17,
2,
232,
1,
193,
1,
155,
1,
118,
1,
85,
1,
55,
1,
26,
1,
0,
1,
231,
0,
209,
0,
189,
0,
173,
0,
160,
0,
147,
0,
125,
2,
89,
2,
47,
2,
2,
2,
216,
1,
175,
1,
137,
1,
99,
1,
64,
1,
33,
1,
4,
1,
233,
0,
210,
0,
190,
0,
173,
0,
159,
0,
8,
3,
217,
2,
170,
2,
120,
2,
65,
2,
13,
2,
218,
1,
171,
1,
125,
1,
83,
1,
44,
1,
10,
1,
235,
0,
210,
0,
188,
0,
170,
0,
252,
2,
242,
2,
210,
2,
170,
2,
131,
2,
81,
2,
30,
2,
230,
1,
175,
1,
124,
1,
77,
1,
36,
1,
255,
0,
225,
0,
199,
0,
178,
0,
69,
1,
93,
1,
121,
1,
168,
1,
208,
1,
206,
1,
193,
1,
168,
1,
135,
1,
107,
1,
69,
1,
32,
1,
251,
0,
220,
0,
196,
0,
176,
0,
183,
0,
185,
0,
182,
0,
183,
0,
181,
0,
182,
0,
175,
0,
179,
0,
192,
0,
183,
0,
178,
0,
169,
0,
158,
0,
143,
0,
139,
0,
131,
0,
173,
0,
179,
0,
173,
0,
185,
0,
187,
0,
231,
0,
145,
1,
164,
1,
147,
1,
107,
1,
38,
1,
157,
0,
139,
0,
166,
0,
169,
0,
152,
0,
36,
0,
55,
0,
64,
0,
58,
0,
57,
0,
55,
0,
56,
0,
50,
0,
45,
0,
42,
0,
37,
0,
27,
0,
25,
0,
23,
0,
30,
0,
43,
0,
10,
0,
11,
0,
12,
0,
14,
0,
19,
0,
23,
0,
25,
0,
24,
0,
23,
0,
21,
0,
20,
0,
18,
0,
17,
0,
16,
0,
16,
0,
16,
0,
6,
0,
7,
0,
7,
0,
9,
0,
11,
0,
9,
0,
12,
0,
12,
0,
15,
0,
13,
0,
13,
0,
12,
0,
11,
0,
10,
0,
9,
0,
10,
0,
4,
0,
5,
0,
6,
0,
6,
0,
7,
0,
9,
0,
9,
0,
9,
0,
9,
0,
9,
0,
10,
0,
9,
0,
9,
0,
6,
0,
7,
0,
7,
0,
4,
0,
4,
0,
5,
0,
4,
0,
5,
0,
6,
0,
6,
0,
6,
0,
7,
0,
8,
0,
8,
0,
8,
0,
6,
0,
5,
0,
5,
0,
5,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
6,
0,
6,
0,
4,
0,
5,
0,
4,
0,
4,
0,
5,
0,
4,
0,
4,
0,
5,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
4,
0,
5,
0,
5,
0,
4,
0,
5,
0,
3,
0,
5,
0,
6,
0,
7,
0,
6,
0,
5,
0,
4,
0,
5,
0,
5,
0,
4,
0,
4,
0,
4,
0,
5,
0,
3,
0,
3,
0,
4,
0,
4,
0,
5,
0,
98,
112,
108,
105,
115,
116,
48,
48,
212,
1,
2,
3,
4,
5,
6,
7,
8,
85,
102,
108,
97,
103,
115,
85,
118,
97,
108,
117,
101,
89,
116,
105,
109,
101,
115,
99,
97,
108,
101,
85,
101,
112,
111,
99,
104,
16,
1,
19,
0,
1,
62,
142,
239,
162,
85,
109,
18,
59,
154,
202,
0,
16,
0,
8,
17,
23,
29,
39,
45,
47,
56,
61,
0,
0,
0,
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
9,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
63,
255,
255,
84,
169,
0,
0,
168,
221,
0,
0,
54,
115,
0,
9,
142,
158,
255,
255,
244,
153,
0,
2,
69,
230,
0,
0,
0,
91,
0,
0,
0,
128,
0,
0,
0,
81,
0,
0,
1,
0,
98,
112,
108,
105,
115,
116,
48,
48,
95,
16,
28,
65,
88,
118,
67,
67,
48,
55,
76,
49,
117,
79,
73,
113,
48,
73,
98,
101,
51,
76,
121,
112,
112,
98,
98,
86,
43,
108,
113,
8,
0,
0,
0,
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
39,
0,
0,
0,
0,
0,
80,
32,
4,
113,
55,
53,
48,
110,
0,
55,
67,
65,
54,
53,
50,
48,
68,
45,
67,
50,
49,
55,
45,
52,
56,
68,
49,
45,
56,
48,
49,
67,
45,
50,
69,
49,
57,
54,
69,
66,
66,
68,
52,
65,
50,
0,
0,
0,
249,
65,
0,
1,
39,
154,
0,
0,
1,
55,
16,
0,
0,
27,
0,
0,
0,
0,
0,
0,
20,
142,
0,
2,
96,
195,
0,
0,
19,
224,
51,
67,
53,
51,
52,
69,
66,
69,
45,
52,
65,
53,
51,
45,
52,
55,
69,
70,
45,
57,
49,
55,
65,
45,
66,
55,
56,
53,
57,
55,
51,
55,
50,
53,
57,
52,
0,
0,
0,
0,
0,
0,
0,
0,
1,
98,
112,
108,
105,
115,
116,
48,
48,
212,
1,
2,
3,
4,
5,
6,
6,
7,
81,
51,
81,
49,
81,
50,
81,
48,
16,
0,
34,
0,
0,
0,
0,
16,
1,
8,
17,
19,
21,
23,
25,
27,
32,
0,
0,
0,
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
8,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
34,
0,
2,
38,
42,
0,
0,
19,
59,
98,
112,
108,
105,
115,
116,
48,
48,
210,
1,
2,
3,
4,
81,
49,
81,
50,
16,
3,
162,
5,
10,
210,
6,
7,
8,
9,
83,
50,
46,
49,
83,
50,
46,
50,
35,
64,
157,
225,
32,
96,
0,
0,
0,
35,
64,
124,
160,
0,
0,
0,
0,
0,
210,
6,
7,
11,
12,
35,
0,
0,
0,
0,
0,
0,
0,
0,
35,
64,
72,
0,
0,
0,
0,
0,
0,
8,
13,
15,
17,
19,
22,
27,
31,
35,
44,
53,
58,
67,
0,
0,
0,
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
13,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
76,
98,
112,
108,
105,
115,
116,
48,
48,
16,
0,
8,
0,
0,
0,
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
10
]
},
"SubSecTimeOriginal": "714",
"SubSecTimeDigitized": "714",
"FlashpixVersion": {
"type": "Buffer",
"data": [
48,
49,
48,
48
]
},
"ColorSpace": 65535,
"PixelXDimension": 4032,
"PixelYDimension": 3024,
"SensingMethod": 2,
"SceneType": {
"type": "Buffer",
"data": [
1
]
},
"ExposureMode": 0,
"WhiteBalance": 0,
"FocalLengthIn35mmFilm": 26,
"SceneCaptureType": 0,
"LensSpecification": [
1.5399999618512084,
5.699999809263318,
1.5,
2.4
],
"LensMake": "Apple",
"LensModel": "iPhone 14 back dual wide camera 5.7mm f/1.5",
"CompositeImage": 2
},
"GPSInfo": {
"GPSLatitudeRef": "N",
"GPSLatitude": [
42,
42,
22.91
],
"GPSLongitudeRef": "W",
"GPSLongitude": [
7,
1,
50.76
],
"GPSAltitudeRef": 0,
"GPSAltitude": 1265.3340248962656,
"GPSTimeStamp": [
6,
2,
39
],
"GPSSpeedRef": "K",
"GPSSpeed": 0,
"GPSImgDirectionRef": "T",
"GPSImgDirection": 147.2551498127341,
"GPSDestBearingRef": "T",
"GPSDestBearing": 147.2551498127341,
"GPSDateStamp": "2025:10:03",
"GPSHPositioningError": 4.748651528267963
}
}
β
RAW DATA EXTRACTED:
{
type: 'IMAGE (JPEG)',
dimensions: '4032x3024',
title: 'IMG_2360.jpeg'
}
POST /update/d3ccd68a-0493-487d-9273-5fa8ff6a6f9a 200 in 4.0s (compile: 9ms, proxy.ts: 19ms, render: 4.0s)
Two questions
https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content,
{
headers: {
'Authorization': Bearer ${token},
'Accept': '/'
},
}
);
Should this be in the services/onedrive.ts to be compliant with the Data Access Layer (DAL) that nextjs recomments?Those are excellent architectural questions. You're moving from "making it work" to "making it scale," which is exactly where you want to be.
Yes, it absolutely belongs in services/onedrive.ts.
Next.js (and general clean architecture) recommends a Data Access Layer (DAL) to decouple your business logic from the specific implementation of your data source.
Why this is better:
services/onedrive.ts) instead of hunting down fetch calls in your API routes.Recommended structure for services/onedrive.ts:
export async function getOneDriveFileBuffer(oneDriveId: string, token: string): Promise<Buffer> {
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${oneDriveId}/content`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Accept': '*/*'
},
}
);
if (!response.ok) {
throw new Error(`OneDrive download failed: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
getMetadataFromJson?lib/metadata-extractor.ts is the perfect spot.
In the Next.js ecosystem, the lib/ directory is the standard convention for "Internal Libraries" or "Utilities"βcode that is purely functional, doesn't depend on React, and performs a specific technical task (like parsing a buffer).
Why this is the right move:
Structure for lib/metadata-extractor.ts:
import exifReader from 'exif-reader';
// Define an interface for your metadata to keep your Json field typed
export interface FileMetadata {
deviceModel?: string;
capturedAt?: Date;
lat?: number;
lng?: number;
[key: string]: any; // Allows for other fields
}
export function extractExifMetadata(buffer: Buffer): FileMetadata {
// logic goes here...
}
When you put these in their proper places, your API Route or Server Action becomes very readable "glue" code:
services/onedrive.ts)lib/metadata-extractor.ts)Would you like me to help you refine the internal logic of the extractExifMetadata function to ensure it handles the specific "Rational" numbers (fractions) that EXIF often uses for exposure and GPS?