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 }
);
}
})
);
}