Search code examples
javascriptreactjsimagenext.jsnext-auth

Show Images (jpg) against authentication with nextauth 4.22.1


Good day everybody,

as mentioned in the title, I'm trying to show images against authentication using nextauth 4.22.1. I'm using the new /app directory, which is placed in the /src directory. Sometimes it's working but most times it doesn't.

Here is my code: \src\app\fotos\page.js:

import Link from "next/link";
import { getFolders } from "./getFolders";

export default function Fotos() {
    const folderNames = getFolders();
    return (
        <main>
            <h2>Bestellung</h2>
            <p>Hier kommen Fotos hin.</p>
            <h3>Veranstaltungen:</h3>
            <ul>
                {folderNames.map((folderName) => (
                    <li key={folderName}>
                        <Link href={`/fotos/${folderName}`}>{folderName}</Link>
                    </li>
                ))}
            </ul>
        </main>
    );
}

\src\app\fotos\getFolders.js:

import fs from "fs";
import path from "path";

export function getFolders() {
    const directoryPath = path.join(process.cwd(), "/src/app/fotos/[course]");
    const fileNames = fs.readdirSync(directoryPath);
    return fileNames.filter((e) => {
        return e !== ".gitignore" && e !== "page.js" && e !== "FotoPage.js";
    });
}

\src\app\fotos\[course]\page.js:

"use client";

import { SessionProvider } from "next-auth/react";
import FotoPage from "./FotoPage";

export default function Show(props) {
    return (
        <SessionProvider>
            <FotoPage props={props} />
        </SessionProvider>
    );
}

\src\app\fotos\[course]\FotoPage.js:

"use client";

import { getSession } from "next-auth/react";
import { redirect } from "next/navigation";
import Link from "next/link";
import UWImage from "@/components/UWImage";

export default async function FotoPage({ props }) {
    const session = await getSession();
    const folderListPromise = await fetch("/api/getfotofoldercontent");
    const folderList = await folderListPromise.json();
    const course = props.params.course;
    if (folderList.folderNames.includes(course)) {
        if (session?.user.name === course) {
            const fileNamesPromise = await fetch(`/api/getfotofoldercontent/${course}`);
            const fileNamesList = await fileNamesPromise.json();
            return (
                <main>
                    <h2>Fotos aus Kurs {course}</h2>
                    <div style={{ display: "flex", flexWrap: "wrap", gap: "10px", justifyContent: "center" }}>
                        {fileNamesList.fileNames.map((fileName) => (
                            <div key={fileName} style={{ textAlign: "center" }}>
                                <strong>{fileName}</strong>
                                <UWImage course={course} fileName={fileName} />
                            </div>
                        ))}
                    </div>
                </main>
            );
        } else { // not logged in as correct user
            redirect(`/api/auth/signin?callbackUrl=/fotos/${course}`);
        }
    } else {
        return (
            <main>
                <h1>
                    Den Kurs &quot;{course}&quot; gibt es nicht.{" "}
                    <Link href="/fotos" style={{ textDecoration: "underline" }}>
                        Hier
                    </Link>{" "}
                    geht es zu den Kursen.
                </h1>
            </main>
        );
    }
}

\src\components\UWImage.js:

"use client";

import Image from "next/image";

export default function UWImage({ course, fileName }) {
    return (
        <div>
            <Image
                src={`/api/getimage?folderName=${course}&fileName=${fileName}`}
                width={400}
                height={400}
                style={{ width: "auto", height: "30vh", borderRadius: "3%" }}
                alt={fileName}
            />
        </div>
    );
}

\src\app\api\getimage\route.js:

import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { NextResponse } from "next/server";
import { readFileSync } from "fs";
import { join } from "path";

export async function GET(request) {
    const url = new URL(request.url);
    const folderName = url.searchParams.get("folderName");
    const session = await getServerSession(authOptions);
    if (session && session.user.name === folderName) {
        const fileName = url.searchParams.get("fileName");
        const imagePath = join(process.cwd(), "src/app/fotos/[course]/", folderName, fileName);
        const image = readFileSync(imagePath);
        return new NextResponse(image, { "Cache-Control": "private" });
    } else {
        return new NextResponse(null, { status: 401 });
    }
}

\src\app\api\getfotofoldercontent\[[...folder]]\route.js:

import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { NextResponse } from "next/server";
import { getFolders } from "@/app/fotos/getFolders";
import { getFileNames } from "@/app/fotos/getFileNames";

export async function GET(params) {
    const folderName = new URL(params.url).pathname.split("/")[3];
    if (!folderName) {
        return NextResponse.json({ folderNames: getFolders()});
    }
    const session = await getServerSession(authOptions);
    if (session && session.user.name === folderName){
        return NextResponse.json({ fileNames: getFileNames(folderName)});
    }
    return new NextResponse(null, { status: 401 });
}

\src\app\fotos\getFileNames.js:

import fs from "fs";
import path from "path";

export function getFileNames(folderName) {
    const directoryPath = path.join(process.cwd(), "/src/app/fotos/[course]", folderName);
    const fileNames = fs.readdirSync(directoryPath);
    return fileNames;
}

