@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.
Files changed (76) hide show
  1. package/dist/{BlockNoteEditor-OFSTXGZX.js → BlockNoteEditor-7WAWEZVW.js} +13 -13
  2. package/dist/{BlockNoteEditor-OFSTXGZX.js.map → BlockNoteEditor-7WAWEZVW.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-TJNLCNIP.mjs → BlockNoteEditor-UNVKGZ2G.mjs} +3 -3
  4. package/dist/billing/index.js +342 -342
  5. package/dist/billing/index.mjs +2 -2
  6. package/dist/{chunk-H5JZ5E7M.mjs → chunk-6BDOZDZ3.mjs} +1247 -54
  7. package/dist/chunk-6BDOZDZ3.mjs.map +1 -0
  8. package/dist/{chunk-EJALOG7L.js → chunk-JI6BDV7L.js} +1598 -405
  9. package/dist/chunk-JI6BDV7L.js.map +1 -0
  10. package/dist/{chunk-5U4NJJOF.mjs → chunk-LNBT2YPZ.mjs} +289 -2
  11. package/dist/chunk-LNBT2YPZ.mjs.map +1 -0
  12. package/dist/{chunk-NQVPCNRS.js → chunk-O3LLMGP7.js} +290 -3
  13. package/dist/chunk-O3LLMGP7.js.map +1 -0
  14. package/dist/client/index.d.mts +96 -1
  15. package/dist/client/index.d.ts +96 -1
  16. package/dist/client/index.js +9 -3
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/index.mjs +8 -2
  19. package/dist/components/index.d.mts +225 -1
  20. package/dist/components/index.d.ts +225 -1
  21. package/dist/components/index.js +25 -3
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/index.mjs +24 -2
  24. package/dist/contexts/index.js +3 -3
  25. package/dist/contexts/index.mjs +2 -2
  26. package/dist/core/index.d.mts +108 -1
  27. package/dist/core/index.d.ts +108 -1
  28. package/dist/core/index.js +14 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs +13 -1
  31. package/dist/index.d.mts +2 -1
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +14 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +13 -1
  36. package/dist/oauth.interface-DsZ5ecSX.d.mts +119 -0
  37. package/dist/oauth.interface-vL7za9Bz.d.ts +119 -0
  38. package/dist/server/index.js +3 -3
  39. package/dist/server/index.mjs +1 -1
  40. package/package.json +3 -2
  41. package/src/client/index.ts +1 -0
  42. package/src/components/index.ts +1 -0
  43. package/src/core/index.ts +3 -0
  44. package/src/core/registry/ModuleRegistry.ts +2 -0
  45. package/src/features/index.ts +1 -0
  46. package/src/features/oauth/atoms/index.ts +1 -0
  47. package/src/features/oauth/atoms/oauth.atoms.ts +131 -0
  48. package/src/features/oauth/components/OAuthClientCard.tsx +105 -0
  49. package/src/features/oauth/components/OAuthClientDetail.tsx +269 -0
  50. package/src/features/oauth/components/OAuthClientForm.tsx +212 -0
  51. package/src/features/oauth/components/OAuthClientList.tsx +127 -0
  52. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +127 -0
  53. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +152 -0
  54. package/src/features/oauth/components/OAuthScopeSelector.tsx +123 -0
  55. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +41 -0
  56. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +51 -0
  57. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +142 -0
  58. package/src/features/oauth/components/consent/OAuthScopeList.tsx +72 -0
  59. package/src/features/oauth/components/consent/index.ts +4 -0
  60. package/src/features/oauth/components/index.ts +8 -0
  61. package/src/features/oauth/data/index.ts +2 -0
  62. package/src/features/oauth/data/oauth.service.ts +191 -0
  63. package/src/features/oauth/data/oauth.ts +87 -0
  64. package/src/features/oauth/hooks/index.ts +3 -0
  65. package/src/features/oauth/hooks/useOAuthClient.ts +161 -0
  66. package/src/features/oauth/hooks/useOAuthClients.ts +111 -0
  67. package/src/features/oauth/hooks/useOAuthConsent.ts +125 -0
  68. package/src/features/oauth/index.ts +6 -0
  69. package/src/features/oauth/interfaces/index.ts +1 -0
  70. package/src/features/oauth/interfaces/oauth.interface.ts +175 -0
  71. package/src/features/oauth/oauth.module.ts +9 -0
  72. package/dist/chunk-5U4NJJOF.mjs.map +0 -1
  73. package/dist/chunk-EJALOG7L.js.map +0 -1
  74. package/dist/chunk-H5JZ5E7M.mjs.map +0 -1
  75. package/dist/chunk-NQVPCNRS.js.map +0 -1
  76. /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,4 @@
1
+ export * from "./OAuthConsentHeader";
2
+ export * from "./OAuthScopeList";
3
+ export * from "./OAuthConsentActions";
4
+ export * from "./OAuthConsentScreen";
@@ -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,2 @@
1
+ export * from "./oauth";
2
+ export * from "./oauth.service";
@@ -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,3 @@
1
+ export * from "./useOAuthClients";
2
+ export * from "./useOAuthClient";
3
+ export * from "./useOAuthConsent";
@@ -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
+ }