[Update User] can't update user, /api/auth/update-user return 401 (Unauthorized) #934

Closed
opened 2026-03-13 08:10:29 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @vanirvan on GitHub (Mar 28, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

context: This is a Nextjs Application with server action
here's my repository: https://github.com/vanirvan/tanyain

  1. install Nextjs, drizzle, and follow better-auth installation using drizzle as ORM
  2. use my auth config provided in the "auth config" section bellow
  3. change authClient in the lib/auth-client to this:
export const authClient = createAuthClient({
  baseURL: process.env.BETTER_AUTH_BASE_URL!,
  plugins: [
    inferAdditionalFields<typeof auth>(), // add this plugin so better-auth can read additional fields from the auth object
  ],
});
  1. create a client component to send data to server action, here's the component but using shadcn-ui:
"use client";

import React, {
  startTransition,
  useActionState,
  useEffect,
  useState,
} from "react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

import { updateNameAction } from "@/lib/actions/update-name-action";
import { authClient } from "@/lib/auth-client";

export function NameField() {
  const { data } = authClient.useSession();

  const [form, setForm] = useState<string>("");
  const [formDisabled, setFormDisabled] = useState<boolean>(false);
  const [formError, setFormError] = useState<string[]>([]);
  const [state, formAction] = useActionState(updateNameAction, {
    success: false,
    message: null,
  });
  const [showToast, setShowToast] = useState(false);

  useEffect(() => {
    if (data) {
      setForm(data.user.name);
    }
  }, [data]);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setFormDisabled(true);
    setFormError([]);
    startTransition(() => {
      formAction(form);
    });
  };

  useEffect(() => {
    if (!state.success && state.message) {
      setFormError(state.message);
    } else if (state.success && !state.message) {
      toast.success("Name updated successfully.");
    }

    setFormDisabled(false);
  }, [state]);

  useEffect(() => {
    if (showToast) {
      setTimeout(() => {
        setShowToast(false);
      }, 3000); // 3 seconds
    }
  }, [showToast]);

  return (
    <>
      <Card id="name">
        <CardContent>
          <form
            onSubmit={handleSubmit}
            id="name-form"
            className="flex flex-col gap-6"
          >
            <Label htmlFor="name-field" asChild>
              <div className="flex flex-col items-start">
                <h1 className="text-xl font-bold">Name</h1>
                <h2 className="text-foreground/75 text-sm">Your full name</h2>
              </div>
            </Label>
            <div className="flex flex-col gap-3">
              <Input
                id="name-field"
                placeholder="Your Name"
                value={form ?? ""}
                disabled={formDisabled}
                maxLength={64}
                onChange={(e) => setForm(e.target.value)}
              />
              {formError.length > 0 && (
                <div className="flex flex-col gap-1">
                  {formError.map((error, index) => (
                    <p key={index} className="text-xs text-red-500">
                      {error}
                    </p>
                  ))}
                </div>
              )}
            </div>
          </form>
        </CardContent>
        <CardFooter className="flex justify-between border-t">
          <p className="text-foreground/50 text-sm">
            Please use 64 characters or less.
          </p>
          <Button form="name-form" disabled={formDisabled}>
            Save
          </Button>
        </CardFooter>
      </Card>
    </>
  );
}
  1. create the server action in lib/action/update-name-action.ts:
"use server";

import { revalidatePath } from "next/cache";
import { z } from "zod";
import { authClient } from "@/lib/auth-client";

const nameSchema = z
  .string()
  .max(64, { message: "Must be less than 64 characters." });

type UpdateNameResult = {
  success: boolean;
  message: string[] | null;
};

export async function updateNameAction(
  prevState: UpdateNameResult,
  name: string,
): Promise<UpdateNameResult> {
  const validatedFields = await nameSchema.safeParse(name);
  if (!validatedFields.success) {
    return {
      success: false,
      message: validatedFields.error.flatten().formErrors,
    };
  }

  try {
    const updateUserName = await authClient.updateUser({ name: validatedFields.data });
    console.log(updateUserName);
    console.log(updateUserName.error);

    revalidatePath("/settings");
    return { success: true, message: null };
  } catch (e) {
    console.error("Error updating name:", e);
    return { success: false, message: ["Failed to update name."] };
  }
}
  1. create signIn button component:
"use client";

export function SignInButton(){
  const handleSignIn = () => {
    authClient.signIn.social({
      provider: "google"
    })
  }

  return (
    <button onClick={() => authClient.signIn.social()}>Sign In</button>
  )
}
  1. put client component and signin button component to app/page.tsx
  2. sign in first, then try to change your Name

Current vs. Expected behavior

in the log, it looks like this:

POST /api/auth/update-user 401 in 156ms
{ data: null, error: { status: 401, statusText: 'UNAUTHORIZED' } }

i want to know why it's unauthorized

What version of Better Auth are you using?

^1.2.4

Provide environment information

- OS: Windows 10
- Browser: Chrome

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

import { db } from "@/lib/db/db";
import * as drizzleSchema from "@/lib/db/schema";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: { ...drizzleSchema },
  }),
  user: {
    additionalFields: {
      username: {
        type: "string",
        unique: true,
      },
    },
  },
  emailAndPassword: {
    enabled: false,
  },
  socialProviders: {
    github: {
      clientId: process.env.BETTER_AUTH_GITHUB_CLIENT_ID as string,
      clientSecret: process.env.BETTER_AUTH_GITHUB_CLIENT_SECRET as string,
    },
    google: {
      clientId: process.env.BETTER_AUTH_GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.BETTER_AUTH_GOOGLE_CLIENT_SECRET as string,
    },
  },
});

