Dillan Wilding
Dillan Wilding

Reputation: 1111

How to use numeric user id with next-auth

I recently found and started testing create-t3-app as a new base for NextJS projects because it takes care of a lot of boilerplate setup for things like TypeScript, trpc, prisma, and next-auth so it'd save me a ton of time. While that's relevant, I don't think it's the source of my problem. My problem is that I use a MySQL database with auto-incrementing user IDs but it seems like the types for the next-auth package are forcing me to use a string (DefaultUser in next-auth/core/types.d.ts and AdapterUser in next-auth/adapters.d.ts both set the type of id to string and the comments say UUIDs). Thinking that I could possibly extend what's there to support numeric user IDs, I added this to next-auth.d.ts:

import { DefaultSession, DefaultUser } from 'next-auth'

declare module 'next-auth' {
  interface User extends DefaultUser {
    id: number;
  }
  interface Session {
    user?: {
      id: number;
    } & DefaultSession['user'];
  }
}

Which seems to work most places except in [...nextauth].ts where it is giving me this error

Type 'string | number' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'.ts(2322)

On the session.user.id = user.id line in this section of code

export const authOptions: NextAuthOptions = {
  // Include user.id on session
  callbacks: {
    session({ session, user }) {
      if (session.user) {
        session.user.id = user.id
      }
      return session
    }
  },
  adapter: PrismaAdapter(prisma),
  providers: []
}

export default NextAuth(authOptions)

The TypeScript error goes away if I delete the id: string; line from the AdapterUser from next-auth/adapters.d.ts which then falls back to id: number; because I set that on User

export interface AdapterUser extends User {
    id: string; // <-- I removed this
    email: string;
    emailVerified: Date | null;
}

I don't think I should have to modify the library's types to support numeric user IDs but I'm all out of ideas how to solve this and haven't found answers online. Or should I not be using numeric IDs even though my database does? If it helps, I'm using

next 12.3.1
next-auth 4.12.3
react 18.2.0
react-dom 18.2.0
superjson 1.9.1
zod 3.18.0
eslint 8.22.0
postcss 8.4.14
prettier 2.7.1
prisma 4.4.0
tailwindcss 3.1.6
typescrypt 4.8.0

Upvotes: 9

Views: 2431

Answers (2)

Pedro Del Moral Lopez
Pedro Del Moral Lopez

Reputation: 191

Expanding on the above accepted answer, I utilized runtime typeguards to make my life a little easier.

In my case, my Prisma user had a few extra attributes as well:

model User {
  id                  Int                 @id @default(autoincrement())
  uuid                String              @unique @default(cuid())
  name                String?
  email               String?             @unique
  emailVerified       DateTime?
  image               String?
  accounts            Account[]
  categories          Category[]
  sessions            Session[]
  records             Record[]
  permissionsGranted  SharePermission[]     @relation("permissions_granted")
  permissionsReceived SharePermission[]     @relation("permissions_received")
  @@unique([id, uuid])
  @@index(fields: [id, uuid], name: "user_by_id_idx")
}

So I defined these typeguards:

import { User as PrismaUser } from '@prisma/client';
import { Session, User } from 'next-auth';

export interface IdentifiedSession extends Session {
    id: number;
    user: PrismaUser;
}

export const isNextAuthUser = (value: unknown): value is User => {
    const { id, name, email, image } = (value ?? {}) as User;
    const idIsValid = typeof id === 'number';
    const nameIsValid = typeof name === 'string' || !name;
    const emailIsValid = typeof email === 'string' || !email;
    const imageIsValid = typeof image === 'string' || !image;
    return idIsValid && nameIsValid && emailIsValid && imageIsValid;
};

export const isPrismaUser = (value: unknown): value is PrismaUser => {
    const { uuid, emailVerified } = (value ?? {}) as PrismaUser;
    const uuidIsValid = typeof uuid === 'string';
    const emailVerifiedIsEmail = emailVerified instanceof Date || !emailVerified;
    return isNextAuthUser(value) && uuidIsValid && emailVerifiedIsEmail;
};

export const isSession = (value: unknown): value is Session => {
    const { user, expires } = (value ?? {}) as Session;
    const userIsValid = isPrismaUser(user) || isNextAuthUser(user) || !user;
    const expiresIsValid = typeof expires === 'string';
    return expiresIsValid && userIsValid;
};

export const isIdentifiedSession = (value: unknown): value is IdentifiedSession => {
    const session = (value ?? {}) as IdentifiedSession;
    const sessionIsValid = isSession(session);
    const sessionUserIsValid = isPrismaUser(session.user);
    return sessionIsValid && sessionUserIsValid;
};

Then, in the session callback, I utilized one of the typeguards like so:

session: async ({ session, user }) => {
  if (!isPrismaUser(user)) {
    return session;
  }
  const identifiedSession: IdentifiedSession = {
    ...session,
    user,
    id: user.id,
  };
  return identifiedSession;
}

Which does mean that the signature of the callback is now Session | IdentifiedSession, which I handle in downstream consumers as such

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
    const session = await getSession({ req });
    if (!isIdentifiedSession(session)) {
        return {
            props: {
                records: [],
            },
        };
    }
    const records = await prisma.record.findMany({
        where: {
            reporter: {
                id: session.id,
            },
        },
        orderBy: {
            date: 'asc',
        },
        include: {
            category: {
                select: {
                    title: true,
                },
            },
        },
    });
    return {
        props: { records },
    };
};

Upvotes: 0

tenshi
tenshi

Reputation: 26327

Unfortunately it doesn't seem possible to "patch up" or change the types the library has - even trying to change NextAuthOptions doesn't work since subsequent property definitions must have the same type. In the meantime, you'll have to use a runtime check/coercion or cast:

export const authOptions: NextAuthOptions = {
  // Include user.id on session
  callbacks: {
    session({ session, user }) {
      if (session.user) {
        if (typeof user.id !== "number") throw new Error("id should a number");
        session.user.id = user.id // OK
        // session.user.id = +user.id // more dangerous but still works
        // session.user.id = user.id as number // also dangerous
      }
      return session
    }
  },
  adapter: PrismaAdapter(prisma),
  providers: []
}

Upvotes: 3

Related Questions