All files / src/lib/auth auth.ts

0% Statements 0/127
100% Branches 0/0
0% Functions 0/1
0% Lines 0/127

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                                                                                                                                                                                                               
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import bcryptjs from "bcryptjs";
import {
  isAccountLocked,
  getRemainingLockoutTime,
  recordFailedLoginAttempt,
  resetFailedLoginAttempts } from "./account-lockout";
import { logLoginAttempt } from "@/lib/audit-logger";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }},
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error("Invalid credentials");
        }

        const email = (credentials.email as string).toLowerCase();

        const user = await prisma.user.findUnique({
          where: { email }});

        if (!user || !user.password) {
          // Log failed attempt for non-existent user (don't reveal user doesn't exist)
          await logLoginAttempt(
            new Request("http://localhost"),
            null,
            false,
            email
          );
          throw new Error("Invalid credentials");
        }

        // Check if account is locked
        if (await isAccountLocked(user.id)) {
          const remainingTime = await getRemainingLockoutTime(user.id);
          const minutes = remainingTime ? Math.ceil(remainingTime / 60) : 15;
          throw new Error(
            `Account locked. Please try again in ${minutes} minute${minutes !== 1 ? "s" : ""}.`
          );
        }

        const isPasswordValid = await bcryptjs.compare(
          credentials.password as string,
          user.password
        );

        if (!isPasswordValid) {
          // Record failed login attempt
          const wasLocked = await recordFailedLoginAttempt(user.id);
          await logLoginAttempt(
            new Request("http://localhost"),
            user.id,
            false,
            email
          );

          if (wasLocked) {
            throw new Error(
              "Account locked due to too many failed attempts. Please try again in 15 minutes."
            );
          }

          throw new Error("Invalid credentials");
        }

        // Successful login - reset failed attempts
        await resetFailedLoginAttempts(user.id);
        await logLoginAttempt(
          new Request("http://localhost"),
          user.id,
          true,
          email
        );

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          image: user.image};
      }}),
    // GitHub OAuth Provider
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
  pages: {
    signIn: "/signin"},
  callbacks: {
    async signIn() {
      // Allow all sign-ins - role defaults are handled by the database schema
      return true;
    },
    async jwt({ token, user }) {
      if (user) {
        // Convert user.id to number (NextAuth serializes it as string)
        const userId = typeof user.id === 'string' ? parseInt(user.id, 10) : user.id as number;
        token.id = userId;
        // Fetch user from database to get the role
        const dbUser = await prisma.user.findUnique({
          where: { id: userId }});
        if (dbUser) {
          token.role = dbUser.role || "CUSTOMER";
        }
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user && typeof token.id === 'number') {
        (session.user as { id: number; role?: string }).id = token.id;
        (session.user as { id: number; role?: string }).role = token.role as string;
      }
      return session;
    }},
  session: {
    strategy: "jwt"},
  secret: process.env.NEXTAUTH_SECRET});