\src\app\api\auth\signin\page.js:

import LoginForm from "@/components/LoginForm";

export default function LoginPage() {
    return (
        <main style={{ display: "flex", justifyContent: "center" }}>
            <div className="LoginContainer">
                <br />
                <div className="LoginBackgroundImage">
                    <h1 className="LoginHeading">Login</h1>
                </div>
                <br />
                <LoginForm />
            </div>
        </main>
    );
}

\src\components\LoginForm.js:

"use client";

import { signIn } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";
import { useState } from "react";

export default function LoginForm() {
    const router = useRouter();
    const callbackUrl = useSearchParams().get("callbackUrl") || "/";
    let [loading, setLoading] = useState(false);
    let [course, setCourse] = useState(callbackUrl.split("/").pop());
    let [password, setPassword] = useState("");
    const [error, setError] = useState("");

    async function onSubmit(e) {
        e.preventDefault();
        try {
            setLoading(true);
            const res = await signIn("credentials", {
                redirect: false,
                courseName: course,
                password: password,
                callbackUrl
            });
            setLoading(false);
            if (!res?.error) {
                setError("");
                router.push(callbackUrl);
            } else {
                setError("falsches Passwort");
            }
        } catch (error) {
            setLoading(false);
            setError(error);
        }
    }

    const handleChange = (event) => {
        event.preventDefault();
        const { name, value } = event.target;
        if (name === "course") {
            setCourse(value);
        } else if (name === "password") {
            setPassword(value);
        }
    };

    return (
        <form onSubmit={onSubmit}>
            {error ? <strong className="w3-text-red">{error}</strong> : ""}
            <div className="mb-6">
                <input
                    style={{ margin: 5 }}
                    required
                    type="text"
                    className="input-box"
                    name="course"
                    value={course}
                    onChange={handleChange}
                    placeholder="KURS32XYZ"
                />
            </div>
            <div className="mb-6">
                <input
                    style={{ margin: 5 }}
                    required
                    type="password"
                    className="input-box"
                    name="password"
                    value={password}
                    onChange={handleChange}
                    placeholder="Passwort"
                />
            </div>
            <button style={{ margin: 5, padding: 2 }} type="submit" disabled={loading}>
                {loading ? "lädt..." : "Anmelden"}
            </button>
        </form>
    );
}

\src\app\api\auth\[...nextauth]\route.js:

import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

\src\lib\auth.js:

import CredentialsProvider from "next-auth/providers/credentials";

// TODO: replace with real passwords
const passwordList = {
    "5CR33N5H0T5": "12345",
    W4LLP4P3R5: "54321",
    FR4NC3: "france",
    QW3RTZ: "qwertz",
    V1T450L: "vitasol",
    "3RINN3RUNG": "erinnerung",
    H4LL0W: "halloween",
    MILRI4: "milria"
};

function isCorrectCredentials(credentials) {
    return passwordList[credentials.courseName] === credentials.password;
}

export const authOptions = {
    session: {
        strategy: "jwt"
    },
    providers: [
        CredentialsProvider({
            name: "credentials",
            credentials: {
                courseName: {
                    label: "Kursname",
                    type: "text",
                    placeholder: "KURS32XYZ"
                },
                password: { label: "Passwort", type: "password" }
            },
            async authorize(credentials) {
                return isCorrectCredentials(credentials) ? { id: "1", name: credentials.courseName } : null;
            }
        })
    ],
    pages: {
        signIn: "api/auth/signin"
    }
};

\src\app\layout.js:

import "./globals.css";
import "./w3.css";
import { Inter } from "next/font/google";
import Header from "@/components/Header";
import Footer from "@/components/Footer";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
    title: "AquaFotos - Unterwasser-Bilder",
    description: "Unterwasser-Fotos in bester Qualität"
};

export default function RootLayout({ children }) {
    return (
        <html lang="de">
            <body className={inter.className}>
                <Header />
                {children}
                <Footer />
            </body>
        </html>
    );
}

\package.json:

{
  "name": "aquafotos.com",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "eslint": "8.43.0",
    "eslint-config-next": "^12.0.4",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-react": "^7.25.3",
    "eslint-plugin-react-hooks": "^4.6.0",
    "next": "^13.4.8-canary.2",
    "next-auth": "^4.22.1",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}

I hope, I didn't forget something important. I think, the content of Header and Footer is not relevant for the question, but if I'm wrong, I can share the content as well. The error, I get to see often is, when I'm trying to see the Images of a folder (for example W4LLP4P3R5, I'm trying to access http://localhost:3000/fotos/W4LLP4P3R5), the Browser is trying to get the session over and over again. I can see that in the developer console as the value of the cookie "next-auth.session-token" is changing all the time. In my local console in vs code, I'm getting the message (node:28917) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time over and over again (the number in "(node:28917)" is changing). In the browser console, I'm getting the following error sometimes:

