@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.
- package/dist/{BlockNoteEditor-7OSPCSFW.js → BlockNoteEditor-CKMTHP7C.js} +13 -13
- package/dist/{BlockNoteEditor-7OSPCSFW.js.map → BlockNoteEditor-CKMTHP7C.js.map} +1 -1
- package/dist/{BlockNoteEditor-63GKCJK3.mjs → BlockNoteEditor-EJQLNOLB.mjs} +3 -3
- package/dist/billing/index.js +345 -348
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +6 -9
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-UTPWUC6O.mjs → chunk-JNLXGGHE.mjs} +5790 -4519
- package/dist/chunk-JNLXGGHE.mjs.map +1 -0
- package/dist/{chunk-5U4NJJOF.mjs → chunk-LNBT2YPZ.mjs} +289 -2
- package/dist/chunk-LNBT2YPZ.mjs.map +1 -0
- package/dist/{chunk-NQVPCNRS.js → chunk-O3LLMGP7.js} +290 -3
- package/dist/chunk-O3LLMGP7.js.map +1 -0
- package/dist/{chunk-HIKTQMCR.js → chunk-YYZ2U4WU.js} +7332 -6061
- package/dist/chunk-YYZ2U4WU.js.map +1 -0
- package/dist/client/index.d.mts +96 -1
- package/dist/client/index.d.ts +96 -1
- package/dist/client/index.js +9 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +8 -2
- package/dist/components/index.d.mts +291 -32
- package/dist/components/index.d.ts +291 -32
- package/dist/components/index.js +43 -3
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +58 -18
- package/dist/contexts/index.js +3 -3
- package/dist/contexts/index.mjs +2 -2
- package/dist/core/index.d.mts +108 -1
- package/dist/core/index.d.ts +108 -1
- package/dist/core/index.js +14 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +13 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -1
- package/dist/oauth.interface-DsZ5ecSX.d.mts +119 -0
- package/dist/oauth.interface-vL7za9Bz.d.ts +119 -0
- package/dist/scripts/generate-web-module/templates/components/editor.template.js +11 -13
- package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +13 -26
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.js +59 -76
- package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/field-mapper.js +10 -12
- package/dist/scripts/generate-web-module/transformers/field-mapper.js.map +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/templates/components/editor.template.ts +11 -13
- package/scripts/generate-web-module/templates/components/multi-selector.template.ts +13 -26
- package/scripts/generate-web-module/templates/components/selector.template.ts +59 -76
- package/scripts/generate-web-module/transformers/field-mapper.ts +10 -12
- package/src/client/index.ts +1 -0
- package/src/components/forms/FormCheckbox.tsx +18 -24
- package/src/components/forms/FormDate.tsx +103 -116
- package/src/components/forms/FormDateTime.tsx +122 -130
- package/src/components/forms/FormFieldWrapper.tsx +54 -0
- package/src/components/forms/FormInput.tsx +58 -46
- package/src/components/forms/FormPassword.tsx +17 -24
- package/src/components/forms/FormPlaceAutocomplete.tsx +50 -75
- package/src/components/forms/FormSelect.tsx +29 -35
- package/src/components/forms/FormSlider.tsx +23 -27
- package/src/components/forms/FormSwitch.tsx +12 -14
- package/src/components/forms/FormTextarea.tsx +12 -19
- package/src/components/forms/index.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +3 -0
- package/src/core/registry/ModuleRegistry.ts +2 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +9 -13
- package/src/features/company/components/forms/CompanyConfigurationSecurityForm.tsx +19 -33
- package/src/features/feature/components/forms/FormFeatures.tsx +3 -4
- package/src/features/index.ts +1 -0
- package/src/features/oauth/atoms/index.ts +1 -0
- package/src/features/oauth/atoms/oauth.atoms.ts +131 -0
- package/src/features/oauth/components/OAuthClientCard.tsx +105 -0
- package/src/features/oauth/components/OAuthClientDetail.tsx +269 -0
- package/src/features/oauth/components/OAuthClientForm.tsx +212 -0
- package/src/features/oauth/components/OAuthClientList.tsx +127 -0
- package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +127 -0
- package/src/features/oauth/components/OAuthRedirectUriInput.tsx +152 -0
- package/src/features/oauth/components/OAuthScopeSelector.tsx +123 -0
- package/src/features/oauth/components/consent/OAuthConsentActions.tsx +41 -0
- package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +51 -0
- package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +142 -0
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +72 -0
- package/src/features/oauth/components/consent/index.ts +4 -0
- package/src/features/oauth/components/index.ts +8 -0
- package/src/features/oauth/data/index.ts +2 -0
- package/src/features/oauth/data/oauth.service.ts +191 -0
- package/src/features/oauth/data/oauth.ts +87 -0
- package/src/features/oauth/hooks/index.ts +3 -0
- package/src/features/oauth/hooks/useOAuthClient.ts +161 -0
- package/src/features/oauth/hooks/useOAuthClients.ts +111 -0
- package/src/features/oauth/hooks/useOAuthConsent.ts +125 -0
- package/src/features/oauth/index.ts +6 -0
- package/src/features/oauth/interfaces/index.ts +1 -0
- package/src/features/oauth/interfaces/oauth.interface.ts +175 -0
- package/src/features/oauth/oauth.module.ts +9 -0
- package/src/features/role/components/forms/FormRoles.tsx +40 -51
- package/src/features/user/components/forms/UserMultiSelect.tsx +12 -29
- package/src/features/user/components/forms/UserSelector.tsx +79 -91
- package/src/shadcnui/index.ts +2 -0
- package/src/shadcnui/ui/field.tsx +3 -3
- package/src/shadcnui/ui/form.tsx +17 -134
- package/src/shadcnui/ui/input-group.tsx +4 -4
- package/dist/chunk-5U4NJJOF.mjs.map +0 -1
- package/dist/chunk-HIKTQMCR.js.map +0 -1
- package/dist/chunk-NQVPCNRS.js.map +0 -1
- package/dist/chunk-UTPWUC6O.mjs.map +0 -1
- package/src/components/forms/FormContainerGeneric.tsx +0 -39
- /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
|
-
<
|
|
253
|
-
<
|
|
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
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
</
|
|
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 {
|
|
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
|
-
<
|
|
51
|
+
<FormFieldWrapper
|
|
51
52
|
key={currentField}
|
|
52
|
-
|
|
53
|
+
form={form}
|
|
53
54
|
name={currentField}
|
|
54
|
-
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
<
|
|
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,
|
|
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
|
-
<
|
|
47
|
+
<Label htmlFor={feature.id} className="ml-3 cursor-pointer font-normal">
|
|
48
48
|
{feature.name}
|
|
49
|
-
</
|
|
49
|
+
</Label>
|
|
50
50
|
</div>
|
|
51
51
|
))}
|
|
52
52
|
</div>
|
|
53
53
|
</ScrollArea>
|
|
54
|
-
<FormMessage />
|
|
55
54
|
</div>
|
|
56
55
|
);
|
|
57
56
|
}
|
package/src/features/index.ts
CHANGED
|
@@ -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
|
+
}
|