Docs
Organization Info
Experimental
Organization Info
Displays essential organization details, including name and display name, providing a quick glance at organization information.
General
Installation
Install the following dependencies:
npx shadcn-ui@latest add avatar card button form input 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 { Avatar, AvatarImage } from "@/components/ui/avatar";
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 { Separator } from "@/components/ui/separator";
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 OrganizationInfo({
orgId,
organization,
onFetch,
onSave,
}: {
orgId: string;
organization?: KeyValueMap;
onFetch: (id: string) => Promise<KeyValueMap>;
onSave?: (
id: string,
values: KeyValueMap
) => Promise<{
organization?: KeyValueMap;
status: number;
}>;
}) {
const [fetching, setFetching] = useState(false);
const [info, setInfo] = useState<KeyValueMap | undefined>(organization);
const { toast } = useToast();
const [working, setWorking] = useState<boolean>(false);
const schema = z.object({
name: z.string(),
display_name: z.string(),
branding: z
.object({
logo_url: z.string().url(),
})
.optional(),
});
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: organization,
});
async function onSubmit(values: z.infer<typeof schema>) {
setWorking(true);
if (typeof onSave === "function") {
const response = await onSave(orgId, values);
if (response.status !== 200) {
toast({
title: "Info",
description:
"There was a problem updating the info. Try again later.",
});
}
}
setWorking(false);
}
const handleFetching = useCallback(
async function handleFetching() {
setFetching(true);
const response = await onFetch(orgId);
if (response.status !== 200) {
return setFetching(false);
}
setInfo(response.organization);
form.reset(response.organization, { keepValues: false });
setFetching(false);
},
[form, onFetch, orgId]
);
useEffect(() => {
(async () => {
if (!info) {
await handleFetching();
}
})();
}, [handleFetching, info]);
return (
<Card className="w-full">
<CardHeader className="p-4 md:p-6">
<CardTitle className="text-lg font-normal">General</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 info...
</span>
</div>
</CardContent>
)}
{!info && !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 info. Try again later.
</p>
</Label>
</div>
</div>
</CardContent>
)}
{info && !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">
<div className="grid items-center gap-1.5">
<FormField
control={form.control}
name={"name"}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid items-center gap-1.5">
<FormField
control={form.control}
name={"display_name"}
render={({ field }) => (
<FormItem>
<FormLabel>Display Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid items-center gap-1.5">
<FormField
control={form.control}
name={"branding.logo_url"}
render={({ field }) => (
<FormItem>
<FormLabel>Logo URL</FormLabel>
<div className="border rounded-md flex justify-center items-center">
<Avatar className="h-8 w-8 flex items-center ml-[3px] mt-[2px] mb-[2px] rounded-md">
<AvatarImage
src={info.branding.logo_url}
alt={info.name}
/>
</Avatar>
<FormControl>
<Input {...field} className="border-0" />
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
</div>
</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
</Button>
</CardFooter>
</form>
</Form>
)}
</Card>
);
}
Update the import paths to match your project setup.
React Hooks
useOrganizations
The hook can be used to create or update an organization.
"use client";
import { useCallback } from "react";
interface KeyValueMap {
[key: string]: any;
}
type OrganizationCreationResponse = {
name: string;
display_name?: string;
branding?: {
logo_url?: string;
colors?: {
primary: string;
page_background: string;
};
};
metadata?: {
[key: string]: any;
};
enabled_connections?: Array<{
connection_id: string;
assign_membership_on_login?: boolean;
show_as_button?: boolean;
}>;
id: string;
};
export default function useOrganizations() {
const createOrganization = useCallback(
async (
values: any
): Promise<{
organization?: OrganizationCreationResponse;
status: number;
}> => {
try {
/**
* '/api/auth/orgs' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-creator#nextjs-routers
*/
const response = await fetch("/api/auth/orgs", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: values.organization_name }),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: OrganizationCreationResponse =
await response.json();
return {
organization,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const fetchOrganization = useCallback(
async (id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: KeyValueMap = await response.json();
return {
organization,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const updateOrganization = useCallback(
async (
id: string,
values: KeyValueMap
): Promise<{
organization?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/{id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const organization: KeyValueMap = await response.json();
return { organization, status: response.status };
} catch (e) {
console.error(e);
return { status: 500 };
}
},
[]
);
const startSelfServiceConfiguration = useCallback(
async (
values: KeyValueMap
): Promise<{
selfService?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/sso' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch("/api/auth/orgs/sso", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
organizations_to_enable: values.organizations_to_enable,
}),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const selfService = await response.json();
return {
selfService,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const fetchOrganizationConnections = useCallback(
async (id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}/connections' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/${id}/connections`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const connections: KeyValueMap[] = await response.json();
return {
connections,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
const startSelfServiceConnectionUpdate = useCallback(
async (
values: KeyValueMap
): Promise<{
selfService?: KeyValueMap;
status: number;
}> => {
try {
/**
* '/api/auth/orgs/sso' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-metadata#nextjs-routers
*/
const response = await fetch(`/api/auth/orgs/sso`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const selfService: KeyValueMap = await response.json();
return { selfService, status: response.status };
} catch (e) {
console.error(e);
return { status: 500 };
}
},
[]
);
const deleteOrganizationConnection = useCallback(
async (id: string, connection_id: string): Promise<KeyValueMap> => {
try {
/**
* '/api/auth/orgs/{id}/connections/{connection_id}' is a custom endpoint which will proxy
* the request to the Auth0 Management API.
*
* Proxy sample at: https://components.lab.auth0.com/docs/components/organization-sso#nextjs-routers
*/
const response = await fetch(
`/api/auth/orgs/${id}/connections/${connection_id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
// TODO: Better handling rate limits
if (response.status === 429) {
return { status: 429 };
}
const connection: KeyValueMap[] = await response.json();
return {
connection,
status: response.status,
};
} catch (error) {
console.error(error);
return { status: 500 };
}
},
[]
);
return {
createOrganization,
fetchOrganization,
updateOrganization,
fetchOrganizationConnections,
deleteOrganizationConnection,
startSelfServiceConfiguration,
startSelfServiceConnectionUpdate,
};
}
NextJS routers
Organizations router
The route can be used to create or update an organization.
import {
AuthenticationClient,
ManagementClient,
PostOrganizationsRequest,
} from "auth0";
import { NextRequest, 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!,
});
const authClient = new AuthenticationClient({
domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host,
clientId: process.env.AUTH0_CLIENT_ID_MGMT!,
clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!,
});
type HandleOrganizationCreationParams = Pick<
PostOrganizationsRequest,
"enabled_connections"
>;
type SelfServiceParams = {
clients_to_enable: string[];
};
/**
* @example
*
* export const POST = handleOrganizationCreation({
* enabled_connections: [{
* connection_id: process.env.ORGANIZATIONS_ENABLED_CONNECTION!,
* assign_membership_on_login: false,
* }]
* });
*/
// TODO: better error handling
export function handleOrganizationCreation(
params?: HandleOrganizationCreationParams
) {
return withRateLimit(
withApiAuthRequired(async (request: Request): Promise<NextResponse> => {
try {
const session = await getSession();
const userId = session?.user.sub;
const body: PostOrganizationsRequest = await request.json();
const postOrganization: PostOrganizationsRequest = {
name: body.name,
};
if (params && params.enabled_connections) {
postOrganization.enabled_connections = params.enabled_connections;
}
// Create organization
const { data: organization } = await client.organizations.create(
postOrganization
);
// Add current user to new organization
await client.organizations.addMembers(
{ id: organization.id },
{ members: [userId] }
);
return NextResponse.json(
{
id: organization.id,
name: organization.name,
display_name: organization.display_name,
},
{
status: 200,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error creating organization" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const GET = handleFetchOrganization();
*/
export function handleFetchOrganization() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const response = await client.organizations.get({ id: org_id });
const { data } = response;
const org = {
id: data.id,
name: data.name,
display_name: data.display_name,
branding: data.branding || {
logo_url: `https://cdn.auth0.com/avatars/c.png`,
colors: {},
},
metadata: data.metadata || {},
};
return NextResponse.json(org, {
status: response.status,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const PUT = handleOrganizationUpdate();
*/
// TODO: better error handling
export function handleOrganizationUpdate() {
return withRateLimit(
withApiAuthRequired(
async (request: Request, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const org = await request.json();
await client.organizations.update({ id: org_id }, org);
return NextResponse.json(org, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error updating organization" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const POST = handleSelfService();
*/
export function handleSelfService(params: SelfServiceParams) {
return withRateLimit(
withApiAuthRequired(async (request: NextRequest): Promise<NextResponse> => {
try {
const body = await request.json();
const { data: tokens } = await authClient.oauth.clientCredentialsGrant({
audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
});
const $selfServiceProfile = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles`,
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
}
);
const profile = (await $selfServiceProfile.json()).pop();
const randomId = crypto.getRandomValues(new Uint32Array(4)).join("-");
const $ticket = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles/${profile.id}/sso-ticket`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
connection_config: {
name: `${randomId}`,
},
enabled_clients: params.clients_to_enable,
enabled_organizations: body.organizations_to_enable.map(
(organization_id: string) => ({ organization_id })
),
}),
}
);
const data = await $ticket.json();
return NextResponse.json(data, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const PUT = handleSelfService();
*/
export function handleConnectionUpdate(params: SelfServiceParams) {
return withRateLimit(
withApiAuthRequired(async (request: NextRequest): Promise<NextResponse> => {
try {
const body = await request.json();
const { data: tokens } = await authClient.oauth.clientCredentialsGrant({
audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
});
const $selfServiceProfile = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles`,
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
}
);
const profile = (await $selfServiceProfile.json()).pop();
const $ticket = await fetch(
`${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/self-service-profiles/${profile.id}/sso-ticket`,
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
connection_id: body.connection_id,
}),
}
);
const data = await $ticket.json();
return NextResponse.json(data, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
})
);
}
/**
* @example export const GET = handleFetchEnabledConnections();
*/
export function handleFetchEnabledConnections() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const response = await client.organizations.getEnabledConnections({
id: org_id,
});
const { data } = response;
return NextResponse.json(
data.filter((d) =>
// Note: Only strategies support on self-service.
["oidc", "okta", "samlp", "waad", "google-apps", "adfs"].includes(
d.connection.strategy
)
),
{
status: response.status,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error fetching user metadata" },
{ status: 500 }
);
}
}
)
);
}
/**
* @example export const DELETE = handleDeleteConnection();
*/
export function handleDeleteConnection() {
return withRateLimit(
withApiAuthRequired(
async (request: NextRequest, { params }): Promise<NextResponse> => {
try {
const org_id = params?.id as string;
const connection_id = params?.connection_id as string;
await client.organizations.deleteEnabledConnection({
id: org_id,
connectionId: connection_id,
});
await client.connections.delete({ id: connection_id });
return NextResponse.json(
{},
{
status: 200,
}
);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Error deleting connection" },
{ status: 500 }
);
}
}
)
);
}