@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,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { Checkbox, Label } from "../../../shadcnui";
|
|
5
|
+
import { AVAILABLE_OAUTH_SCOPES, OAuthScopeInfo } from "../interfaces/oauth.interface";
|
|
6
|
+
|
|
7
|
+
export interface OAuthScopeSelectorProps {
|
|
8
|
+
/** Currently selected scopes */
|
|
9
|
+
value: string[];
|
|
10
|
+
/** Called when selection changes */
|
|
11
|
+
onChange: (scopes: string[]) => void;
|
|
12
|
+
/** Available scopes to display (defaults to all) */
|
|
13
|
+
availableScopes?: OAuthScopeInfo[];
|
|
14
|
+
/** Whether selector is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Error message */
|
|
17
|
+
error?: string;
|
|
18
|
+
/** Label text */
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Checkbox selector for OAuth scopes
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const [scopes, setScopes] = useState<string[]>([]);
|
|
28
|
+
*
|
|
29
|
+
* <OAuthScopeSelector
|
|
30
|
+
* value={scopes}
|
|
31
|
+
* onChange={setScopes}
|
|
32
|
+
* error={errors.scopes}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function OAuthScopeSelector({
|
|
37
|
+
value,
|
|
38
|
+
onChange,
|
|
39
|
+
availableScopes = AVAILABLE_OAUTH_SCOPES,
|
|
40
|
+
disabled = false,
|
|
41
|
+
error,
|
|
42
|
+
label = "Allowed Scopes",
|
|
43
|
+
}: OAuthScopeSelectorProps) {
|
|
44
|
+
const handleToggle = useCallback(
|
|
45
|
+
(scope: string, checked: boolean) => {
|
|
46
|
+
if (checked) {
|
|
47
|
+
onChange([...value, scope]);
|
|
48
|
+
} else {
|
|
49
|
+
onChange(value.filter((s) => s !== scope));
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[value, onChange]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Group scopes by category (before the colon)
|
|
56
|
+
const groupedScopes = availableScopes.reduce((acc, scope) => {
|
|
57
|
+
const [category] = scope.scope.split(":");
|
|
58
|
+
const groupName = category === scope.scope ? "General" : category;
|
|
59
|
+
|
|
60
|
+
if (!acc[groupName]) {
|
|
61
|
+
acc[groupName] = [];
|
|
62
|
+
}
|
|
63
|
+
acc[groupName].push(scope);
|
|
64
|
+
return acc;
|
|
65
|
+
}, {} as Record<string, OAuthScopeInfo[]>);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-4">
|
|
69
|
+
<div>
|
|
70
|
+
<Label>{label} *</Label>
|
|
71
|
+
<p className="text-sm text-muted-foreground">
|
|
72
|
+
Select the permissions your application needs.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
{Object.entries(groupedScopes).map(([groupName, scopes]) => (
|
|
78
|
+
<div key={groupName} className="space-y-2">
|
|
79
|
+
<h4 className="text-sm font-medium capitalize">{groupName}</h4>
|
|
80
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 pl-2">
|
|
81
|
+
{scopes.map((scopeInfo) => {
|
|
82
|
+
const isChecked = value.includes(scopeInfo.scope);
|
|
83
|
+
const isAdmin = scopeInfo.scope === "admin";
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
key={scopeInfo.scope}
|
|
88
|
+
className={`flex items-start space-x-3 p-2 rounded-md border ${
|
|
89
|
+
isChecked ? "bg-primary/5 border-primary/20" : "border-transparent"
|
|
90
|
+
} ${isAdmin ? "bg-destructive/5" : ""}`}
|
|
91
|
+
>
|
|
92
|
+
<Checkbox
|
|
93
|
+
id={`scope-${scopeInfo.scope}`}
|
|
94
|
+
checked={isChecked}
|
|
95
|
+
onCheckedChange={(checked) =>
|
|
96
|
+
handleToggle(scopeInfo.scope, checked === true)
|
|
97
|
+
}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
/>
|
|
100
|
+
<div className="flex-1">
|
|
101
|
+
<Label
|
|
102
|
+
htmlFor={`scope-${scopeInfo.scope}`}
|
|
103
|
+
className="text-sm font-medium cursor-pointer"
|
|
104
|
+
>
|
|
105
|
+
{scopeInfo.name}
|
|
106
|
+
{isAdmin && (
|
|
107
|
+
<span className="ml-2 text-xs text-destructive">(Dangerous)</span>
|
|
108
|
+
)}
|
|
109
|
+
</Label>
|
|
110
|
+
<p className="text-xs text-muted-foreground">{scopeInfo.description}</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../../../../shadcnui";
|
|
4
|
+
|
|
5
|
+
export interface OAuthConsentActionsProps {
|
|
6
|
+
/** Called when user clicks Authorize */
|
|
7
|
+
onApprove: () => void;
|
|
8
|
+
/** Called when user clicks Deny */
|
|
9
|
+
onDeny: () => void;
|
|
10
|
+
/** Whether an action is in progress */
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Action buttons for OAuth consent screen
|
|
16
|
+
*/
|
|
17
|
+
export function OAuthConsentActions({
|
|
18
|
+
onApprove,
|
|
19
|
+
onDeny,
|
|
20
|
+
isLoading = false,
|
|
21
|
+
}: OAuthConsentActionsProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
24
|
+
<Button
|
|
25
|
+
variant="outline"
|
|
26
|
+
onClick={onDeny}
|
|
27
|
+
disabled={isLoading}
|
|
28
|
+
className="flex-1"
|
|
29
|
+
>
|
|
30
|
+
Deny
|
|
31
|
+
</Button>
|
|
32
|
+
<Button
|
|
33
|
+
onClick={onApprove}
|
|
34
|
+
disabled={isLoading}
|
|
35
|
+
className="flex-1"
|
|
36
|
+
>
|
|
37
|
+
{isLoading ? "Authorizing..." : "Authorize"}
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Shield } from "lucide-react";
|
|
4
|
+
import { OAuthClientInterface } from "../../interfaces/oauth.interface";
|
|
5
|
+
|
|
6
|
+
export interface OAuthConsentHeaderProps {
|
|
7
|
+
/** The requesting OAuth client */
|
|
8
|
+
client: OAuthClientInterface;
|
|
9
|
+
/** Optional logo URL override */
|
|
10
|
+
logoUrl?: string;
|
|
11
|
+
/** Application name (e.g., "Only35") */
|
|
12
|
+
appName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Header component for OAuth consent screen
|
|
17
|
+
* Shows platform logo and requesting app information
|
|
18
|
+
*/
|
|
19
|
+
export function OAuthConsentHeader({
|
|
20
|
+
client,
|
|
21
|
+
logoUrl,
|
|
22
|
+
appName = "Only35",
|
|
23
|
+
}: OAuthConsentHeaderProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="text-center space-y-4">
|
|
26
|
+
{/* Platform Logo */}
|
|
27
|
+
<div className="flex justify-center">
|
|
28
|
+
{logoUrl ? (
|
|
29
|
+
<img
|
|
30
|
+
src={logoUrl}
|
|
31
|
+
alt={appName}
|
|
32
|
+
className="h-12 w-auto"
|
|
33
|
+
/>
|
|
34
|
+
) : (
|
|
35
|
+
<div className="h-12 w-12 rounded-full bg-primary flex items-center justify-center">
|
|
36
|
+
<Shield className="h-6 w-6 text-primary-foreground" />
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Authorization Request */}
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<h1 className="text-2xl font-bold">Authorize {client.name}</h1>
|
|
44
|
+
<p className="text-muted-foreground">
|
|
45
|
+
<span className="font-medium text-foreground">{client.name}</span>
|
|
46
|
+
{" "}wants to access your {appName} account
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink, AlertTriangle, Loader2 } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardFooter,
|
|
8
|
+
Separator,
|
|
9
|
+
Alert,
|
|
10
|
+
AlertDescription,
|
|
11
|
+
} from "../../../../shadcnui";
|
|
12
|
+
import { OAuthConsentHeader } from "./OAuthConsentHeader";
|
|
13
|
+
import { OAuthScopeList } from "./OAuthScopeList";
|
|
14
|
+
import { OAuthConsentActions } from "./OAuthConsentActions";
|
|
15
|
+
import { useOAuthConsent } from "../../hooks/useOAuthConsent";
|
|
16
|
+
import { OAuthConsentRequest } from "../../interfaces/oauth.interface";
|
|
17
|
+
|
|
18
|
+
export interface OAuthConsentScreenProps {
|
|
19
|
+
/** OAuth authorization parameters */
|
|
20
|
+
params: OAuthConsentRequest;
|
|
21
|
+
/** Optional platform logo URL */
|
|
22
|
+
logoUrl?: string;
|
|
23
|
+
/** Platform name */
|
|
24
|
+
appName?: string;
|
|
25
|
+
/** Terms of Service URL */
|
|
26
|
+
termsUrl?: string;
|
|
27
|
+
/** Privacy Policy URL */
|
|
28
|
+
privacyUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Main OAuth consent screen component
|
|
33
|
+
* Displays client info, requested scopes, and approve/deny buttons
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <OAuthConsentScreen
|
|
38
|
+
* params={{
|
|
39
|
+
* clientId: searchParams.client_id,
|
|
40
|
+
* redirectUri: searchParams.redirect_uri,
|
|
41
|
+
* scope: searchParams.scope,
|
|
42
|
+
* state: searchParams.state,
|
|
43
|
+
* }}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function OAuthConsentScreen({
|
|
48
|
+
params,
|
|
49
|
+
logoUrl,
|
|
50
|
+
appName = "Only35",
|
|
51
|
+
termsUrl = "/terms",
|
|
52
|
+
privacyUrl = "/privacy",
|
|
53
|
+
}: OAuthConsentScreenProps) {
|
|
54
|
+
const { clientInfo, isLoading, error, approve, deny, isSubmitting } = useOAuthConsent(params);
|
|
55
|
+
|
|
56
|
+
// Loading state
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
60
|
+
<Card className="w-full max-w-md">
|
|
61
|
+
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
62
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
63
|
+
<p className="mt-4 text-muted-foreground">Loading authorization request...</p>
|
|
64
|
+
</CardContent>
|
|
65
|
+
</Card>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Error state
|
|
71
|
+
if (error || !clientInfo) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
74
|
+
<Card className="w-full max-w-md">
|
|
75
|
+
<CardContent className="py-8">
|
|
76
|
+
<Alert variant="destructive">
|
|
77
|
+
<AlertTriangle className="h-4 w-4" />
|
|
78
|
+
<AlertDescription>
|
|
79
|
+
{error?.message || "Invalid authorization request. Please try again."}
|
|
80
|
+
</AlertDescription>
|
|
81
|
+
</Alert>
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { client, scopes } = clientInfo;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="min-h-screen flex items-center justify-center p-4 bg-muted/30">
|
|
92
|
+
<Card className="w-full max-w-md">
|
|
93
|
+
<CardContent className="pt-6 space-y-6">
|
|
94
|
+
{/* Header */}
|
|
95
|
+
<OAuthConsentHeader
|
|
96
|
+
client={client}
|
|
97
|
+
logoUrl={logoUrl}
|
|
98
|
+
appName={appName}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<Separator />
|
|
102
|
+
|
|
103
|
+
{/* Scopes */}
|
|
104
|
+
<OAuthScopeList scopes={scopes} />
|
|
105
|
+
|
|
106
|
+
<Separator />
|
|
107
|
+
|
|
108
|
+
{/* Redirect URI Notice */}
|
|
109
|
+
<div className="flex items-start gap-2 text-sm text-muted-foreground">
|
|
110
|
+
<ExternalLink className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
111
|
+
<div>
|
|
112
|
+
<span>Authorizing will redirect you to:</span>
|
|
113
|
+
<p className="font-mono text-xs mt-1 break-all">{params.redirectUri}</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Actions */}
|
|
118
|
+
<OAuthConsentActions
|
|
119
|
+
onApprove={approve}
|
|
120
|
+
onDeny={deny}
|
|
121
|
+
isLoading={isSubmitting}
|
|
122
|
+
/>
|
|
123
|
+
</CardContent>
|
|
124
|
+
|
|
125
|
+
{/* Footer */}
|
|
126
|
+
<CardFooter className="justify-center">
|
|
127
|
+
<p className="text-xs text-center text-muted-foreground">
|
|
128
|
+
By authorizing, you agree to the app's{" "}
|
|
129
|
+
<a href={termsUrl} className="underline hover:text-foreground" target="_blank" rel="noopener">
|
|
130
|
+
Terms of Service
|
|
131
|
+
</a>
|
|
132
|
+
{" "}and{" "}
|
|
133
|
+
<a href={privacyUrl} className="underline hover:text-foreground" target="_blank" rel="noopener">
|
|
134
|
+
Privacy Policy
|
|
135
|
+
</a>
|
|
136
|
+
.
|
|
137
|
+
</p>
|
|
138
|
+
</CardFooter>
|
|
139
|
+
</Card>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Eye,
|
|
5
|
+
Pencil,
|
|
6
|
+
Image,
|
|
7
|
+
Upload,
|
|
8
|
+
Film,
|
|
9
|
+
FolderPlus,
|
|
10
|
+
User,
|
|
11
|
+
Shield,
|
|
12
|
+
LucideIcon,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { OAuthScopeInfo } from "../../interfaces/oauth.interface";
|
|
15
|
+
|
|
16
|
+
export interface OAuthScopeListProps {
|
|
17
|
+
/** List of requested scopes */
|
|
18
|
+
scopes: OAuthScopeInfo[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Map scope icons to Lucide components */
|
|
22
|
+
const SCOPE_ICONS: Record<string, LucideIcon> = {
|
|
23
|
+
eye: Eye,
|
|
24
|
+
pencil: Pencil,
|
|
25
|
+
image: Image,
|
|
26
|
+
upload: Upload,
|
|
27
|
+
film: Film,
|
|
28
|
+
"folder-plus": FolderPlus,
|
|
29
|
+
user: User,
|
|
30
|
+
shield: Shield,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* List of requested OAuth scopes for consent display
|
|
35
|
+
*/
|
|
36
|
+
export function OAuthScopeList({ scopes }: OAuthScopeListProps) {
|
|
37
|
+
if (scopes.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-3">
|
|
43
|
+
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
44
|
+
This will allow the application to:
|
|
45
|
+
</h2>
|
|
46
|
+
<ul className="space-y-3">
|
|
47
|
+
{scopes.map((scope) => {
|
|
48
|
+
const IconComponent = scope.icon ? SCOPE_ICONS[scope.icon] : Eye;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<li
|
|
52
|
+
key={scope.scope}
|
|
53
|
+
className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"
|
|
54
|
+
>
|
|
55
|
+
<div className="flex-shrink-0 mt-0.5">
|
|
56
|
+
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
|
57
|
+
{IconComponent && (
|
|
58
|
+
<IconComponent className="h-4 w-4 text-primary" />
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex-1">
|
|
63
|
+
<p className="font-medium">{scope.name}</p>
|
|
64
|
+
<p className="text-sm text-muted-foreground">{scope.description}</p>
|
|
65
|
+
</div>
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</ul>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./OAuthRedirectUriInput";
|
|
2
|
+
export * from "./OAuthScopeSelector";
|
|
3
|
+
export * from "./OAuthClientSecretDisplay";
|
|
4
|
+
export * from "./OAuthClientCard";
|
|
5
|
+
export * from "./OAuthClientList";
|
|
6
|
+
export * from "./OAuthClientForm";
|
|
7
|
+
export * from "./OAuthClientDetail";
|
|
8
|
+
export * from "./consent";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef } from "../../../core";
|
|
2
|
+
import {
|
|
3
|
+
OAuthClientCreateRequest,
|
|
4
|
+
OAuthClientCreateResponse,
|
|
5
|
+
OAuthClientInput,
|
|
6
|
+
OAuthClientInterface,
|
|
7
|
+
OAuthConsentInfo,
|
|
8
|
+
OAuthConsentRequest,
|
|
9
|
+
} from "../interfaces/oauth.interface";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service for OAuth client management and authorization consent flow.
|
|
13
|
+
*
|
|
14
|
+
* Client Management endpoints:
|
|
15
|
+
* - GET /oauth/clients - List all clients for current user
|
|
16
|
+
* - GET /oauth/clients/:clientId - Get single client
|
|
17
|
+
* - POST /oauth/clients - Create new client (returns secret once)
|
|
18
|
+
* - PATCH /oauth/clients/:clientId - Update client
|
|
19
|
+
* - DELETE /oauth/clients/:clientId - Delete client
|
|
20
|
+
* - POST /oauth/clients/:clientId/regenerate-secret - Regenerate client secret
|
|
21
|
+
*
|
|
22
|
+
* Consent Flow endpoints:
|
|
23
|
+
* - GET /oauth/authorize/info - Get client info for consent screen
|
|
24
|
+
* - POST /oauth/authorize/approve - Approve authorization
|
|
25
|
+
* - POST /oauth/authorize/deny - Deny authorization
|
|
26
|
+
*/
|
|
27
|
+
export class OAuthService extends AbstractService {
|
|
28
|
+
// ==========================================
|
|
29
|
+
// CLIENT MANAGEMENT
|
|
30
|
+
// ==========================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* List all OAuth clients for the current user
|
|
34
|
+
*/
|
|
35
|
+
static async listClients(params?: { next?: NextRef }): Promise<OAuthClientInterface[]> {
|
|
36
|
+
return this.callApi<OAuthClientInterface[]>({
|
|
37
|
+
type: Modules.OAuth,
|
|
38
|
+
method: HttpMethod.GET,
|
|
39
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/clients" }).generate(),
|
|
40
|
+
next: params?.next,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get a single OAuth client by ID
|
|
46
|
+
*/
|
|
47
|
+
static async getClient(params: { clientId: string }): Promise<OAuthClientInterface> {
|
|
48
|
+
return this.callApi<OAuthClientInterface>({
|
|
49
|
+
type: Modules.OAuth,
|
|
50
|
+
method: HttpMethod.GET,
|
|
51
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/clients", id: params.clientId }).generate(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new OAuth client
|
|
57
|
+
* @returns The created client AND the client secret (shown only once!)
|
|
58
|
+
*/
|
|
59
|
+
static async createClient(data: OAuthClientCreateRequest): Promise<OAuthClientCreateResponse> {
|
|
60
|
+
const result = await this.callApiWithMeta<OAuthClientInterface>({
|
|
61
|
+
type: Modules.OAuth,
|
|
62
|
+
method: HttpMethod.POST,
|
|
63
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/clients" }).generate(),
|
|
64
|
+
input: data,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
client: result.data,
|
|
69
|
+
clientSecret: result.meta?.clientSecret as string | undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Update an existing OAuth client
|
|
75
|
+
*/
|
|
76
|
+
static async updateClient(params: {
|
|
77
|
+
clientId: string;
|
|
78
|
+
data: Partial<OAuthClientInput>;
|
|
79
|
+
}): Promise<OAuthClientInterface> {
|
|
80
|
+
return this.callApi<OAuthClientInterface>({
|
|
81
|
+
type: Modules.OAuth,
|
|
82
|
+
method: HttpMethod.PATCH,
|
|
83
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/clients", id: params.clientId }).generate(),
|
|
84
|
+
input: { id: params.clientId, ...params.data },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Delete an OAuth client
|
|
90
|
+
*/
|
|
91
|
+
static async deleteClient(params: { clientId: string }): Promise<void> {
|
|
92
|
+
await this.callApi({
|
|
93
|
+
type: Modules.OAuth,
|
|
94
|
+
method: HttpMethod.DELETE,
|
|
95
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/clients", id: params.clientId }).generate(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Regenerate the client secret
|
|
101
|
+
* @returns The new client secret (shown only once!)
|
|
102
|
+
*/
|
|
103
|
+
static async regenerateSecret(params: { clientId: string }): Promise<{ clientSecret: string }> {
|
|
104
|
+
const result = await this.callApiWithMeta<OAuthClientInterface>({
|
|
105
|
+
type: Modules.OAuth,
|
|
106
|
+
method: HttpMethod.POST,
|
|
107
|
+
endpoint: new EndpointCreator({
|
|
108
|
+
endpoint: "oauth/clients",
|
|
109
|
+
id: params.clientId,
|
|
110
|
+
childEndpoint: "regenerate-secret",
|
|
111
|
+
}).generate(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
clientSecret: result.meta?.clientSecret as string,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==========================================
|
|
120
|
+
// CONSENT FLOW
|
|
121
|
+
// ==========================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get client information for the consent screen
|
|
125
|
+
* Called when user is redirected to /oauth/authorize
|
|
126
|
+
*/
|
|
127
|
+
static async getAuthorizationInfo(params: OAuthConsentRequest): Promise<OAuthConsentInfo> {
|
|
128
|
+
const endpoint = new EndpointCreator({ endpoint: "oauth/authorize/info" });
|
|
129
|
+
|
|
130
|
+
// Add query parameters
|
|
131
|
+
endpoint.addAdditionalParam("client_id", params.clientId);
|
|
132
|
+
endpoint.addAdditionalParam("redirect_uri", params.redirectUri);
|
|
133
|
+
endpoint.addAdditionalParam("scope", params.scope);
|
|
134
|
+
if (params.state) endpoint.addAdditionalParam("state", params.state);
|
|
135
|
+
if (params.codeChallenge) endpoint.addAdditionalParam("code_challenge", params.codeChallenge);
|
|
136
|
+
if (params.codeChallengeMethod) endpoint.addAdditionalParam("code_challenge_method", params.codeChallengeMethod);
|
|
137
|
+
|
|
138
|
+
return this.callApi<OAuthConsentInfo>({
|
|
139
|
+
type: Modules.OAuth,
|
|
140
|
+
method: HttpMethod.GET,
|
|
141
|
+
endpoint: endpoint.generate(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Approve the authorization request
|
|
147
|
+
* @returns Redirect URL with authorization code
|
|
148
|
+
*/
|
|
149
|
+
static async approveAuthorization(params: OAuthConsentRequest): Promise<{ redirectUrl: string }> {
|
|
150
|
+
const result = await this.callApiWithMeta<unknown>({
|
|
151
|
+
type: Modules.OAuth,
|
|
152
|
+
method: HttpMethod.POST,
|
|
153
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/authorize/approve" }).generate(),
|
|
154
|
+
input: {
|
|
155
|
+
client_id: params.clientId,
|
|
156
|
+
redirect_uri: params.redirectUri,
|
|
157
|
+
scope: params.scope,
|
|
158
|
+
state: params.state,
|
|
159
|
+
code_challenge: params.codeChallenge,
|
|
160
|
+
code_challenge_method: params.codeChallengeMethod,
|
|
161
|
+
},
|
|
162
|
+
overridesJsonApiCreation: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
redirectUrl: result.meta?.redirectUrl as string,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Deny the authorization request
|
|
172
|
+
* @returns Redirect URL with error=access_denied
|
|
173
|
+
*/
|
|
174
|
+
static async denyAuthorization(params: OAuthConsentRequest): Promise<{ redirectUrl: string }> {
|
|
175
|
+
const result = await this.callApiWithMeta<unknown>({
|
|
176
|
+
type: Modules.OAuth,
|
|
177
|
+
method: HttpMethod.POST,
|
|
178
|
+
endpoint: new EndpointCreator({ endpoint: "oauth/authorize/deny" }).generate(),
|
|
179
|
+
input: {
|
|
180
|
+
client_id: params.clientId,
|
|
181
|
+
redirect_uri: params.redirectUri,
|
|
182
|
+
state: params.state,
|
|
183
|
+
},
|
|
184
|
+
overridesJsonApiCreation: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
redirectUrl: result.meta?.redirectUrl as string,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|