diff --git a/README.md b/README.md index 8a10ca6..cdfd27b 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ bun install bun run dev ``` -Create an account using the authentication UI on http://localhost:3000. -The verification code can be found on the dummy SMTP dashboard on http://localhost:4436. - Inside another terminal session we can start the dashboard UI: ```bash diff --git a/authentication/src/app/flow/error/page.tsx b/authentication/src/app/flow/error/page.tsx index fa87dc4..e35626f 100644 --- a/authentication/src/app/flow/error/page.tsx +++ b/authentication/src/app/flow/error/page.tsx @@ -51,7 +51,7 @@ export default function Error() { return ( <> - + An error occurred @@ -61,7 +61,7 @@ export default function Error() {

- - - - ); -} - -export default DynamicForm; diff --git a/dashboard/src/components/forms/IdentityTraitForm.tsx b/dashboard/src/components/forms/IdentityTraitForm.tsx new file mode 100644 index 0000000..8f863d8 --- /dev/null +++ b/dashboard/src/components/forms/IdentityTraitForm.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { generateZodSchema, KratosSchema, KratosSchemaProperties } from '@/lib/forms/identity-form'; +import { useForm, UseFormReturn } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { toast } from 'sonner'; +import { Identity } from '@ory/client'; +import { Checkbox } from '@/components/ui/checkbox'; + +interface IdentityTraitFormProps { + schema: KratosSchema; + identity: Identity; +} + +function renderUiNodes(form: UseFormReturn, properties: KratosSchemaProperties, prefix?: string): any { + + let keyPrefix = prefix ? prefix + '.' : ''; + + return Object.entries(properties).map(([key, value]) => { + if (value.type === 'object') { + return renderUiNodes(form, value.properties!, key); + } else if (value.type === 'boolean') { + return ( + ( + + + {value.title} + + )} + /> + ); + } else { + return ( + ( + + {value.title} + + + + + )} + /> + ); + } + }, + ); +} + +export function IdentityTraitForm({ schema, identity }: IdentityTraitFormProps) { + + const zodIdentitySchema = generateZodSchema(schema); + const form = useForm>({ + defaultValues: identity.traits, + resolver: zodResolver(zodIdentitySchema), + }); + + function onSubmit(values: z.infer) { + toast.message(JSON.stringify(values, null, 4)); + } + + return ( +
+ + { + renderUiNodes(form, schema.properties.traits.properties) + } +
+ + ); +} diff --git a/dashboard/src/components/identity/identity-actions.tsx b/dashboard/src/components/identity/identity-actions.tsx deleted file mode 100644 index 0ddcf32..0000000 --- a/dashboard/src/components/identity/identity-actions.tsx +++ /dev/null @@ -1,223 +0,0 @@ -'use client'; - -import { Identity } from '@ory/client'; -import { Button, buttonVariants } from '@/components/ui/button'; -import { Copy, Key, Link, Trash, UserCheck, UserMinus, UserX } from 'lucide-react'; -import { ConfirmationDialogWrapper } from '@/components/confirmation-dialog-wrapper'; -import { - blockIdentity, - createRecoveryCode, - createRecoveryLink, - deleteIdentity, - deleteIdentitySessions, - unblockIdentity, -} from '@/lib/action/identity'; -import { toast } from 'sonner'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -interface IdentityActionProps { - identity: Identity; -} - -export function IdentityActions({ identity }: IdentityActionProps, -) { - - const router = useRouter(); - - const [dialogVisible, setDialogVisible] = useState(false); - const [dialogLink, setDialogLink] = useState(''); - const [dialogCode, setDialogCode] = useState(undefined); - - return ( - <> - setDialogVisible(value)}> - - - Recovery account - - You created a recovery flow. Provide the user with the following information so they can - access their account again. - - -
- -
- - -
-

- { - dialogCode ? - 'The user will need this link to access the recovery flow.' - : - 'This magic link will authenticate the user automatically' - } -

-
- { - dialogCode ? -
- -
- - -
-

- The user will need to enter this code on the recovery page. -

-
- : - <> - } - - - Close - - -
-
- - { - await createRecoveryCode(identity.id) - .then((response) => { - setDialogLink(response.recovery_link); - setDialogCode(response.recovery_code); - setDialogVisible(true); - }) - .catch(() => toast.error('Creating recovery code failed')); - }} - tooltipContent="Create recovery code" - dialogTitle="Create recovery code" - dialogDescription="Are you sure you want to create a recovery code for this identity?" - dialogButtonSubmit="Create code" - > - - - - { - await createRecoveryLink(identity.id) - .then((response) => { - setDialogLink(response.recovery_link); - setDialogCode(undefined); - setDialogVisible(true); - }) - .catch(() => toast.error('Creating recovery link failed. It is likely magic-links are disabled on your Ory Kratos instance.')); - }} - tooltipContent="Create recovery link" - dialogTitle="Create recovery link" - dialogDescription="Are you sure you want to create a recovery link for this identity?" - dialogButtonSubmit="Create link" - > - - - - { - identity.state === 'active' ? - { - await blockIdentity(identity.id) - .then(() => toast.success('Identity deactivated')) - .catch(() => toast.error('Deactivating identity failed')); - }} - tooltipContent="Deactivate identity" - dialogTitle="Deactivate identity" - dialogDescription="Are you sure you want to deactivate this identity? The user will not be able to sign-in or use any active session until re-activation!" - dialogButtonSubmit="Deactivate" - > - - - : - { - await unblockIdentity(identity.id) - .then(() => toast.success('Identity activated')) - .catch(() => toast.error('Activating identity failed')); - }} - tooltipContent="Activate identity" - dialogTitle="Activate identity" - dialogDescription="Are you sure you want to activate this identity?" - dialogButtonSubmit="Activate" - > - - - } - - { - await deleteIdentitySessions(identity.id) - .then(() => toast.success('All sessions invalidated')) - .catch(() => toast.error('Invalidating all sessions failed')); - }} - tooltipContent="Invalidate all sessions" - dialogTitle="Invalidate all sessions" - dialogDescription="Are you sure you want to invalidate and delete ALL session of this identity? This action is irreversible!" - dialogButtonSubmit="Invalidate sessions" - dialogButtonSubmitProps={{ variant: 'destructive' }} - > - - - - { - await deleteIdentity(identity.id) - .then(() => { - toast.success('Identity deleted'); - router.push('/user'); - }) - .catch(() => toast.error('Deleting identity failed')); - }} - tooltipContent="Delete identity" - dialogTitle="Delete identity" - dialogDescription="Are you sure you want to delete this identity? This action is irreversible!" - dialogButtonSubmit="Delete identity" - dialogButtonSubmitProps={{ variant: 'destructive' }} - > - - - - ); -} \ No newline at end of file diff --git a/dashboard/src/components/identity/identity-credentials.tsx b/dashboard/src/components/identity/identity-credentials.tsx deleted file mode 100644 index f362971..0000000 --- a/dashboard/src/components/identity/identity-credentials.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { ConfirmationDialogWrapper } from '@/components/confirmation-dialog-wrapper'; -import { deleteIdentityCredential } from '@/lib/action/identity'; -import { Button } from '@/components/ui/button'; -import { Trash } from 'lucide-react'; -import { DeleteIdentityCredentialsTypeEnum, Identity } from '@ory/client'; -import { toast } from 'sonner'; - -interface IdentityCredentialsProps { - identity: Identity; -} - -export function IdentityCredentials({ identity }: IdentityCredentialsProps) { - return ( - - - - Type - Value - - - - - { - Object.entries(identity.credentials!).map(([key, value]) => { - return ( - - {key} - {value.identifiers![0]} - - { - Object.values(DeleteIdentityCredentialsTypeEnum).includes(key as DeleteIdentityCredentialsTypeEnum) && - key !== 'password' && key !== 'code' && - ( - { - deleteIdentityCredential({ id: identity.id, type: key as never }) - .then(() => toast.success(`Credential ${key} deleted`)) - .catch(() => toast.error(`Deleting credential ${key} failed`)); - }} - dialogTitle="Delete credential" - dialogDescription={`Are you sure you want to remove the credential of type ${key} from this identity?`} - dialogButtonSubmit={`Delete ${key}`} - dialogButtonSubmitProps={{ variant: 'destructive' }}> - - - ) - } - - - ); - }) - } - -
- ); -} \ No newline at end of file diff --git a/dashboard/src/components/identity/identity-traits.tsx b/dashboard/src/components/identity/identity-traits.tsx deleted file mode 100644 index 1606db4..0000000 --- a/dashboard/src/components/identity/identity-traits.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { KratosSchema, kratosSchemaToZod } from '@/lib/forms/identity-form'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Identity } from '@ory/client'; -import { toast } from 'sonner'; -import DynamicForm from '@/components/dynamic-form'; -import { FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/form'; -import { Textarea } from '@/components/ui/textarea'; -import { zu } from 'zod_utilz'; -import { updateIdentity } from '@/lib/action/identity'; -import { useState } from 'react'; - -interface IdentityTraitFormProps { - schema: KratosSchema; - identity: Identity; -} - -export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) { - - const [currentIdentity, setCurrentIdentity] = useState(identity); - - const generated = kratosSchemaToZod(schema); - const metadata = z.object({ - metadata_public: zu.stringToJSON(), - metadata_admin: zu.stringToJSON(), - }); - - const zodIdentitySchema = generated.merge(metadata); - - const form = useForm>({ - resolver: zodResolver(zodIdentitySchema), - defaultValues: { - ...currentIdentity.traits, - metadata_public: currentIdentity.metadata_public ? - JSON.stringify(currentIdentity.metadata_public) : '{}', - metadata_admin: currentIdentity.metadata_admin ? - JSON.stringify(currentIdentity.metadata_admin) : '{}', - }, - }); - - const onValid = (data: z.infer) => { - - const traits = structuredClone(data); - delete traits['metadata_public']; - delete traits['metadata_admin']; - - updateIdentity({ - id: currentIdentity.id, - body: { - schema_id: currentIdentity.schema_id, - state: currentIdentity.state!, - traits: traits, - metadata_public: data.metadata_public, - metadata_admin: data.metadata_admin, - }, - }) - .then((identity) => { - setCurrentIdentity(identity); - toast.success('Identity updated'); - }) - .catch(() => { - toast.error('Updating identity failed'); - }); - }; - - const onInvalid = (data: z.infer) => { - console.log('data', data); - toast.error('Invalid values'); - }; - - return ( - - ( - - Public Metadata - -