LogoGalsenext
Guide et Tutoriel

Authentification

---

Authentication and Onboarding

Authentication in your project uses NextAuth.js, a comprehensive identity and session management framework. Here are the different aspects:

NextAuth Configuration

The file pages/api/auth/[...nextauth].ts configures the authentication options for NextAuth:

import { prisma } from "@/src/lib/prisma";
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth, { AuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
 
const FixedPrismaAdapter = PrismaAdapter(prisma) as any;
 
export const authOptions: AuthOptions = {
  session: {
    strategy: "jwt",
  },
  adapter: FixedPrismaAdapter,
  theme: {
    logo: "/galsenext.png",
  },
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      profile(profile) {
        return {
          id: profile.sub,
          name: `${profile.given_name} ${profile.family_name}`,
          email: profile.email,
          image: profile.picture,
          role: profile.role ? profile.role : "user",
          emailVerified: null,
          createdAt: new Date(),
          updatedAt: new Date(),
          username: profile.username ? profile.username : "",
          bio: profile.bio ? profile.bio : "",
          link: profile.link ? profile.link : "",
          isOnboarded: false,
        };
      },
    }),
  ],
 
  callbacks: {
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
        session.user.isOnboarded = token.isOnboarded as boolean;
      }
      return session;
    },
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
        token.isOnboarded = user.isOnboarded;
      }
 
      if (!account) {
        const dbUser = await prisma.user.findUnique({
          where: { id: token.id as string },
          select: { isOnboarded: true },
        });
        if (dbUser) {
          token.isOnboarded = dbUser.isOnboarded;
        }
      }
 
      return token;
    },
  },
};
 
export default NextAuth(authOptions);

Session Management with JWT

  1. JWT (JSON Web Tokens): Session management is handled using JWT, which securely stores session information in the token.
  2. Callbacks:
    • session callback: When retrieving a session, additional user information (id, role, isOnboarded) is added to session.user.
    • jwt callback: When creating the JWT token, user information such as id, role, and isOnboarded is added.
  3. Role System:
    • Roles (user, admin, etc.) are assigned to users and managed via the JWT token. This system controls access and privileges on the platform.

Onboarding API

The file app/api/user/onboard/route.ts contains the onboarding API, which is called to update user information after the first login.

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { prisma } from "@/src/lib/prisma";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
 
export async function POST(request: NextRequest) {
  const session = await getServerSession(authOptions);
 
  if (!session) {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }
 
  const { username, bio } = await request.json();
 
  try {
    const updatedUser = await prisma.user.update({
      where: { id: session.user.id },
      data: {
        username,
        bio,
        isOnboarded: true,
      },
    });
 
    return NextResponse.json({
      message: "Onboarding réussi",
      user: updatedUser,
    });
  } catch (error) {
    return NextResponse.json(
      { error: "Erreur lors de l'onboarding" },
      { status: 500 }
    );
  }
}
  • User Authentication: Checks if the user is authenticated by retrieving the server session using getServerSession.
  • Updating User Data: Updates user information in the database to mark onboarding as completed (isOnboarded: true).

Onboarding Component: component/onboarding/Onboarding.tsx

The Onboarding.tsx component guides the user through the onboarding process after their first login. Here is the component code reorganized into sections for clarity:

Type Definitions and Imports

"use client";
 
import React, { useState } from "react";
import { useSession } from "next-auth/react";
import { Input } from "@/components/ui/input";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { CheckCircle, Circle, Loader2, XCircle } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "../ui/button";
 
interface ProgressBarProps {
  currentStep: number;
  totalSteps: number;
}
 
interface OnboardingStepProps {
  image: string;
  description: string;
  onNext: () => void;
  stepNumber: number;
  totalSteps: number;
}
 
interface OnboardingFormProps {
  onComplete: (userData: { username: string; bio: string }) => Promise<void>;
  stepNumber: number;
  totalSteps: number;
}
 
interface OnboardingStep {
  image: string;
  description: string;
}

Progress Bar Component

const ProgressBar: React.FC<ProgressBarProps> = ({ currentStep, totalSteps }) => {
  const progress = ((currentStep - 1) / (totalSteps - 1)) * 100;
 
  const getIcon = (index: number) => {
    if (index < currentStep - 1) {
      return <CheckCircle className="w-6 h-6 text-green-500" />;
    } else if (index === currentStep - 1) {
      return <Circle className="w-6 h-6 text-blue-500" />;
    } else {
      return <XCircle className="w-6 h-6 text-gray-400" />;
    }
  };
 
  return (
    <div className="w-full relative">
      <div className="h-2 bg-gray-200 mt-3">
        <div
          className="absolute top-0 left-0 h-full bg-green-500 transition-all duration-300 ease-in-out"
          style={{ width: `${progress}%` }}
        ></div>
      </div>
      <div className="absolute flex w-full justify-between top-1/2 transform -translate-y-1/2">
        {Array.from({ length: totalSteps }).map((_, index) => (
          <div key={index} className="flex items-center justify-center w-8 h-8 bg-white rounded-full border-2">
            {getIcon(index)}
          </div>
        ))}
      </div>
    </div>
  );
};