Uncaught (in promise) TypeError: param is undefined
    FotoPage FotoPage.js:8
    describeNativeComponentFrame react-dom.development.js:2534
    describeFunctionComponentFrame react-dom.development.js:2629
    describeFiber react-dom.development.js:2708
    getStackByFiberInDevAndProd react-dom.development.js:2727
    createCapturedValueAtFiber react-dom.development.js:14060
    throwException react-dom.development.js:14550
    throwAndUnwindWorkLoop react-dom.development.js:24676
    renderRootConcurrent react-dom.development.js:24291
    performConcurrentWorkOnRoot react-dom.development.js:23343
    workLoop scheduler.development.js:261
    flushWork scheduler.development.js:230
    performWorkUntilDeadline scheduler.development.js:537
    EventHandlerNonNull* scheduler.development.js:575
    <anonymous> scheduler.development.js:638
    NextJS 4
    <anonymous> index.js:6
    NextJS 4
    <anonymous> react-dom.development.js:27
    <anonymous> react-dom.development.js:36657
    NextJS 4
    <anonymous> index.js:37
    NextJS 4
    <anonymous> client.js:3
    NextJS 4
    <anonymous> app-index.js:14
    NextJS 4
    <anonymous> app-next-dev.js:8
    appBootstrap app-bootstrap.js:58
    loadScriptsInSequence app-bootstrap.js:23
    appBootstrap app-bootstrap.js:57
    <anonymous> app-next-dev.js:7
    NextJS 7
FotoPage.js:8:39

But in some cases it's just working fine. The error above suggests that when calling http://localhost:3000/fotos/W4LLP4P3R5 (for example) the parameter is undefined, but I don't know how that can be. Expected behaviour: Try to get the session. If I'm logged in as the user with the correct name (name of the folder, in this case "W4LLP4P3R5"), show the images, the folder contains. If I'm not logged in, or logged in as a wrong user, redirect to login-page. After login, redirect back to the callbackUrl. You can try this with every Images-Folder. Just place the folder in \src\app\fotos\[course] and place a password-entry in \src\lib\auth.js. Maybe I'm thinking too complicated and what I want to achieve is much easier to do. if anyone has an idea, i'm grateful for it.


Solution

  • Just in case anyone else faces the same problem: I got it to work reliably with the following changes:

    In \src\app\fotos\[course]\FotoPage.js:

    • replace the redirect-method from next/navigation with the useRouter().push-method
    • delete the <main>-tags which surrounded the JSX
      "use client";
      
      import { getSession } from "next-auth/react";
      import { useRouter } from "next/navigation";
      import Link from "next/link";
      import UWImage from "@/components/UWImage";
      
      export default async function FotoPage({ props }) {
          const router = useRouter();
          const session = await getSession();
          const folderListPromise = await fetch("/api/getfotofoldercontent");
          const folderList = await folderListPromise.json();
          const course = props.params.course;
          if (folderList.folderNames.includes(course)) {
              if (session?.user.name === course) {
                  const fileNamesPromise = await fetch(`/api/getfotofoldercontent/${course}`);
                  const fileNamesList = await fileNamesPromise.json();
                  return (
                      <>
                          <h2>Fotos aus Kurs {course}</h2>
                          <div style={{ display: "flex", flexWrap: "wrap", gap: "10px", justifyContent: "center" }}>
                              {fileNamesList.fileNames.map((fileName) => (
                                  <div key={fileName} style={{ textAlign: "center" }}>
                                      <strong>{fileName}</strong>
                                      <UWImage course={course} fileName={fileName} />
                                  </div>
                              ))}
                          </div>
                      </>
                  );
              } else {
                  // not logged in as correct user
                  router.push(`/api/auth/signin?callbackUrl=/fotos/${course}`);
              }
          } else {
              return (
                  <h1>
                      Den Kurs &quot;{course}&quot; gibt es nicht.{" "}
                      <Link href="/fotos" style={{ textDecoration: "underline" }}>
                          Hier
                      </Link>{" "}
                      geht es zu den Kursen.
                  </h1>
              );
          }
      }

    In \src\app\fotos\[course]\page.js:

    • surround <Fotopage>-tag with <main>-tag
      "use client";
      
      import { SessionProvider } from "next-auth/react";
      import FotoPage from "./FotoPage";
      
      export default function Show(props) {
          return (
              <SessionProvider>
                  <main>
                      <FotoPage props={props} />
                  </main>
              </SessionProvider>
          );
      }

    In \src\app\fotos\page.js:

    • add prefetch={false} to the <Link>-tag
      import Link from "next/link";
      import { getFolders } from "./getFolders";
      
      export default function Fotos() {
          const folderNames = getFolders();
          return (
              <main>
                  <h2>Bestellung</h2>
                  <p>Hier kommen Fotos hin.</p>
                  <h3>Veranstaltungen:</h3>
                  <ul>
                      {folderNames.map((folderName) => (
                          <li key={folderName}>
                              <Link href={`/fotos/${folderName}`} prefetch={false}>{folderName}</Link>
                          </li>
                      ))}
                  </ul>
              </main>
          );
      }