@carlonicora/nextjs-jsonapi 1.24.3 → 1.25.1
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-OFSTXGZX.js → BlockNoteEditor-7WAWEZVW.js} +13 -13
- package/dist/{BlockNoteEditor-OFSTXGZX.js.map → BlockNoteEditor-7WAWEZVW.js.map} +1 -1
- package/dist/{BlockNoteEditor-TJNLCNIP.mjs → BlockNoteEditor-UNVKGZ2G.mjs} +3 -3
- package/dist/billing/index.js +342 -342
- package/dist/billing/index.mjs +2 -2
- package/dist/{chunk-H5JZ5E7M.mjs → chunk-6BDOZDZ3.mjs} +1247 -54
- package/dist/chunk-6BDOZDZ3.mjs.map +1 -0
- package/dist/{chunk-EJALOG7L.js → chunk-JI6BDV7L.js} +1598 -405
- package/dist/chunk-JI6BDV7L.js.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/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 +225 -1
- package/dist/components/index.d.ts +225 -1
- package/dist/components/index.js +25 -3
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +24 -2
- 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/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +3 -2
- package/src/client/index.ts +1 -0
- 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/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/dist/chunk-5U4NJJOF.mjs.map +0 -1
- package/dist/chunk-EJALOG7L.js.map +0 -1
- package/dist/chunk-H5JZ5E7M.mjs.map +0 -1
- package/dist/chunk-NQVPCNRS.js.map +0 -1
- /package/dist/{BlockNoteEditor-TJNLCNIP.mjs.map → BlockNoteEditor-UNVKGZ2G.mjs.map} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
|
|
2
|
+
import { OAuthClientInput, OAuthClientInterface } from "../interfaces/oauth.interface";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OAuth client data model
|
|
6
|
+
* Represents a registered OAuth application that can request access tokens
|
|
7
|
+
*/
|
|
8
|
+
export class OAuthClient extends AbstractApiData implements OAuthClientInterface {
|
|
9
|
+
private _clientId?: string;
|
|
10
|
+
private _name?: string;
|
|
11
|
+
private _description?: string;
|
|
12
|
+
private _redirectUris: string[] = [];
|
|
13
|
+
private _allowedScopes: string[] = [];
|
|
14
|
+
private _allowedGrantTypes: string[] = [];
|
|
15
|
+
private _isConfidential: boolean = true;
|
|
16
|
+
private _isActive: boolean = true;
|
|
17
|
+
|
|
18
|
+
get clientId(): string {
|
|
19
|
+
return this._clientId ?? this.id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get name(): string {
|
|
23
|
+
if (!this._name) throw new Error("Name is not defined");
|
|
24
|
+
return this._name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get description(): string | undefined {
|
|
28
|
+
return this._description;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get redirectUris(): string[] {
|
|
32
|
+
return this._redirectUris;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get allowedScopes(): string[] {
|
|
36
|
+
return this._allowedScopes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get allowedGrantTypes(): string[] {
|
|
40
|
+
return this._allowedGrantTypes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get isConfidential(): boolean {
|
|
44
|
+
return this._isConfidential;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get isActive(): boolean {
|
|
48
|
+
return this._isActive;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
52
|
+
super.rehydrate(data);
|
|
53
|
+
|
|
54
|
+
const attrs = data.jsonApi.attributes || {};
|
|
55
|
+
|
|
56
|
+
this._clientId = attrs.clientId ?? this._id;
|
|
57
|
+
this._name = attrs.name;
|
|
58
|
+
this._description = attrs.description;
|
|
59
|
+
this._redirectUris = attrs.redirectUris ?? [];
|
|
60
|
+
this._allowedScopes = attrs.allowedScopes ?? [];
|
|
61
|
+
this._allowedGrantTypes = attrs.allowedGrantTypes ?? [];
|
|
62
|
+
this._isConfidential = attrs.isConfidential ?? true;
|
|
63
|
+
this._isActive = attrs.isActive ?? true;
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createJsonApi(data: OAuthClientInput) {
|
|
69
|
+
const response: any = {
|
|
70
|
+
data: {
|
|
71
|
+
type: "oauth-clients",
|
|
72
|
+
attributes: {},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (data.id) response.data.id = data.id;
|
|
77
|
+
if (data.name !== undefined) response.data.attributes.name = data.name;
|
|
78
|
+
if (data.description !== undefined) response.data.attributes.description = data.description;
|
|
79
|
+
if (data.redirectUris !== undefined) response.data.attributes.redirectUris = data.redirectUris;
|
|
80
|
+
if (data.allowedScopes !== undefined) response.data.attributes.allowedScopes = data.allowedScopes;
|
|
81
|
+
if (data.allowedGrantTypes !== undefined) response.data.attributes.allowedGrantTypes = data.allowedGrantTypes;
|
|
82
|
+
if (data.isConfidential !== undefined) response.data.attributes.isConfidential = data.isConfidential;
|
|
83
|
+
if (data.isActive !== undefined) response.data.attributes.isActive = data.isActive;
|
|
84
|
+
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAtomValue, useSetAtom } from "jotai";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
oauthClientByIdAtom,
|
|
7
|
+
removeOAuthClientAtom,
|
|
8
|
+
setNewClientSecretAtom,
|
|
9
|
+
updateOAuthClientAtom,
|
|
10
|
+
} from "../atoms/oauth.atoms";
|
|
11
|
+
import { OAuthClientInput, OAuthClientInterface } from "../interfaces/oauth.interface";
|
|
12
|
+
import { OAuthService } from "../data/oauth.service";
|
|
13
|
+
|
|
14
|
+
export interface UseOAuthClientReturn {
|
|
15
|
+
/** The OAuth client (from store or fetched) */
|
|
16
|
+
client: OAuthClientInterface | null;
|
|
17
|
+
/** Whether the client is being loaded */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** Error from last operation */
|
|
20
|
+
error: Error | null;
|
|
21
|
+
/** Update the client */
|
|
22
|
+
update: (data: Partial<OAuthClientInput>) => Promise<void>;
|
|
23
|
+
/** Delete the client */
|
|
24
|
+
deleteClient: () => Promise<void>;
|
|
25
|
+
/** Regenerate the client secret */
|
|
26
|
+
regenerateSecret: () => Promise<string>;
|
|
27
|
+
/** Refetch client from API */
|
|
28
|
+
refetch: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook for managing a single OAuth client
|
|
33
|
+
*
|
|
34
|
+
* @param clientId - The client ID to manage
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const { client, update, deleteClient, regenerateSecret } = useOAuthClient(clientId);
|
|
39
|
+
*
|
|
40
|
+
* const handleRegenerate = async () => {
|
|
41
|
+
* const newSecret = await regenerateSecret();
|
|
42
|
+
* // newSecret is shown only once!
|
|
43
|
+
* };
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useOAuthClient(clientId: string): UseOAuthClientReturn {
|
|
47
|
+
// Try to get from store first (populated by useOAuthClients)
|
|
48
|
+
const storedClient = useAtomValue(oauthClientByIdAtom(clientId));
|
|
49
|
+
const updateClientInStore = useSetAtom(updateOAuthClientAtom);
|
|
50
|
+
const removeClientFromStore = useSetAtom(removeOAuthClientAtom);
|
|
51
|
+
const setNewClientSecret = useSetAtom(setNewClientSecretAtom);
|
|
52
|
+
|
|
53
|
+
// Local state for fetched client (if not in store)
|
|
54
|
+
const [fetchedClient, setFetchedClient] = useState<OAuthClientInterface | null>(null);
|
|
55
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
56
|
+
const [error, setError] = useState<Error | null>(null);
|
|
57
|
+
|
|
58
|
+
const client = storedClient || fetchedClient;
|
|
59
|
+
|
|
60
|
+
const fetchClient = useCallback(async () => {
|
|
61
|
+
if (!clientId) return;
|
|
62
|
+
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const fetched = await OAuthService.getClient({ clientId });
|
|
68
|
+
setFetchedClient(fetched);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("[useOAuthClient] Failed to fetch client:", err);
|
|
71
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch OAuth client"));
|
|
72
|
+
} finally {
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}, [clientId]);
|
|
76
|
+
|
|
77
|
+
// Fetch if not in store
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!storedClient && clientId) {
|
|
80
|
+
fetchClient();
|
|
81
|
+
}
|
|
82
|
+
}, [storedClient, clientId, fetchClient]);
|
|
83
|
+
|
|
84
|
+
const update = useCallback(
|
|
85
|
+
async (data: Partial<OAuthClientInput>): Promise<void> => {
|
|
86
|
+
if (!clientId) throw new Error("No client ID");
|
|
87
|
+
|
|
88
|
+
setIsLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const updated = await OAuthService.updateClient({ clientId, data });
|
|
93
|
+
updateClientInStore(updated);
|
|
94
|
+
setFetchedClient(updated);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("[useOAuthClient] Failed to update client:", err);
|
|
97
|
+
const error = err instanceof Error ? err : new Error("Failed to update OAuth client");
|
|
98
|
+
setError(error);
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[clientId, updateClientInStore],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const deleteClient = useCallback(async (): Promise<void> => {
|
|
108
|
+
if (!clientId) throw new Error("No client ID");
|
|
109
|
+
|
|
110
|
+
setIsLoading(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await OAuthService.deleteClient({ clientId });
|
|
115
|
+
removeClientFromStore(clientId);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error("[useOAuthClient] Failed to delete client:", err);
|
|
118
|
+
const error = err instanceof Error ? err : new Error("Failed to delete OAuth client");
|
|
119
|
+
setError(error);
|
|
120
|
+
throw error;
|
|
121
|
+
} finally {
|
|
122
|
+
setIsLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}, [clientId, removeClientFromStore]);
|
|
125
|
+
|
|
126
|
+
const regenerateSecret = useCallback(async (): Promise<string> => {
|
|
127
|
+
if (!clientId) throw new Error("No client ID");
|
|
128
|
+
|
|
129
|
+
setIsLoading(true);
|
|
130
|
+
setError(null);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await OAuthService.regenerateSecret({ clientId });
|
|
134
|
+
|
|
135
|
+
// Store for one-time display
|
|
136
|
+
setNewClientSecret({
|
|
137
|
+
clientId,
|
|
138
|
+
secret: result.clientSecret,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return result.clientSecret;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("[useOAuthClient] Failed to regenerate secret:", err);
|
|
144
|
+
const error = err instanceof Error ? err : new Error("Failed to regenerate client secret");
|
|
145
|
+
setError(error);
|
|
146
|
+
throw error;
|
|
147
|
+
} finally {
|
|
148
|
+
setIsLoading(false);
|
|
149
|
+
}
|
|
150
|
+
}, [clientId, setNewClientSecret]);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
client,
|
|
154
|
+
isLoading,
|
|
155
|
+
error,
|
|
156
|
+
update,
|
|
157
|
+
deleteClient,
|
|
158
|
+
regenerateSecret,
|
|
159
|
+
refetch: fetchClient,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAtom, useSetAtom } from "jotai";
|
|
4
|
+
import { useCallback, useEffect } from "react";
|
|
5
|
+
import {
|
|
6
|
+
addOAuthClientAtom,
|
|
7
|
+
oauthClientsAtom,
|
|
8
|
+
oauthClientsErrorAtom,
|
|
9
|
+
oauthClientsLoadingAtom,
|
|
10
|
+
setNewClientSecretAtom,
|
|
11
|
+
} from "../atoms/oauth.atoms";
|
|
12
|
+
import {
|
|
13
|
+
OAuthClientCreateRequest,
|
|
14
|
+
OAuthClientCreateResponse,
|
|
15
|
+
OAuthClientInterface,
|
|
16
|
+
} from "../interfaces/oauth.interface";
|
|
17
|
+
import { OAuthService } from "../data/oauth.service";
|
|
18
|
+
|
|
19
|
+
export interface UseOAuthClientsReturn {
|
|
20
|
+
/** List of OAuth clients */
|
|
21
|
+
clients: OAuthClientInterface[];
|
|
22
|
+
/** Whether clients are being loaded */
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
/** Error from last operation */
|
|
25
|
+
error: Error | null;
|
|
26
|
+
/** Refetch clients from API */
|
|
27
|
+
refetch: () => Promise<void>;
|
|
28
|
+
/** Create a new OAuth client */
|
|
29
|
+
createClient: (data: OAuthClientCreateRequest) => Promise<OAuthClientCreateResponse>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook for managing OAuth clients list
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const { clients, isLoading, createClient } = useOAuthClients();
|
|
38
|
+
*
|
|
39
|
+
* const handleCreate = async (data) => {
|
|
40
|
+
* const { client, clientSecret } = await createClient(data);
|
|
41
|
+
* // clientSecret is shown only once!
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useOAuthClients(): UseOAuthClientsReturn {
|
|
46
|
+
const [clients, setClients] = useAtom(oauthClientsAtom);
|
|
47
|
+
const [isLoading, setIsLoading] = useAtom(oauthClientsLoadingAtom);
|
|
48
|
+
const [error, setError] = useAtom(oauthClientsErrorAtom);
|
|
49
|
+
const addClient = useSetAtom(addOAuthClientAtom);
|
|
50
|
+
const setNewClientSecret = useSetAtom(setNewClientSecretAtom);
|
|
51
|
+
|
|
52
|
+
const fetchClients = useCallback(async () => {
|
|
53
|
+
setIsLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const fetchedClients = await OAuthService.listClients();
|
|
58
|
+
setClients(fetchedClients);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error("[useOAuthClients] Failed to fetch clients:", err);
|
|
61
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch OAuth clients"));
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
}
|
|
65
|
+
}, [setClients, setIsLoading, setError]);
|
|
66
|
+
|
|
67
|
+
// Fetch clients on mount
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
fetchClients();
|
|
70
|
+
}, [fetchClients]);
|
|
71
|
+
|
|
72
|
+
const createClient = useCallback(
|
|
73
|
+
async (data: OAuthClientCreateRequest): Promise<OAuthClientCreateResponse> => {
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await OAuthService.createClient(data);
|
|
79
|
+
|
|
80
|
+
// Add to local state
|
|
81
|
+
addClient(result.client);
|
|
82
|
+
|
|
83
|
+
// Store secret for one-time display
|
|
84
|
+
if (result.clientSecret) {
|
|
85
|
+
setNewClientSecret({
|
|
86
|
+
clientId: result.client.clientId,
|
|
87
|
+
secret: result.clientSecret,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error("[useOAuthClients] Failed to create client:", err);
|
|
94
|
+
const error = err instanceof Error ? err : new Error("Failed to create OAuth client");
|
|
95
|
+
setError(error);
|
|
96
|
+
throw error;
|
|
97
|
+
} finally {
|
|
98
|
+
setIsLoading(false);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[addClient, setNewClientSecret, setIsLoading, setError],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
clients,
|
|
106
|
+
isLoading,
|
|
107
|
+
error,
|
|
108
|
+
refetch: fetchClients,
|
|
109
|
+
createClient,
|
|
110
|
+
};
|
|
111
|
+
}
|