Onboarding Steps Component

const OnboardingStep: React.FC<OnboardingStepProps> = ({ image, description, onNext, stepNumber, totalSteps }) => {
  return (
    <Card className="flex flex-col items-center justify-center">
      <CardHeader>
        <CardTitle>{`Etape ${stepNumber} sur ${totalSteps}`}</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col items-center justify-center">
        <Image src={image} alt="Onboarding Step" width={400} height={300} />
        <p className="mt-4 text-center">{description}</p>
        <Button onClick={onNext} className="mt-4">
          Suivant
        </Button>
      </CardContent>
    </Card>
  );
};

Onboarding form

const OnboardingForm: React.FC<OnboardingFormProps> = ({ onComplete, stepNumber, totalSteps }) => {
  const [username, setUsername] = useState("");
  const [bio, setBio] = useState("");
  const [loading, setLoading] = useState(false);
 
  const handleSubmit = async () => {
    setLoading(true);
    await onComplete({ username, bio });
    setLoading(false);
  };
 
  return (
    <Card className="w-full">
      <CardHeader>
        <CardTitle>Complétez votre profil</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-4">
          <Input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Nom d'utilisateur" />
          <Input value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Bio" />
          <Button onClick={handleSubmit} disabled={loading}>
            {loading ? <Loader2 className="animate-spin" /> : "Soumettre"}
          </Button>
        </div>
      </CardContent>
    </Card>
  );
};

Core Onboarding Component

const Onboarding: React.FC = () => {
  const router = useRouter();
  const { data: session } = useSession();
  const [currentStep, setCurrentStep] = useState(1);
  const totalSteps = 3; // Nombre total d'étapes d'onboarding
 
  const onboardingSteps: OnboardingStep[] = [
    { image: "/step1.png", description: "Bienvenue sur Galsenext!" },
    { image: "/step2.png", description: "Renseignez votre profil." },
    { image: "/step3.png", description: "Vous êtes prêt à commencer!" },
  ];
 
  const handleNextStep = () => {
    setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
  };
 
  const handleCompleteOnboarding = async (userData: { username: string; bio: string }) => {
    if (!session?.user) return;
 
    const response = await fetch("/api/user/onboard", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(userData),
    });
 
    if (response.ok) {
      router.push("/dashboard");
    } else {
      console.error("Erreur lors de l'onboarding");
    }
  };
 
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <ProgressBar currentStep={currentStep} totalSteps={totalSteps} />
      {currentStep < totalSteps ? (
        <OnboardingStep
          image={onboardingSteps[currentStep - 1].image}
          description={onboardingSteps[currentStep - 1].description}
          onNext={handleNextStep}
          stepNumber={currentStep}
          totalSteps={totalSteps}
        />
      ) : (
        <OnboardingForm onComplete={handleCompleteOnboarding} stepNumber={currentStep} totalSteps={totalSteps} />
      )}
    </div>
  );
};
 
export default Onboarding;

Additional Explanations

  • ProgressBar Component: Displays the user's progress through the onboarding process.
  • OnboardingStep Component: Represents an individual step in the onboarding process.
  • OnboardingForm Component: Form for entering user information (username and bio).
  • Onboarding Component: Manages the complete onboarding process, including step management and onboarding completion.

Explanation of the auth.ts File

The auth.ts file in /src/lib/auth provides utility functions for retrieving server-side authentication sessions. It uses getServerSession from next-auth to obtain the current user's session information. This file facilitates authentication and authorization in various contexts, such as API requests or pages requiring authentication.

import {
  GetServerSidePropsContext,
  NextApiRequest,
  NextApiResponse,
} from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "../../pages/api/auth/[...nextauth]";
 
type ParametersGetServerSession =
  | []
  | [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
  | [NextApiRequest, NextApiResponse];
 
export const getAuthSession = async (
  ...parameters: ParametersGetServerSession
) => {
  const session = await getServerSession(...parameters, authOptions);
  return session;
};
 
export const getRequiredAuthSession = async (
  ...parameters: ParametersGetServerSession
) => {
  const session = await getServerSession(...parameters, authOptions);
 
  if (!session?.user.id) {
    throw new Error("Unauthorized");
  }
 
  return session as {
    user: {
      id: string;
      email?: string;
      image?: string;
      name?: string;
    };
  };
};

Explanation of Main Functions

  1. getAuthSession:

    • This function uses getServerSession to retrieve the current user session. It accepts parameters that can come from various request contexts (server-side or API).
    • It returns the authentication session without checking if the user is authenticated.
  2. getRequiredAuthSession:

    • This function also retrieves the user session via getServerSession, but it performs an additional check to ensure the user is authenticated.
    • If the session does not contain a valid user ID, it throws an "Unauthorized" error.
    • It returns a session object enriched with required user information.

Usage in the Application

  • getAuthSession can be used when you need to access the user session without requiring strict authentication, for example, to customize the content of a public page based on the logged-in user.
  • getRequiredAuthSession is used in routes or pages that require mandatory authentication, ensuring that only an authenticated user can access the resource.

On this page