@carlonicora/nextjs-jsonapi 1.24.2 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/{BlockNoteEditor-7OSPCSFW.js → BlockNoteEditor-CKMTHP7C.js} +13 -13
  2. package/dist/{BlockNoteEditor-7OSPCSFW.js.map → BlockNoteEditor-CKMTHP7C.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-63GKCJK3.mjs → BlockNoteEditor-EJQLNOLB.mjs} +3 -3
  4. package/dist/billing/index.js +345 -348
  5. package/dist/billing/index.js.map +1 -1
  6. package/dist/billing/index.mjs +6 -9
  7. package/dist/billing/index.mjs.map +1 -1
  8. package/dist/{chunk-UTPWUC6O.mjs → chunk-JNLXGGHE.mjs} +5790 -4519
  9. package/dist/chunk-JNLXGGHE.mjs.map +1 -0
  10. package/dist/{chunk-5U4NJJOF.mjs → chunk-LNBT2YPZ.mjs} +289 -2
  11. package/dist/chunk-LNBT2YPZ.mjs.map +1 -0
  12. package/dist/{chunk-NQVPCNRS.js → chunk-O3LLMGP7.js} +290 -3
  13. package/dist/chunk-O3LLMGP7.js.map +1 -0
  14. package/dist/{chunk-HIKTQMCR.js → chunk-YYZ2U4WU.js} +7332 -6061
  15. package/dist/chunk-YYZ2U4WU.js.map +1 -0
  16. package/dist/client/index.d.mts +96 -1
  17. package/dist/client/index.d.ts +96 -1
  18. package/dist/client/index.js +9 -3
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +8 -2
  21. package/dist/components/index.d.mts +291 -32
  22. package/dist/components/index.d.ts +291 -32
  23. package/dist/components/index.js +43 -3
  24. package/dist/components/index.js.map +1 -1
  25. package/dist/components/index.mjs +58 -18
  26. package/dist/contexts/index.js +3 -3
  27. package/dist/contexts/index.mjs +2 -2
  28. package/dist/core/index.d.mts +108 -1
  29. package/dist/core/index.d.ts +108 -1
  30. package/dist/core/index.js +14 -2
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/index.mjs +13 -1
  33. package/dist/index.d.mts +2 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +14 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +13 -1
  38. package/dist/oauth.interface-DsZ5ecSX.d.mts +119 -0
  39. package/dist/oauth.interface-vL7za9Bz.d.ts +119 -0
  40. package/dist/scripts/generate-web-module/templates/components/editor.template.js +11 -13
  41. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
  42. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
  43. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +13 -26
  44. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
  45. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
  46. package/dist/scripts/generate-web-module/templates/components/selector.template.js +59 -76
  47. package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
  48. package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts.map +1 -1
  49. package/dist/scripts/generate-web-module/transformers/field-mapper.js +10 -12
  50. package/dist/scripts/generate-web-module/transformers/field-mapper.js.map +1 -1
  51. package/dist/server/index.js +3 -3
  52. package/dist/server/index.mjs +1 -1
  53. package/package.json +1 -1
  54. package/scripts/generate-web-module/templates/components/editor.template.ts +11 -13
  55. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +13 -26
  56. package/scripts/generate-web-module/templates/components/selector.template.ts +59 -76
  57. package/scripts/generate-web-module/transformers/field-mapper.ts +10 -12
  58. package/src/client/index.ts +1 -0
  59. package/src/components/forms/FormCheckbox.tsx +18 -24
  60. package/src/components/forms/FormDate.tsx +103 -116
  61. package/src/components/forms/FormDateTime.tsx +122 -130
  62. package/src/components/forms/FormFieldWrapper.tsx +54 -0
  63. package/src/components/forms/FormInput.tsx +58 -46
  64. package/src/components/forms/FormPassword.tsx +17 -24
  65. package/src/components/forms/FormPlaceAutocomplete.tsx +50 -75
  66. package/src/components/forms/FormSelect.tsx +29 -35
  67. package/src/components/forms/FormSlider.tsx +23 -27
  68. package/src/components/forms/FormSwitch.tsx +12 -14
  69. package/src/components/forms/FormTextarea.tsx +12 -19
  70. package/src/components/forms/index.ts +1 -1
  71. package/src/components/index.ts +1 -0
  72. package/src/core/index.ts +3 -0
  73. package/src/core/registry/ModuleRegistry.ts +2 -0
  74. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +9 -13
  75. package/src/features/company/components/forms/CompanyConfigurationSecurityForm.tsx +19 -33
  76. package/src/features/feature/components/forms/FormFeatures.tsx +3 -4
  77. package/src/features/index.ts +1 -0
  78. package/src/features/oauth/atoms/index.ts +1 -0
  79. package/src/features/oauth/atoms/oauth.atoms.ts +131 -0
  80. package/src/features/oauth/components/OAuthClientCard.tsx +105 -0
  81. package/src/features/oauth/components/OAuthClientDetail.tsx +269 -0
  82. package/src/features/oauth/components/OAuthClientForm.tsx +212 -0
  83. package/src/features/oauth/components/OAuthClientList.tsx +127 -0
  84. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +127 -0
  85. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +152 -0
  86. package/src/features/oauth/components/OAuthScopeSelector.tsx +123 -0
  87. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +41 -0
  88. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +51 -0
  89. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +142 -0
  90. package/src/features/oauth/components/consent/OAuthScopeList.tsx +72 -0
  91. package/src/features/oauth/components/consent/index.ts +4 -0
  92. package/src/features/oauth/components/index.ts +8 -0
  93. package/src/features/oauth/data/index.ts +2 -0
  94. package/src/features/oauth/data/oauth.service.ts +191 -0
  95. package/src/features/oauth/data/oauth.ts +87 -0
  96. package/src/features/oauth/hooks/index.ts +3 -0
  97. package/src/features/oauth/hooks/useOAuthClient.ts +161 -0
  98. package/src/features/oauth/hooks/useOAuthClients.ts +111 -0
  99. package/src/features/oauth/hooks/useOAuthConsent.ts +125 -0
  100. package/src/features/oauth/index.ts +6 -0
  101. package/src/features/oauth/interfaces/index.ts +1 -0
  102. package/src/features/oauth/interfaces/oauth.interface.ts +175 -0
  103. package/src/features/oauth/oauth.module.ts +9 -0
  104. package/src/features/role/components/forms/FormRoles.tsx +40 -51
  105. package/src/features/user/components/forms/UserMultiSelect.tsx +12 -29
  106. package/src/features/user/components/forms/UserSelector.tsx +79 -91
  107. package/src/shadcnui/index.ts +2 -0
  108. package/src/shadcnui/ui/field.tsx +3 -3
  109. package/src/shadcnui/ui/form.tsx +17 -134
  110. package/src/shadcnui/ui/input-group.tsx +4 -4
  111. package/dist/chunk-5U4NJJOF.mjs.map +0 -1
  112. package/dist/chunk-HIKTQMCR.js.map +0 -1
  113. package/dist/chunk-NQVPCNRS.js.map +0 -1
  114. package/dist/chunk-UTPWUC6O.mjs.map +0 -1
  115. package/src/components/forms/FormContainerGeneric.tsx +0 -39
  116. /package/dist/{BlockNoteEditor-63GKCJK3.mjs.map → BlockNoteEditor-EJQLNOLB.mjs.map} +0 -0
