You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于T3 Stack+NextAuth的无ID自定义用户模型认证问题

问题

使用T3 Stack(App Router、TRPC、Prisma、NextAuth)开发项目时,自定义Prisma的User模型,移除默认ID字段,将username设为主键,仅保留username、email、password、first_name、last_name字段。完成数据库迁移后,在NextAuth配置中使用Credentials Provider时,authorize函数出现类型错误——NextAuth期望User对象包含必填的id字段,现有文档及渠道未找到解决方法,求实现无ID用户认证的可行方案。

src/server/auth.ts

/**
 * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
 * object and keep type safety.
 *
 * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
 */

declare module "next-auth" {
  interface Session extends DefaultSession {
    user: {
      username: string;
      email: string;
      first_name: string;
      last_name: string;
    } & DefaultSession["user"];
  }

  interface User {
    username: string;
    email: string;
    password: string;
    first_name: string;
    last_name: string;
  }
}

/**
 * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
 *
 * @see https://next-auth.js.org/configuration/options
 */
export const authOptions: NextAuthOptions = {
  callbacks: {
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        username: user.username,
        email: user.email,
        first_name: user.first_name,
        last_name: user.last_name,
      },
    }),
  },
  // adapter: PrismaAdapter(db) as Adapter,
  providers: [
    CredentialsProvider({
      // The name to display on the sign in form (e.g. 'Sign in with...')
      name: "Credentials",
      // The credentials is used to generate a suitable form on the sign in page.
      // You can specify whatever fields you are expecting to be submitted.
      // e.g. domain, username, password, 2FA token, etc.
      // You can pass any HTML attribute to the <input> tag through the object.
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" },
      },

      async authorize(credentials, req) {
        const creds = await z
          .object({
            username: z.string(),
            password: z.string().min(6),
          })
          .parseAsync(credentials);

        // Find user by username
        const user = await db.user.findUnique({
          where: { username: creds.username },
        });

        // If user not found, return null
        if (!user) return null;

        // Verify password
        const valid = await verifyPassword(creds.password, user.password);

        // If password is invalid, return null
        if (!valid) return null;

        // If user is found and password is valid, return user
        return user;
      },
    }),
    /**
     * ...add more providers here.
     *
     * Most other providers require a bit more work than the Discord provider. For example, the
     * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
     * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
     *
     * @see https://next-auth.js.org/providers/github
     */
  ],
};

Prisma Schema

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

generator zod {
  provider = "zod-prisma-types"
}

datasource db {
    provider = "postgresql"
    // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
    // Further reading:
    // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
    // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
    url      = env("DATABASE_URL")
}



model Session {
    id           String   @id @default(cuid())
    sessionToken String   @unique
    usernameId       String
    expires      DateTime
    user         User     @relation(fields: [usernameId], references: [username], onDelete: Cascade)
}

model User {
  username String @unique
  email String @unique
  password String 
  first_name String
  last_name String
    sessions      Session[]
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@unique([identifier, token])
}

错误信息

Type '(credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Promise<...>' is not assignable to type '(credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Awaitable<...>'.
  Type 'Promise<{ username: string; email: string; password: string; first_name: string; last_name: string; } | null>' is not assignable to type 'Awaitable<User | null>'.
    Type 'Promise<{ username: string; email: string; password: string; first_name: string; last_name: string; } | null>' is not assignable to type 'PromiseLike<User | null>'.
      Types of property 'then' are incompatible.
        Type '<TResult1 = { username: string; email: string; password: string; first_name: string; last_name: string; } | null, TResult2 = never>(onfulfilled?: ((value: { username: string; email: string; password: string; first_name: string; last_name: string; } | null) => TResult1 | PromiseLike<...>) | null | undefined, onreject...' is not assignable to type '<TResult1 = User | null, TResult2 = never>(onfulfilled?: ((value: User | null) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | null | undefined) => PromiseLike<...>'.
          Types of parameters 'onfulfilled' and 'onfulfilled' are incompatible.
            Types of parameters 'value' and 'value' are incompatible.
              Type '{ username: string; email: string; password: string; first_name: string; last_name: string; } | null' is not assignable to type 'User | null'.
                Property 'id' is missing in type '{ username: string; email: string; password: string; first_name: string; last_name: string; }' but required in type 'User'.ts(2322)
types.d.ts(504, 5): 'id' is declared here.
credentials.d.ts(13, 5): The expected type comes from property 'authorize' which is declared here on type 'UserCredentialsConfig<{ username: { label: string; type: string; placeholder: string; }; password: { label: string; type: string; }; }>'
(property) authorize: (credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Awaitable<User | null>

解决方案

NextAuth核心逻辑依赖User对象存在唯一标识字段id,但不强制要求该字段是数据库自增ID,只要是唯一值即可。通过以下两步解决类型错误:

1. 修正类型扩展,明确包含id字段

在src/server/auth.ts的模块扩展部分,给自定义的User接口添加id字段,对齐NextAuth默认类型:

declare module "next-auth" {
  interface Session extends DefaultSession {
    user: {
      username: string;
      email: string;
      first_name: string;
      last_name: string;
    } & DefaultSession["user"];
  }

  interface User {
    id: string; // 添加该行,匹配NextAuth类型要求
    username: string;
    email: string;
    password: string;
    first_name: string;
    last_name: string;
  }
}

2. 在authorize函数中返回带id的用户对象

找到authorize函数中返回用户的代码,将username赋值给id字段后返回,既满足类型要求,又无需修改数据库结构:

// If user is found and password is valid, return user with id set to username
return { ...user, id: user.username };

修改后,NextAuth可正确识别用户对象,类型错误消失,同时保留自定义的User模型结构。

内容的提问来源于stack exchange,提问作者Ash Gharibyan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 13:28:09