@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Input,
|
|
7
|
+
Label,
|
|
8
|
+
Textarea,
|
|
9
|
+
RadioGroup,
|
|
10
|
+
RadioGroupItem,
|
|
11
|
+
Card,
|
|
12
|
+
CardContent,
|
|
13
|
+
CardDescription,
|
|
14
|
+
CardFooter,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle,
|
|
17
|
+
} from "../../../shadcnui";
|
|
18
|
+
import { OAuthRedirectUriInput } from "./OAuthRedirectUriInput";
|
|
19
|
+
import { OAuthScopeSelector } from "./OAuthScopeSelector";
|
|
20
|
+
import { OAuthClientCreateRequest, OAuthClientInterface, DEFAULT_GRANT_TYPES } from "../interfaces/oauth.interface";
|
|
21
|
+
|
|
22
|
+
export interface OAuthClientFormProps {
|
|
23
|
+
/** Existing client for edit mode (undefined = create mode) */
|
|
24
|
+
client?: OAuthClientInterface;
|
|
25
|
+
/** Called on form submit */
|
|
26
|
+
onSubmit: (data: OAuthClientCreateRequest) => Promise<void>;
|
|
27
|
+
/** Called on cancel */
|
|
28
|
+
onCancel: () => void;
|
|
29
|
+
/** Whether form is submitting */
|
|
30
|
+
isLoading?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FormState {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
redirectUris: string[];
|
|
37
|
+
allowedScopes: string[];
|
|
38
|
+
isConfidential: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FormErrors {
|
|
42
|
+
name?: string;
|
|
43
|
+
redirectUris?: string;
|
|
44
|
+
allowedScopes?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Form for creating or editing an OAuth client
|
|
49
|
+
*/
|
|
50
|
+
export function OAuthClientForm({
|
|
51
|
+
client,
|
|
52
|
+
onSubmit,
|
|
53
|
+
onCancel,
|
|
54
|
+
isLoading = false,
|
|
55
|
+
}: OAuthClientFormProps) {
|
|
56
|
+
const isEditMode = !!client;
|
|
57
|
+
|
|
58
|
+
const [formState, setFormState] = useState<FormState>({
|
|
59
|
+
name: client?.name || "",
|
|
60
|
+
description: client?.description || "",
|
|
61
|
+
redirectUris: client?.redirectUris?.length ? client.redirectUris : [""],
|
|
62
|
+
allowedScopes: client?.allowedScopes || [],
|
|
63
|
+
isConfidential: client?.isConfidential ?? true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [errors, setErrors] = useState<FormErrors>({});
|
|
67
|
+
|
|
68
|
+
const validate = useCallback((): boolean => {
|
|
69
|
+
const newErrors: FormErrors = {};
|
|
70
|
+
|
|
71
|
+
if (!formState.name.trim()) {
|
|
72
|
+
newErrors.name = "Application name is required";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const validUris = formState.redirectUris.filter((uri) => uri.trim());
|
|
76
|
+
if (validUris.length === 0) {
|
|
77
|
+
newErrors.redirectUris = "At least one redirect URI is required";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (formState.allowedScopes.length === 0) {
|
|
81
|
+
newErrors.allowedScopes = "At least one scope must be selected";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setErrors(newErrors);
|
|
85
|
+
return Object.keys(newErrors).length === 0;
|
|
86
|
+
}, [formState]);
|
|
87
|
+
|
|
88
|
+
const handleSubmit = useCallback(
|
|
89
|
+
async (e: React.FormEvent) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
|
|
92
|
+
if (!validate()) return;
|
|
93
|
+
|
|
94
|
+
const data: OAuthClientCreateRequest = {
|
|
95
|
+
name: formState.name.trim(),
|
|
96
|
+
description: formState.description.trim() || undefined,
|
|
97
|
+
redirectUris: formState.redirectUris.filter((uri) => uri.trim()),
|
|
98
|
+
allowedScopes: formState.allowedScopes,
|
|
99
|
+
allowedGrantTypes: DEFAULT_GRANT_TYPES,
|
|
100
|
+
isConfidential: formState.isConfidential,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await onSubmit(data);
|
|
104
|
+
},
|
|
105
|
+
[formState, validate, onSubmit]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Card>
|
|
110
|
+
<form onSubmit={handleSubmit}>
|
|
111
|
+
<CardHeader>
|
|
112
|
+
<CardTitle>{isEditMode ? "Edit Application" : "Create OAuth Application"}</CardTitle>
|
|
113
|
+
<CardDescription>
|
|
114
|
+
{isEditMode
|
|
115
|
+
? "Update your OAuth application settings."
|
|
116
|
+
: "Register a new application to access the API."}
|
|
117
|
+
</CardDescription>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
<CardContent className="space-y-6">
|
|
120
|
+
{/* Name */}
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<Label htmlFor="name">Application Name *</Label>
|
|
123
|
+
<Input
|
|
124
|
+
id="name"
|
|
125
|
+
value={formState.name}
|
|
126
|
+
onChange={(e) => setFormState((s) => ({ ...s, name: e.target.value }))}
|
|
127
|
+
placeholder="My Lightroom Plugin"
|
|
128
|
+
disabled={isLoading}
|
|
129
|
+
className={errors.name ? "border-destructive" : ""}
|
|
130
|
+
/>
|
|
131
|
+
{errors.name && <p className="text-sm text-destructive">{errors.name}</p>}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Description */}
|
|
135
|
+
<div className="space-y-2">
|
|
136
|
+
<Label htmlFor="description">Description</Label>
|
|
137
|
+
<Textarea
|
|
138
|
+
id="description"
|
|
139
|
+
value={formState.description}
|
|
140
|
+
onChange={(e) => setFormState((s) => ({ ...s, description: e.target.value }))}
|
|
141
|
+
placeholder="A brief description of your application"
|
|
142
|
+
disabled={isLoading}
|
|
143
|
+
rows={3}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Redirect URIs */}
|
|
148
|
+
<OAuthRedirectUriInput
|
|
149
|
+
value={formState.redirectUris}
|
|
150
|
+
onChange={(uris) => setFormState((s) => ({ ...s, redirectUris: uris }))}
|
|
151
|
+
error={errors.redirectUris}
|
|
152
|
+
disabled={isLoading}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{/* Scopes */}
|
|
156
|
+
<OAuthScopeSelector
|
|
157
|
+
value={formState.allowedScopes}
|
|
158
|
+
onChange={(scopes) => setFormState((s) => ({ ...s, allowedScopes: scopes }))}
|
|
159
|
+
error={errors.allowedScopes}
|
|
160
|
+
disabled={isLoading}
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{/* Client Type */}
|
|
164
|
+
<div className="space-y-3">
|
|
165
|
+
<Label>Client Type</Label>
|
|
166
|
+
<RadioGroup
|
|
167
|
+
value={formState.isConfidential ? "confidential" : "public"}
|
|
168
|
+
onValueChange={(v) => setFormState((s) => ({ ...s, isConfidential: v === "confidential" }))}
|
|
169
|
+
disabled={isLoading || isEditMode}
|
|
170
|
+
>
|
|
171
|
+
<div className="flex items-start space-x-3 p-3 rounded-md border">
|
|
172
|
+
<RadioGroupItem value="confidential" id="confidential" className="mt-1" />
|
|
173
|
+
<div>
|
|
174
|
+
<Label htmlFor="confidential" className="font-medium cursor-pointer">
|
|
175
|
+
Confidential
|
|
176
|
+
</Label>
|
|
177
|
+
<p className="text-sm text-muted-foreground">
|
|
178
|
+
Server-side application that can securely store the client secret.
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div className="flex items-start space-x-3 p-3 rounded-md border">
|
|
183
|
+
<RadioGroupItem value="public" id="public" className="mt-1" />
|
|
184
|
+
<div>
|
|
185
|
+
<Label htmlFor="public" className="font-medium cursor-pointer">
|
|
186
|
+
Public
|
|
187
|
+
</Label>
|
|
188
|
+
<p className="text-sm text-muted-foreground">
|
|
189
|
+
Mobile or desktop application. Requires PKCE for authorization.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</RadioGroup>
|
|
194
|
+
{isEditMode && (
|
|
195
|
+
<p className="text-sm text-muted-foreground">
|
|
196
|
+
Client type cannot be changed after creation.
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</CardContent>
|
|
201
|
+
<CardFooter className="flex justify-end gap-3">
|
|
202
|
+
<Button type="button" variant="outline" onClick={onCancel} disabled={isLoading}>
|
|
203
|
+
Cancel
|
|
204
|
+
</Button>
|
|
205
|
+
<Button type="submit" disabled={isLoading}>
|
|
206
|
+
{isLoading ? "Saving..." : isEditMode ? "Save Changes" : "Create Application"}
|
|
207
|
+
</Button>
|
|
208
|
+
</CardFooter>
|
|
209
|
+
</form>
|
|
210
|
+
</Card>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plus, Key } from "lucide-react";
|
|
4
|
+
import { Button, Skeleton } from "../../../shadcnui";
|
|
5
|
+
import { OAuthClientCard } from "./OAuthClientCard";
|
|
6
|
+
import { OAuthClientInterface } from "../interfaces/oauth.interface";
|
|
7
|
+
|
|
8
|
+
export interface OAuthClientListProps {
|
|
9
|
+
/** List of OAuth clients */
|
|
10
|
+
clients: OAuthClientInterface[];
|
|
11
|
+
/** Whether list is loading */
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
/** Error to display */
|
|
14
|
+
error?: Error | null;
|
|
15
|
+
/** Called when a client is clicked */
|
|
16
|
+
onClientClick?: (client: OAuthClientInterface) => void;
|
|
17
|
+
/** Called when create button is clicked */
|
|
18
|
+
onCreateClick?: () => void;
|
|
19
|
+
/** Called when edit is clicked on a client */
|
|
20
|
+
onEditClick?: (client: OAuthClientInterface) => void;
|
|
21
|
+
/** Called when delete is clicked on a client */
|
|
22
|
+
onDeleteClick?: (client: OAuthClientInterface) => void;
|
|
23
|
+
/** Message to show when list is empty */
|
|
24
|
+
emptyStateMessage?: string;
|
|
25
|
+
/** Title for the list */
|
|
26
|
+
title?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Component for displaying a list of OAuth clients
|
|
31
|
+
*/
|
|
32
|
+
export function OAuthClientList({
|
|
33
|
+
clients,
|
|
34
|
+
isLoading = false,
|
|
35
|
+
error,
|
|
36
|
+
onClientClick,
|
|
37
|
+
onCreateClick,
|
|
38
|
+
onEditClick,
|
|
39
|
+
onDeleteClick,
|
|
40
|
+
emptyStateMessage = "No OAuth applications yet. Create one to get started.",
|
|
41
|
+
title = "OAuth Applications",
|
|
42
|
+
}: OAuthClientListProps) {
|
|
43
|
+
// Loading state
|
|
44
|
+
if (isLoading && clients.length === 0) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-4">
|
|
47
|
+
<div className="flex items-center justify-between">
|
|
48
|
+
<h2 className="text-2xl font-bold">{title}</h2>
|
|
49
|
+
<Skeleton className="h-10 w-32" />
|
|
50
|
+
</div>
|
|
51
|
+
<div className="space-y-3">
|
|
52
|
+
{[1, 2, 3].map((i) => (
|
|
53
|
+
<Skeleton key={i} className="h-32 w-full" />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Error state
|
|
61
|
+
if (error) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
<div className="flex items-center justify-between">
|
|
65
|
+
<h2 className="text-2xl font-bold">{title}</h2>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center">
|
|
68
|
+
<p className="text-destructive">{error.message}</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Empty state
|
|
75
|
+
if (clients.length === 0) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="space-y-4">
|
|
78
|
+
<div className="flex items-center justify-between">
|
|
79
|
+
<h2 className="text-2xl font-bold">{title}</h2>
|
|
80
|
+
{onCreateClick && (
|
|
81
|
+
<Button onClick={onCreateClick}>
|
|
82
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
83
|
+
New App
|
|
84
|
+
</Button>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<div className="rounded-lg border border-dashed p-12 text-center">
|
|
88
|
+
<Key className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
89
|
+
<h3 className="text-lg font-medium mb-2">No OAuth Applications</h3>
|
|
90
|
+
<p className="text-muted-foreground mb-4">{emptyStateMessage}</p>
|
|
91
|
+
{onCreateClick && (
|
|
92
|
+
<Button onClick={onCreateClick}>
|
|
93
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
94
|
+
Create Application
|
|
95
|
+
</Button>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// List view
|
|
103
|
+
return (
|
|
104
|
+
<div className="space-y-4">
|
|
105
|
+
<div className="flex items-center justify-between">
|
|
106
|
+
<h2 className="text-2xl font-bold">{title}</h2>
|
|
107
|
+
{onCreateClick && (
|
|
108
|
+
<Button onClick={onCreateClick}>
|
|
109
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
110
|
+
New App
|
|
111
|
+
</Button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
<div className="space-y-3">
|
|
115
|
+
{clients.map((client) => (
|
|
116
|
+
<OAuthClientCard
|
|
117
|
+
key={client.id || client.clientId}
|
|
118
|
+
client={client}
|
|
119
|
+
onClick={() => onClientClick?.(client)}
|
|
120
|
+
onEdit={onEditClick ? () => onEditClick(client) : undefined}
|
|
121
|
+
onDelete={onDeleteClick ? () => onDeleteClick(client) : undefined}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { Copy, Check, AlertTriangle } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
Alert,
|
|
14
|
+
AlertDescription,
|
|
15
|
+
Input,
|
|
16
|
+
} from "../../../shadcnui";
|
|
17
|
+
|
|
18
|
+
export interface OAuthClientSecretDisplayProps {
|
|
19
|
+
/** The client secret to display */
|
|
20
|
+
secret: string;
|
|
21
|
+
/** Called when user dismisses the dialog */
|
|
22
|
+
onDismiss: () => void;
|
|
23
|
+
/** Whether the dialog is open */
|
|
24
|
+
open: boolean;
|
|
25
|
+
/** Optional client name for context */
|
|
26
|
+
clientName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Modal dialog for displaying a client secret ONE TIME
|
|
31
|
+
* Shows warning that secret cannot be retrieved again
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* const [secret, setSecret] = useAtom(oauthNewClientSecretAtom);
|
|
36
|
+
*
|
|
37
|
+
* <OAuthClientSecretDisplay
|
|
38
|
+
* secret={secret || ''}
|
|
39
|
+
* open={!!secret}
|
|
40
|
+
* onDismiss={() => setSecret(null)}
|
|
41
|
+
* />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function OAuthClientSecretDisplay({
|
|
45
|
+
secret,
|
|
46
|
+
onDismiss,
|
|
47
|
+
open,
|
|
48
|
+
clientName,
|
|
49
|
+
}: OAuthClientSecretDisplayProps) {
|
|
50
|
+
const [copied, setCopied] = useState(false);
|
|
51
|
+
|
|
52
|
+
const handleCopy = useCallback(async () => {
|
|
53
|
+
try {
|
|
54
|
+
await navigator.clipboard.writeText(secret);
|
|
55
|
+
setCopied(true);
|
|
56
|
+
setTimeout(() => setCopied(false), 2000);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("Failed to copy to clipboard:", err);
|
|
59
|
+
}
|
|
60
|
+
}, [secret]);
|
|
61
|
+
|
|
62
|
+
const handleDismiss = useCallback(() => {
|
|
63
|
+
setCopied(false);
|
|
64
|
+
onDismiss();
|
|
65
|
+
}, [onDismiss]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleDismiss()}>
|
|
69
|
+
<DialogContent className="sm:max-w-md">
|
|
70
|
+
<DialogHeader>
|
|
71
|
+
<DialogTitle className="flex items-center gap-2">
|
|
72
|
+
<AlertTriangle className="h-5 w-5 text-warning" />
|
|
73
|
+
Save Your Client Secret
|
|
74
|
+
</DialogTitle>
|
|
75
|
+
<DialogDescription>
|
|
76
|
+
{clientName
|
|
77
|
+
? `Your client secret for "${clientName}" is shown below.`
|
|
78
|
+
: "Your client secret is shown below."}
|
|
79
|
+
</DialogDescription>
|
|
80
|
+
</DialogHeader>
|
|
81
|
+
|
|
82
|
+
<Alert variant="destructive" className="my-4">
|
|
83
|
+
<AlertTriangle className="h-4 w-4" />
|
|
84
|
+
<AlertDescription>
|
|
85
|
+
<strong>This is the only time your client secret will be displayed.</strong>
|
|
86
|
+
<br />
|
|
87
|
+
Copy it now and store it securely. You will not be able to retrieve it later.
|
|
88
|
+
</AlertDescription>
|
|
89
|
+
</Alert>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center space-x-2">
|
|
92
|
+
<div className="flex-1">
|
|
93
|
+
<Input
|
|
94
|
+
value={secret}
|
|
95
|
+
readOnly
|
|
96
|
+
className="font-mono text-sm"
|
|
97
|
+
onClick={(e) => e.currentTarget.select()}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<Button
|
|
101
|
+
type="button"
|
|
102
|
+
variant="outline"
|
|
103
|
+
size="icon"
|
|
104
|
+
onClick={handleCopy}
|
|
105
|
+
title={copied ? "Copied!" : "Copy to clipboard"}
|
|
106
|
+
>
|
|
107
|
+
{copied ? (
|
|
108
|
+
<Check className="h-4 w-4 text-green-600" />
|
|
109
|
+
) : (
|
|
110
|
+
<Copy className="h-4 w-4" />
|
|
111
|
+
)}
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{copied && (
|
|
116
|
+
<p className="text-sm text-green-600 text-center">Copied to clipboard!</p>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<DialogFooter className="mt-4">
|
|
120
|
+
<Button onClick={handleDismiss} className="w-full">
|
|
121
|
+
I've Saved My Secret
|
|
122
|
+
</Button>
|
|
123
|
+
</DialogFooter>
|
|
124
|
+
</DialogContent>
|
|
125
|
+
</Dialog>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { Button, Input, Label } from "../../../shadcnui";
|
|
6
|
+
|
|
7
|
+
export interface OAuthRedirectUriInputProps {
|
|
8
|
+
/** Current array of redirect URIs */
|
|
9
|
+
value: string[];
|
|
10
|
+
/** Called when URIs change */
|
|
11
|
+
onChange: (uris: string[]) => void;
|
|
12
|
+
/** Error message to display */
|
|
13
|
+
error?: string;
|
|
14
|
+
/** Whether input is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Label text */
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates a redirect URI
|
|
22
|
+
* Allows: https://, http://localhost, custom schemes (myapp://)
|
|
23
|
+
*/
|
|
24
|
+
function isValidRedirectUri(uri: string): boolean {
|
|
25
|
+
if (!uri.trim()) return false;
|
|
26
|
+
|
|
27
|
+
// Allow localhost for development
|
|
28
|
+
if (uri.startsWith("http://localhost") || uri.startsWith("http://127.0.0.1")) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Require HTTPS for non-localhost
|
|
33
|
+
if (uri.startsWith("https://")) {
|
|
34
|
+
try {
|
|
35
|
+
new URL(uri);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Allow custom schemes (e.g., myapp://oauth/callback)
|
|
43
|
+
if (uri.includes("://")) {
|
|
44
|
+
const schemeMatch = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
45
|
+
return schemeMatch !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Dynamic input for managing OAuth redirect URIs
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const [redirectUris, setRedirectUris] = useState<string[]>(['']);
|
|
57
|
+
*
|
|
58
|
+
* <OAuthRedirectUriInput
|
|
59
|
+
* value={redirectUris}
|
|
60
|
+
* onChange={setRedirectUris}
|
|
61
|
+
* error={errors.redirectUris}
|
|
62
|
+
* />
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function OAuthRedirectUriInput({
|
|
66
|
+
value,
|
|
67
|
+
onChange,
|
|
68
|
+
error,
|
|
69
|
+
disabled = false,
|
|
70
|
+
label = "Redirect URIs",
|
|
71
|
+
}: OAuthRedirectUriInputProps) {
|
|
72
|
+
const handleAdd = useCallback(() => {
|
|
73
|
+
onChange([...value, ""]);
|
|
74
|
+
}, [value, onChange]);
|
|
75
|
+
|
|
76
|
+
const handleRemove = useCallback(
|
|
77
|
+
(index: number) => {
|
|
78
|
+
const newUris = value.filter((_, i) => i !== index);
|
|
79
|
+
// Keep at least one empty input
|
|
80
|
+
onChange(newUris.length > 0 ? newUris : [""]);
|
|
81
|
+
},
|
|
82
|
+
[value, onChange]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleChange = useCallback(
|
|
86
|
+
(index: number, newValue: string) => {
|
|
87
|
+
const newUris = [...value];
|
|
88
|
+
newUris[index] = newValue;
|
|
89
|
+
onChange(newUris);
|
|
90
|
+
},
|
|
91
|
+
[value, onChange]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<Label>{label} *</Label>
|
|
97
|
+
<p className="text-sm text-muted-foreground">
|
|
98
|
+
Enter the URIs where users will be redirected after authorization.
|
|
99
|
+
Use https:// for production, or custom schemes for mobile apps.
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
<div className="space-y-2">
|
|
103
|
+
{value.map((uri, index) => {
|
|
104
|
+
const isValid = !uri || isValidRedirectUri(uri);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div key={index} className="flex gap-2">
|
|
108
|
+
<div className="flex-1">
|
|
109
|
+
<Input
|
|
110
|
+
value={uri}
|
|
111
|
+
onChange={(e) => handleChange(index, e.target.value)}
|
|
112
|
+
placeholder="https://example.com/callback or myapp://oauth"
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
className={!isValid ? "border-destructive" : ""}
|
|
115
|
+
/>
|
|
116
|
+
{!isValid && (
|
|
117
|
+
<p className="text-xs text-destructive mt-1">
|
|
118
|
+
Must be https://, http://localhost, or a custom scheme (app://)
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
<Button
|
|
123
|
+
type="button"
|
|
124
|
+
variant="ghost"
|
|
125
|
+
size="icon"
|
|
126
|
+
onClick={() => handleRemove(index)}
|
|
127
|
+
disabled={disabled || value.length === 1}
|
|
128
|
+
title="Remove URI"
|
|
129
|
+
>
|
|
130
|
+
<Trash2 className="h-4 w-4" />
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<Button
|
|
138
|
+
type="button"
|
|
139
|
+
variant="outline"
|
|
140
|
+
size="sm"
|
|
141
|
+
onClick={handleAdd}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
className="mt-2"
|
|
144
|
+
>
|
|
145
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
146
|
+
Add Redirect URI
|
|
147
|
+
</Button>
|
|
148
|
+
|
|
149
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|