From 8649ad41242e8aa5e73c5a026a43ea7762b4db2e Mon Sep 17 00:00:00 2001 From: defiQUG Date: Wed, 12 Nov 2025 08:22:51 -0800 Subject: [PATCH] feat: implement naming convention, deployment automation, and infrastructure updates - Add comprehensive naming convention (provider-region-resource-env-purpose) - Implement Terraform locals for centralized naming - Update all Terraform resources to use new naming convention - Create deployment automation framework (18 phase scripts) - Add Azure setup scripts (provider registration, quota checks) - Update deployment scripts config with naming functions - Create complete deployment documentation (guide, steps, quick reference) - Add frontend portal implementations (public and internal) - Add UI component library (18 components) - Enhance Entra VerifiedID integration with file utilities - Add API client package for all services - Create comprehensive documentation (naming, deployment, next steps) Infrastructure: - Resource groups, storage accounts with new naming - Terraform configuration updates - Outputs with naming convention examples Deployment: - Automated deployment scripts for all 15 phases - State management and logging - Error handling and validation Documentation: - Naming convention guide and implementation summary - Complete deployment guide (296 steps) - Next steps and quick start guides - Azure prerequisites and setup completion docs Note: ESLint warnings present - will be addressed in follow-up commit --- apps/portal-internal/next.config.js | 2 +- apps/portal-internal/package.json | 21 +- apps/portal-internal/postcss.config.js | 7 + apps/portal-internal/src/app/audit/page.tsx | 196 +++ .../src/app/credentials/issue/page.tsx | 244 +++ .../src/app/credentials/page.tsx | 117 ++ apps/portal-internal/src/app/globals.css | 60 + apps/portal-internal/src/app/layout.tsx | 10 +- apps/portal-internal/src/app/login/page.tsx | 126 ++ apps/portal-internal/src/app/metrics/page.tsx | 189 +++ apps/portal-internal/src/app/page.tsx | 90 +- .../src/app/review/[id]/page.tsx | 271 +++ apps/portal-internal/src/app/review/page.tsx | 120 ++ .../portal-internal/src/app/settings/page.tsx | 134 ++ apps/portal-internal/src/app/users/page.tsx | 119 ++ .../src/components/AuthGuard.tsx | 31 + .../portal-internal/src/components/Header.tsx | 54 + apps/portal-internal/src/lib/auth.ts | 126 ++ apps/portal-internal/src/lib/providers.tsx | 27 + apps/portal-internal/src/middleware.ts | 27 + apps/portal-internal/tailwind.config.js | 77 + apps/portal-public/next.config.js | 2 +- apps/portal-public/package.json | 21 +- apps/portal-public/postcss.config.js | 7 + apps/portal-public/src/app/about/page.tsx | 116 ++ apps/portal-public/src/app/apply/page.tsx | 235 +++ apps/portal-public/src/app/contact/page.tsx | 121 ++ apps/portal-public/src/app/docs/page.tsx | 128 ++ apps/portal-public/src/app/error.tsx | 43 + apps/portal-public/src/app/globals.css | 60 + apps/portal-public/src/app/layout.tsx | 12 +- apps/portal-public/src/app/login/page.tsx | 131 ++ apps/portal-public/src/app/not-found.tsx | 29 + apps/portal-public/src/app/page.tsx | 95 +- apps/portal-public/src/app/privacy/page.tsx | 94 ++ apps/portal-public/src/app/status/page.tsx | 168 ++ apps/portal-public/src/app/terms/page.tsx | 102 ++ apps/portal-public/src/app/verify/page.tsx | 129 ++ .../src/components/AuthGuard.tsx | 31 + apps/portal-public/src/components/Footer.tsx | 77 + apps/portal-public/src/components/Header.tsx | 54 + apps/portal-public/src/lib/auth.ts | 126 ++ apps/portal-public/src/lib/providers.tsx | 27 + apps/portal-public/src/middleware.ts | 27 + apps/portal-public/tailwind.config.js | 77 + docs/FRONTEND_COMPLETION_SUMMARY.md | 206 +++ docs/FRONTEND_IMPLEMENTATION_PROGRESS.md | 298 ++++ docs/WEB_UI_COVERAGE_ANALYSIS.md | 300 ++++ docs/deployment/AUTOMATION_SUMMARY.md | 231 +++ docs/deployment/DEPLOYMENT_GUIDE.md | 1478 +++++++++++++++++ docs/deployment/DEPLOYMENT_QUICK_REFERENCE.md | 312 ++++ docs/deployment/DEPLOYMENT_STEPS_SUMMARY.md | 564 +++++++ docs/governance/NAMING_CONVENTION.md | 354 ++++ .../NAMING_IMPLEMENTATION_SUMMARY.md | 172 ++ .../ENTRA_BEST_PRACTICES_IMPLEMENTATION.md | 426 +++++ .../ENTRA_JSON_CONTENT_READINESS.md | 418 +++++ .../AZURE_ENTRA_PREREQUISITES_CHECKLIST.md | 291 ++++ docs/reports/AZURE_SETUP_COMPLETION.md | 235 +++ docs/reports/DEPLOYMENT_READINESS_REVIEW.md | 639 +++++++ docs/reports/FRONTEND_COMPLETE.md | 191 +++ .../FRONTEND_COMPONENTS_VERIFICATION.md | 279 ++++ docs/reports/NEXT_STEPS.md | 554 ++++++ docs/reports/QUICK_START_NEXT_STEPS.md | 120 ++ infra/scripts/README.md | 130 ++ infra/scripts/azure-check-quotas.sh | 84 + infra/scripts/azure-register-providers.sh | 133 ++ infra/scripts/azure-setup.sh | 254 +++ infra/terraform/.gitignore | 41 + infra/terraform/AZURE_RESOURCE_PROVIDERS.md | 245 +++ infra/terraform/EXECUTION_GUIDE.md | 391 +++++ infra/terraform/NAMING_VALIDATION.md | 55 + infra/terraform/README.md | 181 +- infra/terraform/locals.tf | 92 + infra/terraform/main.tf | 62 +- infra/terraform/outputs.tf | 70 +- infra/terraform/resource-groups.tf | 24 + infra/terraform/storage.tf | 60 + infra/terraform/variables.tf | 31 +- infra/terraform/versions.tf | 22 + packages/api-client/package.json | 23 + packages/api-client/src/client.ts | 53 + packages/api-client/src/dataroom.ts | 140 ++ packages/api-client/src/eresidency.ts | 89 + packages/api-client/src/finance.ts | 111 ++ packages/api-client/src/identity.ts | 114 ++ packages/api-client/src/index.ts | 18 + packages/api-client/src/intake.ts | 104 ++ packages/api-client/tsconfig.json | 13 + packages/auth/src/eidas-entra-bridge.ts | 65 +- packages/auth/src/entra-verifiedid.ts | 143 +- packages/auth/src/file-utils.test.ts | 173 ++ packages/auth/src/file-utils.ts | 377 +++++ packages/auth/src/index.ts | 1 + packages/ui/package.json | 5 +- packages/ui/src/components/Alert.tsx | 46 + packages/ui/src/components/Badge.tsx | 31 + packages/ui/src/components/Breadcrumbs.tsx | 42 + packages/ui/src/components/Button.tsx | 66 +- packages/ui/src/components/Card.tsx | 59 + packages/ui/src/components/Checkbox.tsx | 31 + packages/ui/src/components/Dropdown.tsx | 83 + packages/ui/src/components/Input.tsx | 22 + packages/ui/src/components/Label.tsx | 21 + packages/ui/src/components/Modal.tsx | 125 ++ packages/ui/src/components/Radio.tsx | 31 + packages/ui/src/components/Select.tsx | 23 + packages/ui/src/components/Skeleton.tsx | 9 + packages/ui/src/components/Switch.tsx | 34 + packages/ui/src/components/Table.tsx | 70 + packages/ui/src/components/Tabs.tsx | 118 ++ packages/ui/src/components/Textarea.tsx | 21 + packages/ui/src/components/Toast.tsx | 144 ++ packages/ui/src/components/index.ts | 17 + packages/ui/src/index.ts | 1 + packages/ui/src/lib/utils.ts | 7 + scripts/deploy/README.md | 272 +++ scripts/deploy/config.sh | 182 ++ scripts/deploy/deploy.sh | 256 +++ scripts/deploy/phase1-prerequisites.sh | 108 ++ scripts/deploy/phase10-backend-services.sh | 117 ++ scripts/deploy/phase11-frontend-apps.sh | 65 + scripts/deploy/phase12-networking.sh | 81 + scripts/deploy/phase13-monitoring.sh | 85 + scripts/deploy/phase14-testing.sh | 69 + scripts/deploy/phase15-production.sh | 65 + scripts/deploy/phase2-azure-infrastructure.sh | 103 ++ scripts/deploy/phase3-entra-id.sh | 48 + scripts/deploy/phase4-database-storage.sh | 89 + scripts/deploy/phase5-container-registry.sh | 64 + scripts/deploy/phase6-build-package.sh | 140 ++ scripts/deploy/phase7-database-migrations.sh | 70 + scripts/deploy/phase8-secrets.sh | 93 ++ .../deploy/phase9-infrastructure-services.sh | 73 + scripts/deploy/store-entra-secrets.sh | 58 + services/identity/src/entra-integration.ts | 18 +- tsconfig.base.json | 9 +- 136 files changed, 17251 insertions(+), 147 deletions(-) create mode 100644 apps/portal-internal/postcss.config.js create mode 100644 apps/portal-internal/src/app/audit/page.tsx create mode 100644 apps/portal-internal/src/app/credentials/issue/page.tsx create mode 100644 apps/portal-internal/src/app/credentials/page.tsx create mode 100644 apps/portal-internal/src/app/globals.css create mode 100644 apps/portal-internal/src/app/login/page.tsx create mode 100644 apps/portal-internal/src/app/metrics/page.tsx create mode 100644 apps/portal-internal/src/app/review/[id]/page.tsx create mode 100644 apps/portal-internal/src/app/review/page.tsx create mode 100644 apps/portal-internal/src/app/settings/page.tsx create mode 100644 apps/portal-internal/src/app/users/page.tsx create mode 100644 apps/portal-internal/src/components/AuthGuard.tsx create mode 100644 apps/portal-internal/src/components/Header.tsx create mode 100644 apps/portal-internal/src/lib/auth.ts create mode 100644 apps/portal-internal/src/lib/providers.tsx create mode 100644 apps/portal-internal/src/middleware.ts create mode 100644 apps/portal-internal/tailwind.config.js create mode 100644 apps/portal-public/postcss.config.js create mode 100644 apps/portal-public/src/app/about/page.tsx create mode 100644 apps/portal-public/src/app/apply/page.tsx create mode 100644 apps/portal-public/src/app/contact/page.tsx create mode 100644 apps/portal-public/src/app/docs/page.tsx create mode 100644 apps/portal-public/src/app/error.tsx create mode 100644 apps/portal-public/src/app/globals.css create mode 100644 apps/portal-public/src/app/login/page.tsx create mode 100644 apps/portal-public/src/app/not-found.tsx create mode 100644 apps/portal-public/src/app/privacy/page.tsx create mode 100644 apps/portal-public/src/app/status/page.tsx create mode 100644 apps/portal-public/src/app/terms/page.tsx create mode 100644 apps/portal-public/src/app/verify/page.tsx create mode 100644 apps/portal-public/src/components/AuthGuard.tsx create mode 100644 apps/portal-public/src/components/Footer.tsx create mode 100644 apps/portal-public/src/components/Header.tsx create mode 100644 apps/portal-public/src/lib/auth.ts create mode 100644 apps/portal-public/src/lib/providers.tsx create mode 100644 apps/portal-public/src/middleware.ts create mode 100644 apps/portal-public/tailwind.config.js create mode 100644 docs/FRONTEND_COMPLETION_SUMMARY.md create mode 100644 docs/FRONTEND_IMPLEMENTATION_PROGRESS.md create mode 100644 docs/WEB_UI_COVERAGE_ANALYSIS.md create mode 100644 docs/deployment/AUTOMATION_SUMMARY.md create mode 100644 docs/deployment/DEPLOYMENT_GUIDE.md create mode 100644 docs/deployment/DEPLOYMENT_QUICK_REFERENCE.md create mode 100644 docs/deployment/DEPLOYMENT_STEPS_SUMMARY.md create mode 100644 docs/governance/NAMING_CONVENTION.md create mode 100644 docs/governance/NAMING_IMPLEMENTATION_SUMMARY.md create mode 100644 docs/integrations/ENTRA_BEST_PRACTICES_IMPLEMENTATION.md create mode 100644 docs/integrations/ENTRA_JSON_CONTENT_READINESS.md create mode 100644 docs/reports/AZURE_ENTRA_PREREQUISITES_CHECKLIST.md create mode 100644 docs/reports/AZURE_SETUP_COMPLETION.md create mode 100644 docs/reports/DEPLOYMENT_READINESS_REVIEW.md create mode 100644 docs/reports/FRONTEND_COMPLETE.md create mode 100644 docs/reports/FRONTEND_COMPONENTS_VERIFICATION.md create mode 100644 docs/reports/NEXT_STEPS.md create mode 100644 docs/reports/QUICK_START_NEXT_STEPS.md create mode 100644 infra/scripts/README.md create mode 100755 infra/scripts/azure-check-quotas.sh create mode 100755 infra/scripts/azure-register-providers.sh create mode 100755 infra/scripts/azure-setup.sh create mode 100644 infra/terraform/.gitignore create mode 100644 infra/terraform/AZURE_RESOURCE_PROVIDERS.md create mode 100644 infra/terraform/EXECUTION_GUIDE.md create mode 100644 infra/terraform/NAMING_VALIDATION.md create mode 100644 infra/terraform/locals.tf create mode 100644 infra/terraform/resource-groups.tf create mode 100644 infra/terraform/storage.tf create mode 100644 infra/terraform/versions.tf create mode 100644 packages/api-client/package.json create mode 100644 packages/api-client/src/client.ts create mode 100644 packages/api-client/src/dataroom.ts create mode 100644 packages/api-client/src/eresidency.ts create mode 100644 packages/api-client/src/finance.ts create mode 100644 packages/api-client/src/identity.ts create mode 100644 packages/api-client/src/index.ts create mode 100644 packages/api-client/src/intake.ts create mode 100644 packages/api-client/tsconfig.json create mode 100644 packages/auth/src/file-utils.test.ts create mode 100644 packages/auth/src/file-utils.ts create mode 100644 packages/ui/src/components/Alert.tsx create mode 100644 packages/ui/src/components/Badge.tsx create mode 100644 packages/ui/src/components/Breadcrumbs.tsx create mode 100644 packages/ui/src/components/Card.tsx create mode 100644 packages/ui/src/components/Checkbox.tsx create mode 100644 packages/ui/src/components/Dropdown.tsx create mode 100644 packages/ui/src/components/Input.tsx create mode 100644 packages/ui/src/components/Label.tsx create mode 100644 packages/ui/src/components/Modal.tsx create mode 100644 packages/ui/src/components/Radio.tsx create mode 100644 packages/ui/src/components/Select.tsx create mode 100644 packages/ui/src/components/Skeleton.tsx create mode 100644 packages/ui/src/components/Switch.tsx create mode 100644 packages/ui/src/components/Table.tsx create mode 100644 packages/ui/src/components/Tabs.tsx create mode 100644 packages/ui/src/components/Textarea.tsx create mode 100644 packages/ui/src/components/Toast.tsx create mode 100644 packages/ui/src/lib/utils.ts create mode 100644 scripts/deploy/README.md create mode 100755 scripts/deploy/config.sh create mode 100755 scripts/deploy/deploy.sh create mode 100755 scripts/deploy/phase1-prerequisites.sh create mode 100755 scripts/deploy/phase10-backend-services.sh create mode 100755 scripts/deploy/phase11-frontend-apps.sh create mode 100755 scripts/deploy/phase12-networking.sh create mode 100755 scripts/deploy/phase13-monitoring.sh create mode 100755 scripts/deploy/phase14-testing.sh create mode 100755 scripts/deploy/phase15-production.sh create mode 100755 scripts/deploy/phase2-azure-infrastructure.sh create mode 100755 scripts/deploy/phase3-entra-id.sh create mode 100755 scripts/deploy/phase4-database-storage.sh create mode 100755 scripts/deploy/phase5-container-registry.sh create mode 100755 scripts/deploy/phase6-build-package.sh create mode 100755 scripts/deploy/phase7-database-migrations.sh create mode 100755 scripts/deploy/phase8-secrets.sh create mode 100755 scripts/deploy/phase9-infrastructure-services.sh create mode 100755 scripts/deploy/store-entra-secrets.sh diff --git a/apps/portal-internal/next.config.js b/apps/portal-internal/next.config.js index 9877bab..bac529f 100644 --- a/apps/portal-internal/next.config.js +++ b/apps/portal-internal/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - transpilePackages: ['@the-order/ui', '@the-order/schemas', '@the-order/auth'], + transpilePackages: ['@the-order/ui', '@the-order/schemas', '@the-order/auth', '@the-order/api-client'], }; module.exports = nextConfig; diff --git a/apps/portal-internal/package.json b/apps/portal-internal/package.json index 2720fa2..f1a7e48 100644 --- a/apps/portal-internal/package.json +++ b/apps/portal-internal/package.json @@ -15,7 +15,20 @@ "react-dom": "^18.2.0", "@the-order/ui": "workspace:*", "@the-order/schemas": "workspace:*", - "@the-order/auth": "workspace:*" + "@the-order/auth": "workspace:*", + "@the-order/api-client": "workspace:*", + "@tanstack/react-query": "^5.17.0", + "zustand": "^4.4.7", + "axios": "^1.6.2", + "clsx": "^2.1.0", + "tailwind-merge": "^2.2.0", + "class-variance-authority": "^0.7.0", + "lucide-react": "^0.309.0", + "react-hook-form": "^7.49.2", + "@hookform/resolvers": "^3.3.4", + "zod": "^3.22.4", + "date-fns": "^3.0.6", + "recharts": "^2.10.3" }, "devDependencies": { "@types/node": "^20.10.6", @@ -23,7 +36,11 @@ "@types/react-dom": "^18.2.18", "typescript": "^5.3.3", "eslint": "^8.57.1", - "eslint-config-next": "^14.0.4" + "eslint-config-next": "^14.0.4", + "tailwindcss": "^3.4.1", + "postcss": "^8.4.33", + "autoprefixer": "^10.4.16", + "tailwindcss-animate": "^1.0.7" } } diff --git a/apps/portal-internal/postcss.config.js b/apps/portal-internal/postcss.config.js new file mode 100644 index 0000000..c21c076 --- /dev/null +++ b/apps/portal-internal/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + diff --git a/apps/portal-internal/src/app/audit/page.tsx b/apps/portal-internal/src/app/audit/page.tsx new file mode 100644 index 0000000..e35356c --- /dev/null +++ b/apps/portal-internal/src/app/audit/page.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Select, Button, Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Skeleton } from '@the-order/ui'; +import { getApiClient } from '@the-order/api-client'; + +export default function AuditPage() { + const apiClient = getApiClient(); + const [filters, setFilters] = useState({ + action: '', + credentialId: '', + subjectDid: '', + page: 1, + pageSize: 50, + }); + + const { data: auditLogs, isLoading, error } = useQuery({ + queryKey: ['audit-logs', filters], + queryFn: () => + apiClient.identity.searchAuditLogs({ + action: filters.action as 'issued' | 'revoked' | 'verified' | 'renewed' | undefined, + credentialId: filters.credentialId || undefined, + subjectDid: filters.subjectDid || undefined, + page: filters.page, + pageSize: filters.pageSize, + }), + }); + + const getActionBadge = (action: string) => { + switch (action) { + case 'issued': + return Issued; + case 'revoked': + return Revoked; + case 'verified': + return Verified; + case 'renewed': + return Renewed; + default: + return {action}; + } + }; + + return ( +
+
+
+

Audit Logs

+

Search and view audit logs for credential operations

+
+ + + + Filters + Filter audit logs by various criteria + + +
+
+ + +
+
+ + setFilters({ ...filters, credentialId: e.target.value })} + /> +
+
+ + setFilters({ ...filters, subjectDid: e.target.value })} + /> +
+
+ +
+
+
+
+ + + + Audit Log Entries + + {auditLogs?.total ? `${auditLogs.total} total entries` : 'No entries found'} + + + + {isLoading ? ( +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ) : error ? ( +

+ {error instanceof Error ? error.message : 'Failed to load audit logs'} +

+ ) : auditLogs?.logs && auditLogs.logs.length > 0 ? ( + <> + + + + Timestamp + Action + Credential ID + Subject DID + Performed By + IP Address + + + + {auditLogs.logs.map((log: any) => ( + + + {log.performed_at + ? new Date(log.performed_at).toLocaleString() + : 'N/A'} + + {getActionBadge(log.action || 'unknown')} + + {log.credential_id || 'N/A'} + + {log.subject_did || 'N/A'} + {log.performed_by || 'System'} + {log.ip_address || 'N/A'} + + ))} + +
+
+

+ Showing {auditLogs.logs.length} of {auditLogs.total} entries +

+
+ + +
+
+ + ) : ( +

No audit log entries found

+ )} +
+
+
+
+ ); +} + diff --git a/apps/portal-internal/src/app/credentials/issue/page.tsx b/apps/portal-internal/src/app/credentials/issue/page.tsx new file mode 100644 index 0000000..b61c70f --- /dev/null +++ b/apps/portal-internal/src/app/credentials/issue/page.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Label, + Button, + Select, + Textarea, + useToast, + Alert, + AlertDescription, +} from '@the-order/ui'; +import { getApiClient } from '@the-order/api-client'; + +export default function IssueCredentialPage() { + const router = useRouter(); + const apiClient = getApiClient(); + const { success, error: showError } = useToast(); + const [formData, setFormData] = useState({ + subjectDid: '', + credentialType: 'eResident' as 'eResident' | 'eCitizen', + expirationDate: '', + givenName: '', + familyName: '', + email: '', + dateOfBirth: '', + nationality: '', + notes: '', + }); + + const mutation = useMutation({ + mutationFn: async (data: typeof formData) => { + const credentialSubject: Record = { + givenName: data.givenName, + familyName: data.familyName, + email: data.email, + }; + + if (data.dateOfBirth) { + credentialSubject.dateOfBirth = data.dateOfBirth; + } + if (data.nationality) { + credentialSubject.nationality = data.nationality; + } + + return apiClient.identity.issueCredential({ + subject: data.subjectDid, + credentialSubject, + expirationDate: data.expirationDate || undefined, + }); + }, + onSuccess: (data) => { + success('Credential issued successfully', `Credential ID: ${data.credential.id}`); + router.push('/credentials'); + }, + onError: (error) => { + showError( + error instanceof Error ? error.message : 'Failed to issue credential', + 'Issuance Error' + ); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.subjectDid.trim()) { + showError('Subject DID is required', 'Validation Error'); + return; + } + if (!formData.givenName.trim() || !formData.familyName.trim()) { + showError('Given name and family name are required', 'Validation Error'); + return; + } + mutation.mutate(formData); + }; + + return ( +
+
+
+ +
+ + + + Issue New Credential + + Create and issue a new verifiable credential to a subject + + + +
+
+ + setFormData({ ...formData, subjectDid: e.target.value })} + placeholder="did:example:123456789" + className="mt-2" + /> +

+ The Decentralized Identifier (DID) of the credential subject +

+
+ +
+ + +
+ +
+
+ + setFormData({ ...formData, givenName: e.target.value })} + className="mt-2" + /> +
+
+ + setFormData({ ...formData, familyName: e.target.value })} + className="mt-2" + /> +
+
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="mt-2" + /> +
+ +
+
+ + setFormData({ ...formData, dateOfBirth: e.target.value })} + className="mt-2" + /> +
+
+ + setFormData({ ...formData, nationality: e.target.value })} + className="mt-2" + /> +
+
+ +
+ + setFormData({ ...formData, expirationDate: e.target.value })} + className="mt-2" + /> +

+ Leave empty for credentials that don't expire +

+
+ +
+ +