@@ -26,6 +26,8 @@ export interface FoundationModuleDefinitions {
26
26
  StripeProduct: ModuleWithPermissions;
27
27
  StripePrice: ModuleWithPermissions;
28
28
  StripeUsage: ModuleWithPermissions;
29
+ // OAuth modules
30
+ OAuth: ModuleWithPermissions;
29
31
  }
30
32
 
31
33
  // App-specific modules - apps will augment this interface ONLY
@@ -16,10 +16,8 @@ import {
16
16
  DialogHeader,
17
17
  DialogTitle,
18
18
  Form,
19
- FormControl,
20
- FormItem,
21
- FormLabel,
22
19
  Input,
20
+ Label,
23
21
  } from "../../../../../shadcnui";
24
22
  import { StripePriceInterface, StripePriceService } from "../../data";
25
23
 
@@ -249,18 +247,16 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
249
247
  />
250
248
 
251
249
  {/* Features List */}
252
- <FormItem>
253
- <FormLabel>Features (optional)</FormLabel>
250
+ <div className="space-y-2">
251
+ <Label>Features (optional)</Label>
254
252
  <div className="space-y-2">
255
253
  {form.watch("features").map((_, index) => (
256
254
  <div key={index} className="flex gap-2">
257
- <FormControl>
258
- <Input
259
- {...form.register(`features.${index}`)}
260
- placeholder={`Feature ${index + 1}`}
261
- className="flex-1"
262
- />
263
- </FormControl>
255
+ <Input
256
+ {...form.register(`features.${index}`)}
257
+ placeholder={`Feature ${index + 1}`}
258
+ className="flex-1"
259
+ />
264
260
  <Button
265
261
  type="button"
266
262
  variant="outline"
@@ -291,7 +287,7 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
291
287
  Add Feature
292
288
  </Button>
293
289
  </div>
294
- </FormItem>
290
+ </div>
295
291
 
296
292
  <FormCheckbox form={form} id="active" name="Active" />
297
293
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
4
  import { UseFormReturn } from "react-hook-form";
5
- import { Checkbox, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from "../../../../shadcnui";
5
+ import { FormFieldWrapper } from "../../../../components/forms";
6
+ import { Checkbox, Input } from "../../../../shadcnui";
6
7
 
7
8
  type SecurityConfigurationFormProps = {
8
9
  form: UseFormReturn<any>;
@@ -47,44 +48,29 @@ export function CompanyConfigurationSecurityForm({ form }: SecurityConfiguration
47
48
  }
48
49
 
49
50
  return (
50
- <FormField
51
+ <FormFieldWrapper
51
52
  key={currentField}
52
- control={form.control}
53
+ form={form}
53
54
  name={currentField}
54
- render={({ field: formField }) =>
55
+ label={label}
56
+ description={type === "checkbox" ? placeholder : undefined}
57
+ isRequired={isRequired}
58
+ orientation={type === "checkbox" ? "horizontal" : "vertical"}
59
+ >
60
+ {(field) =>
55
61
  type === "checkbox" ? (
56
- <FormItem className="flex items-start space-x-4">
57
- <FormControl>
58
- <Checkbox
59
- id={currentField}
60
- checked={formField.value}
61
- onCheckedChange={(checked) => {
62
- return checked ? formField.onChange(true) : formField.onChange(false);
63
- }}
64
- />
65
- </FormControl>
66
- <div className="grid gap-2">
67
- <FormLabel htmlFor={currentField}>
68
- {label} {isRequired && <span className="text-destructive">*</span>}
69
- </FormLabel>
70
- <p className="text-muted-foreground text-sm">{placeholder}</p>
71
- </div>
72
- <FormLabel></FormLabel>
73
- <FormMessage />
74
- </FormItem>
62
+ <Checkbox
63
+ id={currentField}
64
+ checked={field.value}
65
+ onCheckedChange={(checked) => {
66
+ return checked ? field.onChange(true) : field.onChange(false);
67
+ }}
68
+ />
75
69
  ) : (
76
- <FormItem>
77
- <FormLabel>
78
- {label} {isRequired && <span className="text-destructive">*</span>}
79
- </FormLabel>
80
- <FormControl>
81
- <Input type={type} placeholder={placeholder} {...formField} />
82
- </FormControl>
83
- <FormMessage />
84
- </FormItem>
70
+ <Input type={type} placeholder={placeholder} {...field} />
85
71
  )
86
72
  }
87
- />
73
+ </FormFieldWrapper>
88
74
  );
89
75
  });
90
76
  };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Checkbox, FormLabel, FormMessage, ScrollArea } from "../../../../shadcnui";
3
+ import { Checkbox, Label, ScrollArea } from "../../../../shadcnui";
4
4
  import { FeatureInterface } from "../../data";
5
5
 
6
6
  type FormFeaturesProps = {
@@ -44,14 +44,13 @@ export function FormFeatures({ form, name, features, featureField = "featureIds"
44
44
  toggleFeature(feature, val === true);
45
45
  }}
46
46
  />
47
- <FormLabel htmlFor={feature.id} className="ml-3 cursor-pointer font-normal">
47
+ <Label htmlFor={feature.id} className="ml-3 cursor-pointer font-normal">
48
48
  {feature.name}
49
- </FormLabel>
49
+ </Label>
50
50
  </div>
51
51
  ))}
