mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-04-19 09:01:18 +00:00
Compare commits
22 commits
46-add-for
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
7961b50adb | |||
d4b453d71c | |||
b4a7c6f396 | |||
e4a62b914c | |||
bf9a3aac3b | |||
50bedbb976 | |||
3151103195 | |||
5494233e59 | |||
85234b4465 | |||
b3be0440d1 | |||
b72a45f39d | |||
![]() |
0da4158d60 | ||
![]() |
f794f7d700 | ||
![]() |
a72ca49271 | ||
![]() |
86412e0133 | ||
![]() |
3693b0b1f9 | ||
![]() |
4f06445869 | ||
![]() |
cbc6b05173 | ||
![]() |
48c14c08a2 | ||
![]() |
7d7782a92c | ||
![]() |
007098ca91 | ||
![]() |
b2ce32a076 |
15 changed files with 11 additions and 1320 deletions
Binary file not shown.
|
@ -14,15 +14,13 @@
|
||||||
"@ory/integrations": "^1.1.5",
|
"@ory/integrations": "^1.1.5",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-hover-card": "^1.1.2",
|
"@radix-ui/react-hover-card": "^1.1.2",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.3",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@serwist/next": "^9.0.0-preview.21",
|
"@serwist/next": "^9.0.0-preview.21",
|
||||||
|
@ -32,7 +30,6 @@
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "1.0.0",
|
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.38.3",
|
"drizzle-orm": "^0.38.3",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { CreateClientForm } from '@/components/forms/client-form';
|
|
||||||
import { createClient } from '@/lib/action/client';
|
|
||||||
import { checkPermission, requireSession } from '@/lib/action/authentication';
|
|
||||||
import { permission, relation } from '@/lib/permission';
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export default async function CreateClientPage() {
|
|
||||||
|
|
||||||
const session = await requireSession();
|
|
||||||
const identityId = session.identity!.id;
|
|
||||||
|
|
||||||
const pmCreateClient = await checkPermission(permission.client.it, relation.create, identityId);
|
|
||||||
if (!pmCreateClient) {
|
|
||||||
return redirect('/client');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-3xl font-bold leading-tight tracking-tight">Create OAuth2 Client</p>
|
|
||||||
<p className="text-lg font-light">
|
|
||||||
Configure your new OAuth2 Client.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<CreateClientForm action={createClient}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,4 @@
|
||||||
import { getOAuth2Api } from '@/ory/sdk/server';
|
import { getOAuth2Api } from '@/ory/sdk/server';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { checkPermission, requireSession } from '@/lib/action/authentication';
|
|
||||||
import { permission, relation } from '@/lib/permission';
|
|
||||||
import InsufficientPermission from '@/components/insufficient-permission';
|
|
||||||
import { ClientDataTable } from '@/app/(inside)/client/data-table';
|
import { ClientDataTable } from '@/app/(inside)/client/data-table';
|
||||||
|
|
||||||
export interface FetchClientPageProps {
|
export interface FetchClientPageProps {
|
||||||
|
@ -34,12 +29,6 @@ function parseTokens(link: string) {
|
||||||
async function fetchClientPage({ pageSize, pageToken }: FetchClientPageProps) {
|
async function fetchClientPage({ pageSize, pageToken }: FetchClientPageProps) {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const session = await requireSession();
|
|
||||||
const allowed = await checkPermission(permission.client.it, relation.access, session.identity!.id);
|
|
||||||
if (!allowed) {
|
|
||||||
throw Error('Unauthorised');
|
|
||||||
}
|
|
||||||
|
|
||||||
const oAuth2Api = await getOAuth2Api();
|
const oAuth2Api = await getOAuth2Api();
|
||||||
const response = await oAuth2Api.listOAuth2Clients({
|
const response = await oAuth2Api.listOAuth2Clients({
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
@ -54,49 +43,24 @@ async function fetchClientPage({ pageSize, pageToken }: FetchClientPageProps) {
|
||||||
|
|
||||||
export default async function ListClientPage() {
|
export default async function ListClientPage() {
|
||||||
|
|
||||||
const session = await requireSession();
|
|
||||||
const identityId = session.identity!.id;
|
|
||||||
|
|
||||||
const pmAccessClient = await checkPermission(permission.client.it, relation.access, identityId);
|
|
||||||
const pmCreateClient = await checkPermission(permission.client.it, relation.create, identityId);
|
|
||||||
|
|
||||||
let pageSize = 100;
|
let pageSize = 100;
|
||||||
let pageToken: string = '00000000-0000-0000-0000-000000000000';
|
let pageToken: string = '00000000-0000-0000-0000-000000000000';
|
||||||
|
|
||||||
const initialFetch = pmAccessClient && await fetchClientPage({ pageSize, pageToken });
|
const initialFetch = await fetchClientPage({ pageSize, pageToken });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="relative">
|
<div>
|
||||||
<p className="text-3xl font-bold leading-tight tracking-tight">OAuth2 Clients</p>
|
<p className="text-3xl font-bold leading-tight tracking-tight">OAuth2 Clients</p>
|
||||||
<p className="text-lg font-light">
|
<p className="text-lg font-light">
|
||||||
See and manage all OAuth2 clients registered with your Ory Hydra instance
|
See and manage all OAuth2 clients registered with your Ory Hydra instance
|
||||||
</p>
|
</p>
|
||||||
{
|
|
||||||
pmCreateClient && (
|
|
||||||
<Button className="absolute bottom-0 right-0" asChild>
|
|
||||||
<Link href="/client/create">
|
|
||||||
Create new client
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
{
|
<ClientDataTable
|
||||||
pmAccessClient ?
|
|
||||||
(
|
|
||||||
initialFetch && <ClientDataTable
|
|
||||||
data={initialFetch.data}
|
data={initialFetch.data}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
pageToken={initialFetch.tokens.get('next')}
|
pageToken={initialFetch.tokens.get('next')}
|
||||||
fetchClientPage={fetchClientPage}/>
|
fetchClientPage={fetchClientPage}/>
|
||||||
)
|
|
||||||
:
|
|
||||||
<InsufficientPermission
|
|
||||||
permission={permission.client.it}
|
|
||||||
relation={relation.access}
|
|
||||||
identityId={identityId}/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,14 +76,3 @@
|
||||||
@apply bg-sidebar text-foreground;
|
@apply bg-sidebar text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,588 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { clientFormSchema } from '@/lib/forms/client-form';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { OAuth2Client } from '@ory/client';
|
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { Minus } from 'lucide-react';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
interface CreateClientFormProps {
|
|
||||||
action: (data: z.infer<typeof clientFormSchema>) => Promise<AxiosResponse<OAuth2Client, any>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateClientForm({ action }: CreateClientFormProps) {
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [redirectUris, setRedirectUris] = useState<string[]>(['']);
|
|
||||||
const [postLogoutRedirectUris, setPostLogoutRedirectUris] = useState<string[]>(['']);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof clientFormSchema>>({
|
|
||||||
resolver: zodResolver(clientFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
client_name: '',
|
|
||||||
scope: '',
|
|
||||||
redirect_uris: [''],
|
|
||||||
skip: false,
|
|
||||||
logo_uri: '',
|
|
||||||
policy_uri: '',
|
|
||||||
tos_uri: '',
|
|
||||||
owner: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
|
||||||
const [createdClient, setCreatedClient] = useState<OAuth2Client>();
|
|
||||||
const handleSubmit = async (data: z.infer<typeof clientFormSchema>) => {
|
|
||||||
await action(data)
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
return response.data;
|
|
||||||
})
|
|
||||||
.then((client) => {
|
|
||||||
setCreatedClient(client);
|
|
||||||
setSuccessDialogOpen(true);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addRedirectUri = () => {
|
|
||||||
setRedirectUris([...redirectUris, '']);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addPostLogoutRedirectUri = () => {
|
|
||||||
setPostLogoutRedirectUris([...postLogoutRedirectUris, '']);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeRedirectUri = (index: number) => {
|
|
||||||
const updatedRedirectUris = redirectUris.filter((_, i) => i !== index);
|
|
||||||
setRedirectUris(updatedRedirectUris);
|
|
||||||
form.setValue('redirect_uris', updatedRedirectUris);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removePostLogoutRedirectUri = (index: number) => {
|
|
||||||
const updatedPostLogoutRedirectUris = postLogoutRedirectUris.filter((_, i) => i !== index);
|
|
||||||
setPostLogoutRedirectUris(postLogoutRedirectUris);
|
|
||||||
form.setValue('post_logout_redirect_uris', updatedPostLogoutRedirectUris);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (index: number, event: any) => {
|
|
||||||
const updatedRedirectUris = [...redirectUris];
|
|
||||||
updatedRedirectUris[index] = event.target.value;
|
|
||||||
setRedirectUris(updatedRedirectUris);
|
|
||||||
form.setValue('redirect_uris', updatedRedirectUris);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePostLogoutInputChange = (index: number, event: any) => {
|
|
||||||
const updatedPostLogoutRedirectUris = [...postLogoutRedirectUris];
|
|
||||||
updatedPostLogoutRedirectUris[index] = event.target.value;
|
|
||||||
setPostLogoutRedirectUris(updatedPostLogoutRedirectUris);
|
|
||||||
form.setValue('post_logout_redirect_uris', updatedPostLogoutRedirectUris);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
createdClient && (
|
|
||||||
<AlertDialog open={successDialogOpen} onOpenChange={() => setSuccessDialogOpen(false)}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Client created</AlertDialogTitle>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
Your client was created successfully. Make sure to safe the client secret!
|
|
||||||
<Input value={createdClient.client_secret} readOnly/>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
Essentials
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="client_name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Client Name</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
The human-readable name of the client to be presented to the end-user during
|
|
||||||
authorization.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="ACME INC SSO" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="scope"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Scopes</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Scope is a string containing a space-separated list of scope values (as
|
|
||||||
described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use
|
|
||||||
when requesting access tokens.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="post:read post:write user:read" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{redirectUris.map((uri, index) => (
|
|
||||||
<div key={index} className="mb-4">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel htmlFor={`redirect_uri-${index}`}>Redirect
|
|
||||||
URI {index + 1}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id={`redirect_uri-${index}`}
|
|
||||||
value={uri}
|
|
||||||
placeholder="https://"
|
|
||||||
className="pr-10"
|
|
||||||
{...form.register(`redirect_uris.${index}`)}
|
|
||||||
onChange={(event) => handleInputChange(index, event)}
|
|
||||||
/>
|
|
||||||
{redirectUris.length > 1 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="icon"
|
|
||||||
variant="destructive"
|
|
||||||
className="absolute inset-y-0 right-0 rounded-l-none"
|
|
||||||
onClick={() => removeRedirectUri(index)}>
|
|
||||||
<Minus/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
{form.formState.errors?.redirect_uris && form.formState.errors.redirect_uris[index] && (
|
|
||||||
<FormMessage>{form.formState.errors.redirect_uris[index].message}</FormMessage>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button type="button" onClick={addRedirectUri}>
|
|
||||||
Add Redirect URI
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
Consent Screen
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="skip"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>
|
|
||||||
Skip consent
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Whether or not the consent screen is skipped for this client
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
!form.getValues('skip') && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="logo_uri"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Logo URI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
A URL string referencing the client's logo.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="policy_uri"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Policy URI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
A URL string pointing to a human-readable privacy policy
|
|
||||||
document
|
|
||||||
for the client that describes how the deployment organization
|
|
||||||
collects, uses, retains, and discloses personal data.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="tos_uri"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Terms URI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
A URL string pointing to a human-readable terms of service
|
|
||||||
document for the client that describes a contractual
|
|
||||||
relationship between the end-user and the client that the
|
|
||||||
end-user accepts when authorizing the client.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="owner"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Owner</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Owner is a string identifying the owner of the OAuth 2.0 Client.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="ACME INC" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
Supported OAuth2 flows
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Configure allowed grant types and response types for this OAuth2 Client.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="grant_types"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Grant types</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
{/* TODO: add multiselect component */}
|
|
||||||
<Input value="TODO: add multiselect component" readOnly disabled/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="response_types"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Response types</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
{/* TODO: add multiselect component */}
|
|
||||||
<Input value="TODO: add multiselect component" readOnly disabled/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Access token type</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input value="opaque" readOnly disabled/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
Client authentication mechanism
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Set the client authentication method for the token endpoint. By default the client
|
|
||||||
credentials must be sent in the body of an HTTP POST. This option can also specify for
|
|
||||||
sending the credentials encoded in the HTTP Authorization header or by using JSON Web
|
|
||||||
Tokens. Specify none for public clients (native apps, mobile apps) which can not have
|
|
||||||
secrets.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="token_endpoint_auth_method"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Authentication method</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select an authentication mechanism"/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="client_secret_post">HTTP Body <span
|
|
||||||
className="text-sm text-gray-500 ml-2">(client_secret_post)</span></SelectItem>
|
|
||||||
<SelectItem value="client_secret_basic">HTTP Basic Authorization<span
|
|
||||||
className="text-sm text-gray-500 ml-2">(client_secret_basic)</span></SelectItem>
|
|
||||||
<SelectItem value="private_key_jwt">JWT Authentication <span
|
|
||||||
className="text-sm text-gray-500 ml-2">(private_key_jwt)</span></SelectItem>
|
|
||||||
<SelectItem value="none">None <span
|
|
||||||
className="text-sm text-gray-500 ml-2">(none)</span></SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
OpenID Connect logout
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Get more information about using front and backchannels here
|
|
||||||
<Button variant="link" className="p-0" asChild>
|
|
||||||
<Link target="_blank"
|
|
||||||
href="https://www.ory.sh/docs/oauth2-oidc/oidc-logout#openid-connect-front-channel-logout-10">
|
|
||||||
documentation
|
|
||||||
</Link>
|
|
||||||
</Button>.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="frontchannel_logout_session_required"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>
|
|
||||||
Frontchannel Logout Session Required
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Boolean value specifying whether the Relay Party (RP) requires that
|
|
||||||
issuer and session ID query parameters be included to identify the RP
|
|
||||||
session with the OpenID provider (OP) when the Frontchannel Logout URI
|
|
||||||
is used. The default value is false.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="frontchannel_logout_uri"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Frontchannel Logout URI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
URL that will cause the Relying Party (RP) to log itself out when rendered
|
|
||||||
in an iframe by the OpenID provider (OP). An issuer query parameter and a
|
|
||||||
session ID query parameter MAY be included by the OpenID provider (OP) to
|
|
||||||
enable the Relying Party (RP) to validate the request and to determine which
|
|
||||||
of the potentially multiple sessions is to be logged out; if either is
|
|
||||||
included, both MUST be.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="backchannel_logout_session_required"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>
|
|
||||||
Backchannel Logout Session Required
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Boolean value specifying whether the Relying Party (RP) requires that a
|
|
||||||
session ID Claim be included in the Logout Token to identify the Relying
|
|
||||||
Party session with the OpenID provider (OP) when the Backchannel Logout
|
|
||||||
URI is used. If omitted, the default value is false.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="backchannel_logout_uri"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Backchannel Logout URI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
URL that will cause the Relying Party (RP) to log itself out when rendered
|
|
||||||
in an iframe by the OpenID provider (OP). An issuer query parameter and a
|
|
||||||
session ID query parameter MAY be included by the OpenID provider (OP) to
|
|
||||||
enable the Relying Party (RP) to validate the request and to determine which
|
|
||||||
of the potentially multiple sessions is to be logged out; if either is
|
|
||||||
included, both MUST be.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="skip_logout_consent"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">
|
|
||||||
Skip logout consent
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Boolean value specifying whether the additional logout consent screen
|
|
||||||
should be skipped.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{postLogoutRedirectUris.map((uri, index) => (
|
|
||||||
<div key={index} className="mb-4">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel htmlFor={`post_logout_redirect_uri-${index}`}>
|
|
||||||
Post Logout Redirect URI {index + 1}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id={`post_logout_redirect_uri-${index}`}
|
|
||||||
value={uri}
|
|
||||||
placeholder="https://"
|
|
||||||
className="pr-10"
|
|
||||||
{...form.register(`post_logout_redirect_uris.${index}`)}
|
|
||||||
onChange={(event) => handlePostLogoutInputChange(index, event)}
|
|
||||||
/>
|
|
||||||
{postLogoutRedirectUris.length > 1 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="icon"
|
|
||||||
variant="destructive"
|
|
||||||
className="absolute inset-y-0 right-0 rounded-l-none"
|
|
||||||
onClick={() => removePostLogoutRedirectUri(index)}>
|
|
||||||
<Minus/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
{form.formState.errors?.post_logout_redirect_uris && form.formState.errors.post_logout_redirect_uris[index] && (
|
|
||||||
<FormMessage>{form.formState.errors.post_logout_redirect_uris[index].message}</FormMessage>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button type="button" onClick={addPostLogoutRedirectUri}>
|
|
||||||
Add Post Logout Redirect URI
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="space-x-2">
|
|
||||||
<Button type="button" variant="outline" onClick={() => {
|
|
||||||
router.back();
|
|
||||||
}}>Cancel</Button>
|
|
||||||
<Button type="submit">Create client</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex flex-col space-y-1.5 pb-6 pt-6 sm:p-6', className)}
|
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
@ -60,7 +60,7 @@ const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('pb-6 sm:p-6 pt-0', className)} {...props} />
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||||
));
|
));
|
||||||
CardContent.displayName = 'CardContent';
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ const CardFooter = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex items-center pb-6 sm:p-6 pt-0', className)}
|
className={cn('flex items-center p-6 pt-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { type DialogProps } from '@radix-ui/react-dialog';
|
|
||||||
import { Command as CommandPrimitive } from 'cmdk';
|
|
||||||
import { Search } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Command.displayName = CommandPrimitive.displayName;
|
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
|
||||||
<Command
|
|
||||||
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50"/>
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
ref={ref}
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn('-mx-1 h-px bg-border', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=\'true\']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
CommandShortcut.displayName = 'CommandShortcut';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator,
|
|
||||||
};
|
|
|
@ -1,123 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal;
|
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close;
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay/>
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
||||||
<X className="h-4 w-4"/>
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
));
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const DialogHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
DialogHeader.displayName = 'DialogHeader';
|
|
||||||
|
|
||||||
const DialogFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
DialogFooter.displayName = 'DialogFooter';
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'text-lg font-semibold leading-none tracking-tight',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
};
|
|
|
@ -1,129 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
|
||||||
import { Command as CommandPrimitive } from 'cmdk';
|
|
||||||
|
|
||||||
type MultiSelectItem = Record<'value' | 'label', string>;
|
|
||||||
|
|
||||||
interface MultiSelectProps {
|
|
||||||
items: MultiSelectItem[];
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credits: https://github.com/mxkaske/mxkaske.dev/blob/main/components/craft/fancy-multi-select.tsx
|
|
||||||
export function MultiSelect({ items, placeholder }: MultiSelectProps) {
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const [selected, setSelected] = React.useState<MultiSelectItem[]>([]);
|
|
||||||
const [inputValue, setInputValue] = React.useState('');
|
|
||||||
|
|
||||||
const handleUnselect = React.useCallback((item: MultiSelectItem) => {
|
|
||||||
setSelected((prev) => prev.filter((s) => s.value !== item.value));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
const input = inputRef.current;
|
|
||||||
if (input) {
|
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
||||||
if (input.value === '') {
|
|
||||||
setSelected((prev) => {
|
|
||||||
const newSelected = [...prev];
|
|
||||||
newSelected.pop();
|
|
||||||
return newSelected;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This is not a default behaviour of the <input /> field
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
input.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectables = items.filter(
|
|
||||||
(item) => !selected.includes(item),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(selectables, selected, inputValue);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Command
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className="overflow-visible bg-transparent"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="group rounded-md border border-input px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{selected.map((item) => {
|
|
||||||
return (
|
|
||||||
<Badge key={item.value} variant="secondary">
|
|
||||||
{item.label}
|
|
||||||
<button
|
|
||||||
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleUnselect(item);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onClick={() => handleUnselect(item)}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3 text-muted-foreground hover:text-foreground"/>
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{/* Avoid having the "Search" Icon */}
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={inputRef}
|
|
||||||
value={inputValue}
|
|
||||||
onValueChange={setInputValue}
|
|
||||||
onBlur={() => setOpen(false)}
|
|
||||||
onFocus={() => setOpen(true)}
|
|
||||||
placeholder={placeholder ?? 'Select an item...'}
|
|
||||||
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative mt-2">
|
|
||||||
<CommandList>
|
|
||||||
{open && selectables.length > 0 ? (
|
|
||||||
<div
|
|
||||||
className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
|
|
||||||
<CommandGroup className="h-full overflow-auto">
|
|
||||||
{selectables.map((item) => {
|
|
||||||
return (
|
|
||||||
<CommandItem
|
|
||||||
key={item.value}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onSelect={(value) => {
|
|
||||||
setInputValue('');
|
|
||||||
setSelected((prev) => [...prev, item]);
|
|
||||||
}}
|
|
||||||
className={'cursor-pointer'}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</CommandItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandGroup>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</CommandList>
|
|
||||||
</div>
|
|
||||||
</Command>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
||||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root;
|
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group;
|
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value;
|
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50"/>
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
));
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-default items-center justify-center py-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4"/>
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
));
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-default items-center justify-center py-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4"/>
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
));
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName;
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
||||||
position === 'popper' &&
|
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton/>
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
'p-1',
|
|
||||||
position === 'popper' &&
|
|
||||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton/>
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
));
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4"/>
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
));
|
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectLabel,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SwitchPrimitives.Root
|
|
||||||
className={cn(
|
|
||||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<SwitchPrimitives.Thumb
|
|
||||||
className={cn(
|
|
||||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SwitchPrimitives.Root>
|
|
||||||
));
|
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
|
||||||
|
|
||||||
export { Switch };
|
|
|
@ -1,23 +0,0 @@
|
||||||
'use server';
|
|
||||||
|
|
||||||
import { clientFormSchema } from '@/lib/forms/client-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getOAuth2Api } from '@/ory/sdk/server';
|
|
||||||
import { checkPermission, requireSession } from '@/lib/action/authentication';
|
|
||||||
import { permission, relation } from '@/lib/permission';
|
|
||||||
|
|
||||||
export async function createClient(
|
|
||||||
formData: z.infer<typeof clientFormSchema>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
const session = await requireSession();
|
|
||||||
const allowed = await checkPermission(permission.client.it, relation.create, session.identity!.id);
|
|
||||||
if (!allowed) {
|
|
||||||
throw Error('Unauthorised');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(session.identity?.traits.email, 'posted form', formData);
|
|
||||||
|
|
||||||
const oauthApi = await getOAuth2Api();
|
|
||||||
return await oauthApi.createOAuth2Client({ oAuth2Client: formData });
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const clientFormSchema = z.object({
|
|
||||||
access_token_strategy: z.string().default('opaque').readonly(),
|
|
||||||
client_name: z.string().min(1, 'Client name is required'),
|
|
||||||
scope: z.string(),
|
|
||||||
redirect_uris: z.array(z.string().url({ message: 'Invalid URL' })).min(1, { message: 'At least one redirect URI is required' }),
|
|
||||||
skip: z.boolean(),
|
|
||||||
logo_uri: z.string().url(),
|
|
||||||
tos_uri: z.string().url(),
|
|
||||||
policy_uri: z.string().url(),
|
|
||||||
owner: z.string().min(1, 'Owner is required'),
|
|
||||||
grant_types: z.array(z.string()),
|
|
||||||
response_types: z.array(z.string()),
|
|
||||||
token_endpoint_auth_method: z.string(),
|
|
||||||
backchannel_logout_session_required: z.boolean().default(false),
|
|
||||||
backchannel_logout_uri: z.string().url(),
|
|
||||||
frontchannel_logout_session_required: z.boolean().default(false),
|
|
||||||
frontchannel_logout_uri: z.string().url(),
|
|
||||||
skip_logout_consent: z.boolean().default(false),
|
|
||||||
post_logout_redirect_uris: z.array(z.string().url({ message: 'Invalid URL' })).min(1, { message: 'At least one redirect URI is required' }),
|
|
||||||
});
|
|
|
@ -13,9 +13,6 @@ export const permission = {
|
||||||
state: 'admin.user.state',
|
state: 'admin.user.state',
|
||||||
trait: 'admin.user.trait',
|
trait: 'admin.user.trait',
|
||||||
},
|
},
|
||||||
client: {
|
|
||||||
it: 'admin.client',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relation = {
|
export const relation = {
|
||||||
|
|
Loading…
Add table
Reference in a new issue