Docs
User Metadata
Experimental

User Metadata

Displays and edits user metadata with schema validation.

    Preferences

    Installation

    Install the following dependencies:

    npx shadcn-ui@latest add card button form input select toast label separator
    pnpm install zod react-hook-form @hookform/resolvers

    Copy and paste the following code into your project.

    "use client"; import { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Toaster } from "@/components/ui/toaster"; import { useToast } from "@/components/ui/use-toast"; import { zodResolver } from "@hookform/resolvers/zod"; interface KeyValueMap { [key: string]: any; } function Spinner() { return ( <svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2 animate-spin" > <path d="M21 12a9 9 0 1 1-6.219-8.56"></path> </svg> ); } export default function UserMetadataForm({ schema, metadata, onSave, onFetch, }: { schema: any; metadata?: KeyValueMap; onFetch: () => Promise<KeyValueMap>; onSave?: (values: KeyValueMap) => Promise<{ metadata?: KeyValueMap; status: number; }>; }) { const [fetching, setFetching] = useState(false); const [defaultValues, setDefaultValues] = useState<KeyValueMap | undefined>( metadata ); const { toast } = useToast(); const [working, setWorking] = useState<boolean>(false); const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema), defaultValues, }); async function onSubmit(values: z.infer<typeof schema>) { setWorking(true); if (typeof onSave === "function") { const response = await onSave(values); if (response.status !== 200) { toast({ title: "Info", description: "There was a problem updating preferences. Try again later.", }); } } setWorking(false); } const handleFetching = useCallback( async function handleFetching() { setFetching(true); const response = await onFetch(); if (response.status !== 200) { return setFetching(false); } setDefaultValues(response.metadata); form.reset(response.metadata, { keepValues: false }); setFetching(false); }, [form, onFetch] ); useEffect(() => { (async () => { if (!defaultValues) { await handleFetching(); } })(); }, [defaultValues, handleFetching]); return ( <> <Toaster /> <Card className="w-full"> <CardHeader className="p-4 md:p-6"> <CardTitle className="text-lg font-normal">Preferences</CardTitle> <CardDescription></CardDescription> </CardHeader> {fetching && ( <CardContent className="p-4 pt-0 md:p-6 md:pt-0"> <div className="flex w-full items-center justify-left"> <Spinner /> <span className="text-sm text-muted-foreground"> Fetching Preferences... </span> </div> </CardContent> )} {!defaultValues && !fetching && ( <CardContent className="p-4 pt-0 md:p-6 md:pt-0"> <div className="flex flex-col gap-6"> <Separator /> <div className="flex items-center justify-between space-x-2"> <Label className="flex flex-col space-y-2"> <p className="font-normal leading-snug text-muted-foreground max-w-fit"> There was a problem retrieving preferences. Try again later. </p> </Label> </div> </div> </CardContent> )} {defaultValues && !fetching && ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <CardContent className="p-4 pt-0 md:p-6 md:pt-0"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {Object.keys(schema.shape).map((key: any) => { // @ts-ignore const type = schema.shape[key]._def; const formLabel = ( <FormLabel className="capitalize"> {key.replace("_", " ")} </FormLabel> ); if (type.typeName === "ZodEnum") { return ( <FormField key={key} control={form.control} name={key} render={({ field }) => ( <FormItem> {formLabel} <Select onValueChange={field.onChange} defaultValue={field.value} > <FormControl> <SelectTrigger> <SelectValue /> </SelectTrigger> </FormControl> <SelectContent> {type.values.map((value: any) => ( <SelectItem key={value} value={value}> {value} </SelectItem> ))} </SelectContent> </Select> <FormMessage /> </FormItem> )} /> ); } return ( <FormField key={key} control={form.control} name={key} render={({ field }) => ( <FormItem> {formLabel} <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> ); })} </div> </CardContent> <CardFooter className="p-4 pt-0 md:p-6 md:pt-0"> <Button type="submit" disabled={working} className="disabled:opacity-50 ml-auto" > {working && <Spinner />} Save Preferences </Button> </CardFooter> </form> </Form> )} </Card> </> ); }

    Component behavior

    By design, our components provide basic behavior without making any requests to the Auth0 Management API. To help you implement the full feature, we've also included React Hooks and NextJS routers for calling and proxying the Auth0 Management API.

    Update the import paths to match your project setup.

    React Hooks

    useUserMetadata

    A hook to update the user metadata.

    import { useCallback } from "react"; interface KeyValueMap { [key: string]: any; } export default function useUserMedata() { const fetchUserMetadata = useCallback(async (): Promise<KeyValueMap> => { try { /** * '/api/auth/user/metadata' is a custom endpoint which will proxy * the request to the Auth0 Management API. * * Proxy sample at: https://components.lab.auth0.com/docs/components/user-metadata#nextjs-routers */ const response = await fetch("/api/auth/user/metadata", { method: "GET", headers: { "Content-Type": "application/json", }, }); // TODO: Better handling rate limits if (response.status === 429) { return { status: 429 }; } const userMetadata: KeyValueMap = await response.json(); return { metadata: userMetadata, status: response.status, }; } catch (error) { console.error(error); return { status: 500 }; } }, []); const updateUserMetadata = useCallback( async ( values: KeyValueMap ): Promise<{ metadata?: KeyValueMap; status: number; }> => { try { /** * '/api/auth/user/metadata' is a custom endpoint which will proxy * the request to the Auth0 Management API. * * Proxy sample at: https://components.lab.auth0.com/docs/components/user-metadata#nextjs-routers */ const response = await fetch("/api/auth/user/metadata", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(values), }); // TODO: Better handling rate limits if (response.status === 429) { return { status: 429 }; } const metadata: KeyValueMap = await response.json(); return { metadata, status: response.status }; } catch (e) { console.error(e); return { status: 500 }; } }, [] ); return { updateUserMetadata, fetchUserMetadata }; }

    NextJS routers

    UserMetadata router

    Handles user metadata update.

    import { ManagementClient } from "auth0"; import { NextResponse } from "next/server"; import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; /** * Make sure to install the withRateLimit from: * - https://components.lab.auth0.com/docs/rate-limit#helpers */ import { withRateLimit } from "./helpers/rate-limit"; const client = new ManagementClient({ domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host, clientId: process.env.AUTH0_CLIENT_ID_MGMT!, clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!, }); /** * @example export const GET = handleUserMetadataFetch(); */ export function handleUserMetadataFetch() { return withRateLimit( withApiAuthRequired(async (): Promise<NextResponse> => { try { const session = await getSession(); const user_id = session?.user.sub; const response = await client.users.get({ id: user_id, }); const { data } = response; return NextResponse.json(data.user_metadata || {}, { status: response.status, }); } catch (error) { console.error(error); return NextResponse.json( { error: "Error fetching user metadata" }, { status: 500 } ); } }) ); } /** * @example export const PUT = handleUserMetadataUpdate(); */ // TODO: better error handling export function handleUserMetadataUpdate() { return withRateLimit( withApiAuthRequired(async (request: Request): Promise<NextResponse> => { try { const session = await getSession(); const userId = session?.user.sub; const user_metadata = await request.json(); await client.users.update({ id: userId }, { user_metadata }); return NextResponse.json(user_metadata, { status: 200, }); } catch (error) { console.error(error); return NextResponse.json( { error: "Error updating user metadata" }, { status: 500 } ); } }) ); }