52
52
  </div>
53
53
  </ScrollArea>
54
- <FormMessage />
55
54
  </div>
56
55
  );
57
56
  }
@@ -17,3 +17,4 @@ export * from "./role";
17
17
  export * from "./s3";
18
18
  export * from "./search";
19
19
  export * from "./user";
20
+ export * from "./oauth";
@@ -0,0 +1 @@
1
+ export * from "./oauth.atoms";
@@ -0,0 +1,131 @@
1
+ import { atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import { OAuthClientInterface } from "../interfaces/oauth.interface";
4
+
5
+ // ==========================================
6
+ // OAUTH CLIENTS STATE
7
+ // ==========================================
8
+
9
+ /**
10
+ * Primary store for OAuth clients
11
+ * Populated by useOAuthClients hook
12
+ */
13
+ export const oauthClientsAtom = atom<OAuthClientInterface[]>([]);
14
+
15
+ /**
16
+ * Loading state for OAuth clients list
17
+ */
18
+ export const oauthClientsLoadingAtom = atom<boolean>(false);
19
+
20
+ /**
21
+ * Error state for OAuth client operations
22
+ */
23
+ export const oauthClientsErrorAtom = atom<Error | null>(null);
24
+
25
+ /**
26
+ * Derived atom family for getting a single client by ID
27
+ * Usage: const client = useAtomValue(oauthClientByIdAtom(clientId))
28
+ *
29
+ * @param clientId - The client ID (not the internal id, but the clientId field)
30
+ * @returns The client if found, undefined otherwise
31
+ */
32
+ export const oauthClientByIdAtom = atomFamily((clientId: string) =>
33
+ atom((get) => {
34
+ const clients = get(oauthClientsAtom);
35
+ return clients.find((c) => c.clientId === clientId || c.id === clientId);
36
+ }),
37
+ );
38
+
39
+ // ==========================================
40
+ // ONE-TIME SECRET DISPLAY STATE
41
+ // ==========================================
42
+
43
+ /**
44
+ * Stores the client secret for one-time display
45
+ * Set after createClient or regenerateSecret, cleared after user acknowledges
46
+ */
47
+ export const oauthNewClientSecretAtom = atom<string | null>(null);
48
+
49
+ /**
50
+ * The client ID associated with the new secret
51
+ * Used to know which client the secret belongs to
52
+ */
53
+ export const oauthNewClientIdAtom = atom<string | null>(null);
54
+
55
+ // ==========================================
56
+ // CONSENT FLOW STATE
57
+ // ==========================================
58
+
59
+ /**
60
+ * Loading state for consent screen data fetch
61
+ */
62
+ export const oauthConsentLoadingAtom = atom<boolean>(false);
63
+
64
+ /**
65
+ * Error state for consent flow
66
+ */
67
+ export const oauthConsentErrorAtom = atom<Error | null>(null);
68
+
69
+ // ==========================================
70
+ // WRITE ATOMS (for actions)
71
+ // ==========================================
72
+
73
+ /**
74
+ * Write atom to set a new client secret and associated client ID
75
+ */
76
+ export const setNewClientSecretAtom = atom(null, (get, set, value: { clientId: string; secret: string } | null) => {
77
+ if (value === null) {
78
+ set(oauthNewClientSecretAtom, null);
79
+ set(oauthNewClientIdAtom, null);
80
+ } else {
81
+ set(oauthNewClientSecretAtom, value.secret);
82
+ set(oauthNewClientIdAtom, value.clientId);
83
+ }
84
+ });
85
+
86
+ /**
87
+ * Write atom to clear the new client secret (after user acknowledges)
88
+ */
89
+ export const clearNewClientSecretAtom = atom(null, (get, set) => {
90
+ set(oauthNewClientSecretAtom, null);
91
+ set(oauthNewClientIdAtom, null);
92
+ });
93
+
94
+ /**
95
+ * Write atom to update the clients list
96
+ */
97
+ export const setOAuthClientsAtom = atom(null, (get, set, clients: OAuthClientInterface[]) => {
98
+ set(oauthClientsAtom, clients);
99
+ });
100
+
101
+ /**
102
+ * Write atom to add a client to the list
103
+ */
104
+ export const addOAuthClientAtom = atom(null, (get, set, client: OAuthClientInterface) => {
105
+ const clients = get(oauthClientsAtom);
106
+ set(oauthClientsAtom, [...clients, client]);
107
+ });
108
+
109
+ /**
110
+ * Write atom to update a client in the list
111
+ */
112
+ export const updateOAuthClientAtom = atom(null, (get, set, updatedClient: OAuthClientInterface) => {
113
+ const clients = get(oauthClientsAtom);
114
+ const index = clients.findIndex((c) => c.id === updatedClient.id || c.clientId === updatedClient.clientId);
115
+ if (index !== -1) {
116
+ const newClients = [...clients];
117
+ newClients[index] = updatedClient;
118
+ set(oauthClientsAtom, newClients);
119
+ }
120
+ });
121
+
122
+ /**
123
+ * Write atom to remove a client from the list
124
+ */
125
+ export const removeOAuthClientAtom = atom(null, (get, set, clientId: string) => {
126
+ const clients = get(oauthClientsAtom);
127
+ set(
128
+ oauthClientsAtom,
129
+ clients.filter((c) => c.id !== clientId && c.clientId !== clientId),
130
+ );
131
+ });
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { formatDistanceToNow } from "date-fns";
4
+ import { Key, MoreVertical, Pencil, Trash2 } from "lucide-react";
5
+ import {
6
+ Badge,
7
+ Button,
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuTrigger,
17
+ } from "../../../shadcnui";
18
+ import { OAuthClientInterface } from "../interfaces/oauth.interface";
19
+
20
+ export interface OAuthClientCardProps {
21
+ /** The OAuth client to display */
22
+ client: OAuthClientInterface;
23
+ /** Called when card is clicked */
24
+ onClick?: () => void;
25
+ /** Called when edit is clicked */
26
+ onEdit?: () => void;
27
+ /** Called when delete is clicked */
28
+ onDelete?: () => void;
29
+ }
30
+
31
+ /**
32
+ * Card component for displaying an OAuth client in a list
33
+ */
34
+ export function OAuthClientCard({
35
+ client,
36
+ onClick,
37
+ onEdit,
38
+ onDelete,
39
+ }: OAuthClientCardProps) {
40
+ // Truncate client ID for display
41
+ const truncatedId = client.clientId.length > 12
42
+ ? `${client.clientId.slice(0, 8)}...${client.clientId.slice(-4)}`
43
+ : client.clientId;
44
+
45
+ const createdAgo = client.createdAt
46
+ ? formatDistanceToNow(new Date(client.createdAt), { addSuffix: true })
47
+ : "Unknown";
48
+
49
+ return (
50
+ <Card
51
+ className={`cursor-pointer transition-colors hover:bg-accent/50 ${!client.isActive ? "opacity-60" : ""}`}
52
+ onClick={onClick}
53
+ >
54
+ <CardHeader className="pb-2">
55
+ <div className="flex items-start justify-between">
56
+ <div className="flex items-center gap-2">
57
+ <Key className="h-5 w-5 text-muted-foreground" />
58
+ <CardTitle className="text-lg">{client.name}</CardTitle>
59
+ </div>
60
+ <div className="flex items-center gap-2">
61
+ <Badge variant={client.isActive ? "default" : "secondary"}>
62
+ {client.isActive ? "Active" : "Inactive"}
63
+ </Badge>
64
+ {(onEdit || onDelete) && (
65
+ <DropdownMenu>
66
+ <DropdownMenuTrigger onClick={(e) => e.stopPropagation()}>
67
+ <Button render={<div />} nativeButton={false} variant="ghost" size="icon" className="h-8 w-8">
68
+ <MoreVertical className="h-4 w-4" />
69
+ </Button>
70
+ </DropdownMenuTrigger>
71
+ <DropdownMenuContent align="end">
72
+ {onEdit && (
73
+ <DropdownMenuItem onClick={(e) => { e.stopPropagation(); onEdit(); }}>
74
+ <Pencil className="h-4 w-4 mr-2" />
75
+ Edit
76
+ </DropdownMenuItem>
77
+ )}
78
+ {onDelete && (
79
+ <DropdownMenuItem
80
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
81
+ className="text-destructive"
82
+ >
83
+ <Trash2 className="h-4 w-4 mr-2" />
84
+ Delete
85
+ </DropdownMenuItem>
86
+ )}
87
+ </DropdownMenuContent>
88
+ </DropdownMenu>
89
+ )}
90
+ </div>
91
+ </div>
92
+ {client.description && (
93
+ <CardDescription className="line-clamp-2">{client.description}</CardDescription>
94
+ )}
95
+ </CardHeader>
96
+ <CardContent>
97
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
98
+ <span className="font-mono">{truncatedId}</span>
99
+ <span>Created {createdAgo}</span>
100
+ <span>{client.isConfidential ? "Confidential" : "Public"}</span>
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+ );
105
+ }
@@ -0,0 +1,269 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import { format } from "date-fns";
5
+ import { Copy, Check, RefreshCw, Pencil, Trash2, ExternalLink } from "lucide-react";
6
+ import {
7
+ Badge,
8
+ Button,
9
+ Card,
10
+ CardContent,
11
+ CardDescription,
12
+ CardHeader,
13
+ CardTitle,
14
+ Input,
15
+ Label,
16
+ Separator,
17
+ AlertDialog,
18
+ AlertDialogAction,
19
+ AlertDialogCancel,
20
+ AlertDialogContent,
21
+ AlertDialogDescription,
22
+ AlertDialogFooter,
23
+ AlertDialogHeader,
24
+ AlertDialogTitle,
25
+ } from "../../../shadcnui";
26
+ import { OAuthClientInterface, OAUTH_SCOPE_DISPLAY } from "../interfaces/oauth.interface";
27
+
28
+ export interface OAuthClientDetailProps {
29
+ /** The OAuth client to display */
30
+ client: OAuthClientInterface;
31
+ /** Whether data is loading */
32
+ isLoading?: boolean;
33
+ /** Called when edit is clicked */
34
+ onEdit?: () => void;
35
+ /** Called when delete is clicked */
36
+ onDelete?: () => Promise<void>;
37
+ /** Called when regenerate secret is clicked */
38
+ onRegenerateSecret?: () => Promise<void>;
39
+ }
40
+
41
+ /**
42
+ * Detailed view of an OAuth client
43
+ */
44
+ export function OAuthClientDetail({
45
+ client,
46
+ isLoading = false,
47
+ onEdit,
48
+ onDelete,
49
+ onRegenerateSecret,
50
+ }: OAuthClientDetailProps) {
51
+ const [copiedField, setCopiedField] = useState<string | null>(null);
52
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
53
+ const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false);
54
+ const [isDeleting, setIsDeleting] = useState(false);
55
+ const [isRegenerating, setIsRegenerating] = useState(false);
56
+
57
+ const copyToClipboard = useCallback(async (text: string, field: string) => {
58
+ try {
59
+ await navigator.clipboard.writeText(text);
60
+ setCopiedField(field);
61
+ setTimeout(() => setCopiedField(null), 2000);
62
+ } catch (err) {
63
+ console.error("Failed to copy:", err);
64
+ }
65
+ }, []);
66
+
67
+ const handleDelete = useCallback(async () => {
68
+ if (!onDelete) return;
69
+ setIsDeleting(true);
70
+ try {
71
+ await onDelete();
72
+ } finally {
73
+ setIsDeleting(false);
74
+ setShowDeleteConfirm(false);
75
+ }
76
+ }, [onDelete]);
77
+
78
+ const handleRegenerateSecret = useCallback(async () => {
79
+ if (!onRegenerateSecret) return;
80
+ setIsRegenerating(true);
81
+ try {
82
+ await onRegenerateSecret();
83
+ } finally {
84
+ setIsRegenerating(false);
85
+ setShowRegenerateConfirm(false);
86
+ }
87
+ }, [onRegenerateSecret]);
88
+
89
+ const createdDate = client.createdAt
90
+ ? format(new Date(client.createdAt), "MMMM d, yyyy")
91
+ : "Unknown";
92
+
93
+ return (
94
+ <>
95
+ <Card>
96
+ <CardHeader>
97
+ <div className="flex items-start justify-between">
98
+ <div>
99
+ <CardTitle className="text-2xl">{client.name}</CardTitle>
100
+ {client.description && (
101
+ <CardDescription className="mt-1">{client.description}</CardDescription>
102
+ )}
103
+ </div>
104
+ <div className="flex items-center gap-2">
105
+ <Badge variant={client.isActive ? "default" : "secondary"}>
106
+ {client.isActive ? "Active" : "Inactive"}
107
+ </Badge>
108
+ <Badge variant="outline">
109
+ {client.isConfidential ? "Confidential" : "Public"}
110
+ </Badge>
111
+ </div>
112
+ </div>
113
+ </CardHeader>
114
+ <CardContent className="space-y-6">
115
+ {/* Client ID */}
116
+ <div className="space-y-2">
117
+ <Label>Client ID</Label>
118
+ <div className="flex gap-2">
119
+ <Input value={client.clientId} readOnly className="font-mono" />
120
+ <Button
121
+ variant="outline"
122
+ size="icon"
123
+ onClick={() => copyToClipboard(client.clientId, "clientId")}
124
+ title="Copy Client ID"
125
+ >
126
+ {copiedField === "clientId" ? (
127
+ <Check className="h-4 w-4 text-green-600" />
128
+ ) : (
129
+ <Copy className="h-4 w-4" />
130
+ )}
131
+ </Button>
132
+ </div>
133
+ </div>
134
+
135
+ {/* Client Secret */}
136
+ <div className="space-y-2">
137
+ <Label>Client Secret</Label>
138
+ <div className="flex gap-2">
139
+ <Input value="••••••••••••••••••••••••••••••••" readOnly className="font-mono" />
140
+ {onRegenerateSecret && (
141
+ <Button
142
+ variant="outline"
143
+ size="icon"
144
+ onClick={() => setShowRegenerateConfirm(true)}
145
+ title="Regenerate Secret"
146
+ disabled={isLoading}
147
+ >
148
+ <RefreshCw className="h-4 w-4" />
149
+ </Button>
150
+ )}
151
+ </div>
152
+ <p className="text-xs text-muted-foreground">
153
+ Regenerating will invalidate the current secret and all existing tokens.
154
+ </p>
155
+ </div>
156
+
157
+ <Separator />
158
+
159
+ {/* Redirect URIs */}
160
+ <div className="space-y-2">
161
+ <Label>Redirect URIs</Label>
162
+ <ul className="space-y-1">
163
+ {client.redirectUris.map((uri, index) => (
164
+ <li key={index} className="flex items-center gap-2 text-sm font-mono">
165
+ <ExternalLink className="h-3 w-3 text-muted-foreground" />
166
+ {uri}
167
+ </li>
168
+ ))}
169
+ </ul>
170
+ </div>
171
+
172
+ {/* Scopes */}
173
+ <div className="space-y-2">
174
+ <Label>Allowed Scopes</Label>
175
+ <div className="flex flex-wrap gap-2">
176
+ {client.allowedScopes.map((scope) => (
177
+ <Badge key={scope} variant="secondary">
178
+ {OAUTH_SCOPE_DISPLAY[scope]?.name || scope}
179
+ </Badge>
180
+ ))}
181
+ </div>
182
+ </div>
183
+
184
+ {/* Grant Types */}
185
+ <div className="space-y-2">
186
+ <Label>Grant Types</Label>
187
+ <div className="flex flex-wrap gap-2">
188
+ {client.allowedGrantTypes.map((grant) => (
189
+ <Badge key={grant} variant="outline">
190
+ {grant.replace(/_/g, " ")}
191
+ </Badge>
192
+ ))}
193
+ </div>
194
+ </div>
195
+
196
+ <Separator />
197
+
198
+ {/* Metadata */}
199
+ <div className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
200
+ <span>Created: {createdDate}</span>
201
+ </div>
202
+
203
+ {/* Actions */}
204
+ <div className="flex gap-3 pt-4">
205
+ {onEdit && (
206
+ <Button variant="outline" onClick={onEdit} disabled={isLoading}>
207
+ <Pencil className="h-4 w-4 mr-2" />
208
+ Edit
209
+ </Button>
210
+ )}
211
+ {onDelete && (
212
+ <Button
213
+ variant="destructive"
214
+ onClick={() => setShowDeleteConfirm(true)}
215
+ disabled={isLoading}
216
+ >
217
+ <Trash2 className="h-4 w-4 mr-2" />
218
+ Delete
219
+ </Button>
220
+ )}
221
+ </div>
222
+ </CardContent>
223
+ </Card>
224
+
225
+ {/* Delete Confirmation */}
226
+ <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
227
+ <AlertDialogContent>
228
+ <AlertDialogHeader>
229
+ <AlertDialogTitle>Delete OAuth Application?</AlertDialogTitle>
230
+ <AlertDialogDescription>
231
+ This will permanently delete "{client.name}" and revoke all access tokens.
232
+ This action cannot be undone.
233
+ </AlertDialogDescription>
234
+ </AlertDialogHeader>
235
+ <AlertDialogFooter>
236
+ <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
237
+ <AlertDialogAction
238
+ onClick={handleDelete}
239
+ disabled={isDeleting}
240
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
241
+ >
242
+ {isDeleting ? "Deleting..." : "Delete"}
243
+ </AlertDialogAction>
244
+ </AlertDialogFooter>
245
+ </AlertDialogContent>
246
+ </AlertDialog>
247
+
248
+ {/* Regenerate Confirmation */}
249
+ <AlertDialog open={showRegenerateConfirm} onOpenChange={setShowRegenerateConfirm}>
250
+ <AlertDialogContent>
251
+ <AlertDialogHeader>
252
+ <AlertDialogTitle>Regenerate Client Secret?</AlertDialogTitle>
253
+ <AlertDialogDescription>
254
+ This will generate a new client secret and invalidate the old one.
255
+ All existing tokens will be revoked. You will need to update your application
256
+ with the new secret.
257
+ </AlertDialogDescription>
258
+ </AlertDialogHeader>
259
+ <AlertDialogFooter>
260
+ <AlertDialogCancel disabled={isRegenerating}>Cancel</AlertDialogCancel>
261
+ <AlertDialogAction onClick={handleRegenerateSecret} disabled={isRegenerating}>
262
+ {isRegenerating ? "Regenerating..." : "Regenerate"}
263
+ </AlertDialogAction>
264
+ </AlertDialogFooter>
265
+ </AlertDialogContent>
266
+ </AlertDialog>
267
+ </>
268
+ );
269
+ }