Additional context

i only test it locally, learn how to use better-auth with new project

Originally created by @vanirvan on GitHub (Mar 28, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce context: This is a Nextjs Application with server action here's my repository: https://github.com/vanirvan/tanyain 1. install Nextjs, drizzle, and follow better-auth installation using drizzle as ORM 2. use my auth config provided in the "auth config" section bellow 3. change `authClient` in the `lib/auth-client` to this: ``` export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_BASE_URL!, plugins: [ inferAdditionalFields<typeof auth>(), // add this plugin so better-auth can read additional fields from the auth object ], }); ``` 4. create a client component to send data to server action, here's the component but using shadcn-ui: ``` "use client"; import React, { startTransition, useActionState, useEffect, useState, } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { updateNameAction } from "@/lib/actions/update-name-action"; import { authClient } from "@/lib/auth-client"; export function NameField() { const { data } = authClient.useSession(); const [form, setForm] = useState<string>(""); const [formDisabled, setFormDisabled] = useState<boolean>(false); const [formError, setFormError] = useState<string[]>([]); const [state, formAction] = useActionState(updateNameAction, { success: false, message: null, }); const [showToast, setShowToast] = useState(false); useEffect(() => { if (data) { setForm(data.user.name); } }, [data]); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setFormDisabled(true); setFormError([]); startTransition(() => { formAction(form); }); }; useEffect(() => { if (!state.success && state.message) { setFormError(state.message); } else if (state.success && !state.message) { toast.success("Name updated successfully."); } setFormDisabled(false); }, [state]); useEffect(() => { if (showToast) { setTimeout(() => { setShowToast(false); }, 3000); // 3 seconds } }, [showToast]); return ( <> <Card id="name"> <CardContent> <form onSubmit={handleSubmit} id="name-form" className="flex flex-col gap-6" > <Label htmlFor="name-field" asChild> <div className="flex flex-col items-start"> <h1 className="text-xl font-bold">Name</h1> <h2 className="text-foreground/75 text-sm">Your full name</h2> </div> </Label> <div className="flex flex-col gap-3"> <Input id="name-field" placeholder="Your Name" value={form ?? ""} disabled={formDisabled} maxLength={64} onChange={(e) => setForm(e.target.value)} /> {formError.length > 0 && ( <div className="flex flex-col gap-1"> {formError.map((error, index) => ( <p key={index} className="text-xs text-red-500"> {error} </p> ))} </div> )} </div> </form> </CardContent> <CardFooter className="flex justify-between border-t"> <p className="text-foreground/50 text-sm"> Please use 64 characters or less. </p> <Button form="name-form" disabled={formDisabled}> Save </Button> </CardFooter> </Card> </> ); } ``` 5. create the server action in `lib/action/update-name-action.ts`: ``` "use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { authClient } from "@/lib/auth-client"; const nameSchema = z .string() .max(64, { message: "Must be less than 64 characters." }); type UpdateNameResult = { success: boolean; message: string[] | null; }; export async function updateNameAction( prevState: UpdateNameResult, name: string, ): Promise<UpdateNameResult> { const validatedFields = await nameSchema.safeParse(name); if (!validatedFields.success) { return { success: false, message: validatedFields.error.flatten().formErrors, }; } try { const updateUserName = await authClient.updateUser({ name: validatedFields.data }); console.log(updateUserName); console.log(updateUserName.error); revalidatePath("/settings"); return { success: true, message: null }; } catch (e) { console.error("Error updating name:", e); return { success: false, message: ["Failed to update name."] }; } } ``` 6. create signIn button component: ``` "use client"; export function SignInButton(){ const handleSignIn = () => { authClient.signIn.social({ provider: "google" }) } return ( <button onClick={() => authClient.signIn.social()}>Sign In</button> ) } ``` 7. put client component and signin button component to `app/page.tsx` 8. sign in first, then try to change your Name ### Current vs. Expected behavior in the log, it looks like this: ``` POST /api/auth/update-user 401 in 156ms { data: null, error: { status: 401, statusText: 'UNAUTHORIZED' } } ``` i want to know why it's unauthorized ### What version of Better Auth are you using? ^1.2.4 ### Provide environment information ```bash - OS: Windows 10 - Browser: Chrome ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/lib/db/db"; import * as drizzleSchema from "@/lib/db/schema"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: { ...drizzleSchema }, }), user: { additionalFields: { username: { type: "string", unique: true, }, }, }, emailAndPassword: { enabled: false, }, socialProviders: { github: { clientId: process.env.BETTER_AUTH_GITHUB_CLIENT_ID as string, clientSecret: process.env.BETTER_AUTH_GITHUB_CLIENT_SECRET as string, }, google: { clientId: process.env.BETTER_AUTH_GOOGLE_CLIENT_ID as string, clientSecret: process.env.BETTER_AUTH_GOOGLE_CLIENT_SECRET as string, }, }, }); ``` ### Additional context i only test it locally, learn how to use better-auth with new project
GiteaMirror added the bug label 2026-03-13 08:10:29 -05:00
Author
Owner

@Bekacru commented on GitHub (Mar 28, 2025):

authClient is meant to be used on the client. So call it from a client not from the server.

@Bekacru commented on GitHub (Mar 28, 2025): authClient is meant to be used on the client. So call it from a client not from the server.
Author
Owner

@vanirvan commented on GitHub (Mar 28, 2025):

makes sense, my bad.

@vanirvan commented on GitHub (Mar 28, 2025): makes sense, my bad.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#934