diff --git a/src/components/authentication/strategies/StrategySettings.tsx b/src/components/authentication/strategies/StrategySettings.tsx index e377645f..03f81f00 100644 --- a/src/components/authentication/strategies/StrategySettings.tsx +++ b/src/components/authentication/strategies/StrategySettings.tsx @@ -67,7 +67,11 @@ export const StrategySettings: React.FC = ({ - + {strategy.name} settings configuration @@ -79,20 +83,22 @@ export const StrategySettings: React.FC = ({ Documentation - -
- {strategy.form ? ( - { - setOpen(false); - }} - /> - ) : ( - 'No settings available' - )} + +
+
+ {strategy.form ? ( + { + setOpen(false); + }} + /> + ) : ( + 'No settings available' + )} +
diff --git a/src/components/authentication/strategies/settingsConfig/oAuth/appleClientsSection.tsx b/src/components/authentication/strategies/settingsConfig/oAuth/appleClientsSection.tsx new file mode 100644 index 00000000..fae2b689 --- /dev/null +++ b/src/components/authentication/strategies/settingsConfig/oAuth/appleClientsSection.tsx @@ -0,0 +1,280 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { InputField } from '@/components/ui/form-inputs/InputField'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; +import { + FormControl, + FormField, + FormItem, + FormLabel, +} from '@/components/ui/form'; +import { SecretTextarea } from '@/components/ui/secret-textarea'; +import { ChevronDown, PlusIcon, Trash2Icon } from 'lucide-react'; + +type CredentialFields = { + clientId?: string; + teamId?: string; + keyId?: string; + privateKey?: string; +}; + +function credentialLabel(fields: CredentialFields) { + const filled = [ + fields.clientId, + fields.teamId, + fields.keyId, + fields.privateKey, + ] + .map(value => value?.trim()) + .filter(Boolean).length; + if (filled === 0) return 'Not configured'; + if (filled === 4) return 'Credentials complete'; + return 'Incomplete'; +} + +function CredentialBadge({ fields }: { fields: CredentialFields }) { + const label = credentialLabel(fields); + return ( + + {label} + + ); +} + +function fieldPath(prefix: string, name: string) { + return prefix ? `${prefix}${name}` : name; +} + +function AppleClientFields({ + prefix = '', + showIdentity, +}: { + prefix?: string; + showIdentity?: boolean; +}) { + const form = useFormContext(); + + return ( +
+ {showIdentity && ( +
+ + +
+ )} +
+ + +
+ + ( + + + Private Key + + + + + + )} + /> + +
+ ); +} + +function AppleClientCollapsible({ + title, + subtitle, + fields, + onRemove, + children, +}: { + title: string; + subtitle?: string; + fields: CredentialFields; + onRemove?: () => void; + children: ReactNode; +}) { + return ( + +
+ + +
+

{title}

+ {subtitle ? ( +

+ {subtitle} +

+ ) : null} +
+
+ + {onRemove ? ( + + ) : null} +
+ {children} +
+ ); +} + +export function AppleDefaultClientSection() { + const form = useFormContext(); + const fields = { + clientId: form.watch('clientId'), + teamId: form.watch('teamId'), + keyId: form.watch('keyId'), + privateKey: form.watch('privateKey'), + }; + + return ( +
+
+

Default client

+

+ Used when OAuth init does not pass{' '} + + oauthClientId + + . +

+
+ + + +
+ ); +} + +const emptyAppleClient = { + id: '', + name: '', + clientId: '', + teamId: '', + keyId: '', + privateKey: '', + redirect_uri: '', +}; + +export function AppleAdditionalClientsSection() { + const form = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'clients', + }); + + return ( +
+
+

App clients

+

+ Additional credential sets selected at login via{' '} + + oauthClientId + + . +

+
+ + {fields.length === 0 ? ( +
+ No additional app clients configured. +
+ ) : ( +
+ {fields.map((field, index) => { + const prefix = `clients.${index}.`; + const name = form.watch(`${prefix}name`) ?? ''; + const oauthClientId = form.watch(`${prefix}id`) ?? ''; + const displayName = name.trim() || `Client ${index + 1}`; + + return ( + remove(index)} + > + + + ); + })} +
+ )} + + +
+ ); +} diff --git a/src/components/authentication/strategies/settingsConfig/oAuth/appleConfig.tsx b/src/components/authentication/strategies/settingsConfig/oAuth/appleConfig.tsx index 8033490f..66b5ba84 100644 --- a/src/components/authentication/strategies/settingsConfig/oAuth/appleConfig.tsx +++ b/src/components/authentication/strategies/settingsConfig/oAuth/appleConfig.tsx @@ -2,74 +2,60 @@ import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { rhfZodResolver } from '@/lib/zod-form'; -import { oauthDefaultConfig } from '@/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig'; +import { oauthProviderSettingsSchema } from '@/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig'; import SwitchField from '@/components/ui/form-inputs/SwitchField'; -import { InputField } from '@/components/ui/form-inputs/InputField'; import { Button } from '@/components/ui/button'; import { StrategyFormProps } from '@/components/authentication/strategies/interface/StrategyFormProps.interface'; import React from 'react'; +import { Form } from '@/components/ui/form'; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, -} from '@/components/ui/form'; -import { SecretTextarea } from '@/components/ui/secret-textarea'; + AppleAdditionalClientsSection, + AppleDefaultClientSection, +} from '@/components/authentication/strategies/settingsConfig/oAuth/appleClientsSection'; + +export const appleClientEntrySchema = z.object({ + id: z.string().default(''), + name: z.string().default(''), + clientId: z.string().default(''), + teamId: z.string().default(''), + keyId: z.string().default(''), + privateKey: z.string().default(''), + redirect_uri: z.string().default(''), +}); type authStrategyFormType = z.infer; -const authStrategySchema = oauthDefaultConfig.merge( - z.object({ - privateKey: z.string().default(''), - teamId: z.string().default(''), - keyId: z.string().default(''), - }) -); +const authStrategySchema = oauthProviderSettingsSchema.extend({ + privateKey: z.string().default(''), + teamId: z.string().default(''), + keyId: z.string().default(''), + clients: z.array(appleClientEntrySchema).default([]), +}); export const AppleConfigForm: React.FC< StrategyFormProps > = ({ data, onSubmit, onCancel }) => { const form = useForm({ resolver: rhfZodResolver(authStrategySchema), - defaultValues: data ? { ...data } : {}, + defaultValues: { ...data, clients: data?.clients ?? [] }, }); const { isSubmitting } = form.formState; return (
-
-
+
+
-
- - -
- - ( - - - Private Key - - - - - - )} - /> -
- -
-
+ + + +
diff --git a/src/components/authentication/strategies/settingsConfig/oAuth/microsoftConfig.tsx b/src/components/authentication/strategies/settingsConfig/oAuth/microsoftConfig.tsx index 8ff185ce..e28c6319 100644 --- a/src/components/authentication/strategies/settingsConfig/oAuth/microsoftConfig.tsx +++ b/src/components/authentication/strategies/settingsConfig/oAuth/microsoftConfig.tsx @@ -2,21 +2,17 @@ import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { rhfZodResolver } from '@/lib/zod-form'; -import { oauthDefaultConfig } from '@/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig'; +import { oauthProviderSettingsSchema } from '@/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig'; import SwitchField from '@/components/ui/form-inputs/SwitchField'; import { InputField } from '@/components/ui/form-inputs/InputField'; import { Button } from '@/components/ui/button'; import { StrategyFormProps } from '@/components/authentication/strategies/interface/StrategyFormProps.interface'; import React from 'react'; import { Form } from '@/components/ui/form'; -import { TextAreaField } from '@/components/ui/form-inputs/TextAreaField'; - type authStrategyFormType = z.infer; -const authStrategySchema = oauthDefaultConfig.merge( - z.object({ - tenantId: z.string().default(''), - }) -); +const authStrategySchema = oauthProviderSettingsSchema.extend({ + tenantId: z.string().default(''), +}); export const MicrosoftConfigForm: React.FC< StrategyFormProps diff --git a/src/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig.tsx b/src/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig.tsx index d4c76608..26c2b984 100644 --- a/src/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig.tsx +++ b/src/components/authentication/strategies/settingsConfig/oAuth/oauthDefaultConfig.tsx @@ -10,7 +10,7 @@ import SwitchField from '@/components/ui/form-inputs/SwitchField'; import { InputField } from '@/components/ui/form-inputs/InputField'; import { Button } from '@/components/ui/button'; -export const oauthDefaultConfig = z.object({ +export const oauthProviderSettingsSchema = z.object({ enabled: z.boolean().default(false), clientId: z.string().default(''), clientSecret: z.string().default(''), @@ -19,7 +19,7 @@ export const oauthDefaultConfig = z.object({ }); type authStrategyFormType = z.infer; -const authStrategySchema = oauthDefaultConfig; +const authStrategySchema = oauthProviderSettingsSchema; export const OauthDefaultConfigForm: React.FC< StrategyFormProps @@ -29,18 +29,19 @@ export const OauthDefaultConfigForm: React.FC< defaultValues: data ? { ...data } : {}, }); const { isSubmitting } = form.formState; + return ( -
-
+
+
-
+
-
- -
-
+ +
diff --git a/src/lib/models/authentication/apple.config.ts b/src/lib/models/authentication/apple.config.ts index 00874d01..5b4f1916 100644 --- a/src/lib/models/authentication/apple.config.ts +++ b/src/lib/models/authentication/apple.config.ts @@ -1,9 +1,20 @@ import { Oauth2BaseConfig } from '@/lib/models/authentication/oauth2Base.config'; +export type AppleOAuthClientConfig = { + id: string; + name?: string; + clientId: string; + privateKey: string; + teamId: string; + keyId: string; + redirect_uri?: string; +}; + export type AppleConfig = { apple: Oauth2BaseConfig & { privateKey: string; teamId: string; keyId: string; + clients?: AppleOAuthClientConfig[]; }; };