@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,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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { OAuthConsentInfo, OAuthConsentRequest } from "../interfaces/oauth.interface";
|
|
5
|
+
import { OAuthService } from "../data/oauth.service";
|
|
6
|
+
|
|
7
|
+
export interface UseOAuthConsentReturn {
|
|
8
|
+
/** Client and scope info for consent display */
|
|
9
|
+
clientInfo: OAuthConsentInfo | null;
|
|
10
|
+
/** Whether consent info is being loaded */
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
/** Error from consent flow */
|
|
13
|
+
error: Error | null;
|
|
14
|
+
/** Approve the authorization request */
|
|
15
|
+
approve: () => Promise<void>;
|
|
16
|
+
/** Deny the authorization request */
|
|
17
|
+
deny: () => Promise<void>;
|
|
18
|
+
/** Whether approve/deny is in progress */
|
|
19
|
+
isSubmitting: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook for managing the OAuth consent flow
|
|
24
|
+
*
|
|
25
|
+
* @param params - OAuth authorization parameters from URL
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const { clientInfo, isLoading, approve, deny } = useOAuthConsent({
|
|
30
|
+
* clientId: searchParams.client_id,
|
|
31
|
+
* redirectUri: searchParams.redirect_uri,
|
|
32
|
+
* scope: searchParams.scope,
|
|
33
|
+
* state: searchParams.state,
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Render consent screen with clientInfo
|
|
37
|
+
* // On button click: approve() or deny()
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useOAuthConsent(params: OAuthConsentRequest): UseOAuthConsentReturn {
|
|
41
|
+
const [clientInfo, setClientInfo] = useState<OAuthConsentInfo | null>(null);
|
|
42
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
43
|
+
const [error, setError] = useState<Error | null>(null);
|
|
44
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
45
|
+
|
|
46
|
+
// Fetch client info on mount
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const fetchInfo = async () => {
|
|
49
|
+
if (!params.clientId || !params.redirectUri || !params.scope) {
|
|
50
|
+
setError(new Error("Missing required authorization parameters"));
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const info = await OAuthService.getAuthorizationInfo(params);
|
|
60
|
+
setClientInfo(info);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("[useOAuthConsent] Failed to fetch authorization info:", err);
|
|
63
|
+
setError(err instanceof Error ? err : new Error("Failed to load authorization info"));
|
|
64
|
+
} finally {
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
fetchInfo();
|
|
70
|
+
}, [
|
|
71
|
+
params.clientId,
|
|
72
|
+
params.redirectUri,
|
|
73
|
+
params.scope,
|
|
74
|
+
params.state,
|
|
75
|
+
params.codeChallenge,
|
|
76
|
+
params.codeChallengeMethod,
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const approve = useCallback(async (): Promise<void> => {
|
|
80
|
+
setIsSubmitting(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await OAuthService.approveAuthorization(params);
|
|
85
|
+
|
|
86
|
+
// Redirect to client with authorization code
|
|
87
|
+
if (result.redirectUrl) {
|
|
88
|
+
window.location.href = result.redirectUrl;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("[useOAuthConsent] Failed to approve authorization:", err);
|
|
92
|
+
setError(err instanceof Error ? err : new Error("Failed to approve authorization"));
|
|
93
|
+
setIsSubmitting(false);
|
|
94
|
+
}
|
|
95
|
+
// Note: Don't set isSubmitting to false on success - we're redirecting
|
|
96
|
+
}, [params]);
|
|
97
|
+
|
|
98
|
+
const deny = useCallback(async (): Promise<void> => {
|
|
99
|
+
setIsSubmitting(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await OAuthService.denyAuthorization(params);
|
|
104
|
+
|
|
105
|
+
// Redirect to client with error
|
|
106
|
+
if (result.redirectUrl) {
|
|
107
|
+
window.location.href = result.redirectUrl;
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error("[useOAuthConsent] Failed to deny authorization:", err);
|
|
111
|
+
setError(err instanceof Error ? err : new Error("Failed to deny authorization"));
|
|
112
|
+
setIsSubmitting(false);
|
|
113
|
+
}
|
|
114
|
+
// Note: Don't set isSubmitting to false on success - we're redirecting
|
|
115
|
+
}, [params]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
clientInfo,
|
|
119
|
+
isLoading,
|
|
120
|
+
error,
|
|
121
|
+
approve,
|
|
122
|
+
deny,
|
|
123
|
+
isSubmitting,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./oauth.interface";
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth client application interface
|
|
5
|
+
* Represents a registered OAuth application that can request access tokens
|
|
6
|
+
*/
|
|
7
|
+
export interface OAuthClientInterface extends ApiDataInterface {
|
|
8
|
+
/** The public client identifier (UUID format) */
|
|
9
|
+
get clientId(): string;
|
|
10
|
+
/** Human-readable application name */
|
|
11
|
+
get name(): string;
|
|
12
|
+
/** Optional description of the application */
|
|
13
|
+
get description(): string | undefined;
|
|
14
|
+
/** Array of allowed redirect URIs (exact match validation) */
|
|
15
|
+
get redirectUris(): string[];
|
|
16
|
+
/** Array of scopes this client can request */
|
|
17
|
+
get allowedScopes(): string[];
|
|
18
|
+
/** Supported grant types (authorization_code, client_credentials, refresh_token) */
|
|
19
|
+
get allowedGrantTypes(): string[];
|
|
20
|
+
/** True for server-side apps (can keep secret secure), false for mobile/desktop apps */
|
|
21
|
+
get isConfidential(): boolean;
|
|
22
|
+
/** Whether the client is currently active */
|
|
23
|
+
get isActive(): boolean;
|
|
24
|
+
/** When the client was created */
|
|
25
|
+
get createdAt(): Date;
|
|
26
|
+
/** When the client was last updated */
|
|
27
|
+
get updatedAt(): Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Input type for OAuth client CRUD operations
|
|
32
|
+
*/
|
|
33
|
+
export type OAuthClientInput = {
|
|
34
|
+
id?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
redirectUris?: string[];
|
|
38
|
+
allowedScopes?: string[];
|
|
39
|
+
allowedGrantTypes?: string[];
|
|
40
|
+
isConfidential?: boolean;
|
|
41
|
+
isActive?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Request body for creating a new OAuth client
|
|
46
|
+
*/
|
|
47
|
+
export interface OAuthClientCreateRequest {
|
|
48
|
+
/** Required: Human-readable application name */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Optional: Description of the application */
|
|
51
|
+
description?: string;
|
|
52
|
+
/** Required: At least one redirect URI */
|
|
53
|
+
redirectUris: string[];
|
|
54
|
+
/** Required: Array of scopes the client needs */
|
|
55
|
+
allowedScopes: string[];
|
|
56
|
+
/** Optional: Grant types (defaults to authorization_code + refresh_token) */
|
|
57
|
+
allowedGrantTypes?: string[];
|
|
58
|
+
/** Required: Whether this is a confidential client */
|
|
59
|
+
isConfidential: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Response when creating a client (includes one-time secret)
|
|
64
|
+
*/
|
|
65
|
+
export interface OAuthClientCreateResponse {
|
|
66
|
+
client: OAuthClientInterface;
|
|
67
|
+
/** Only returned on creation - must be saved immediately */
|
|
68
|
+
clientSecret?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parameters for the OAuth authorization consent flow
|
|
73
|
+
* Passed via URL query parameters to the consent page
|
|
74
|
+
*/
|
|
75
|
+
export interface OAuthConsentRequest {
|
|
76
|
+
/** The client_id requesting authorization */
|
|
77
|
+
clientId: string;
|
|
78
|
+
/** Where to redirect after authorization */
|
|
79
|
+
redirectUri: string;
|
|
80
|
+
/** Space-separated list of requested scopes */
|
|
81
|
+
scope: string;
|
|
82
|
+
/** CSRF protection token (passed back on redirect) */
|
|
83
|
+
state?: string;
|
|
84
|
+
/** PKCE code challenge (required for public clients) */
|
|
85
|
+
codeChallenge?: string;
|
|
86
|
+
/** PKCE method: 'S256' (recommended) or 'plain' */
|
|
87
|
+
codeChallengeMethod?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Scope information for display in consent screen
|
|
92
|
+
*/
|
|
93
|
+
export interface OAuthScopeInfo {
|
|
94
|
+
/** The scope identifier (e.g., 'photographs:read') */
|
|
95
|
+
scope: string;
|
|
96
|
+
/** Human-readable scope name */
|
|
97
|
+
name: string;
|
|
98
|
+
/** Description of what this scope allows */
|
|
99
|
+
description: string;
|
|
100
|
+
/** Optional icon identifier */
|
|
101
|
+
icon?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Client info returned for consent screen display
|
|
106
|
+
*/
|
|
107
|
+
export interface OAuthConsentInfo {
|
|
108
|
+
client: OAuthClientInterface;
|
|
109
|
+
scopes: OAuthScopeInfo[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Default scope display configuration
|
|
114
|
+
* Maps scope identifiers to human-readable info
|
|
115
|
+
*/
|
|
116
|
+
export const OAUTH_SCOPE_DISPLAY: Record<string, OAuthScopeInfo> = {
|
|
117
|
+
read: {
|
|
118
|
+
scope: "read",
|
|
119
|
+
name: "Read Access",
|
|
120
|
+
description: "Read access to your data",
|
|
121
|
+
icon: "eye",
|
|
122
|
+
},
|
|
123
|
+
write: {
|
|
124
|
+
scope: "write",
|
|
125
|
+
name: "Write Access",
|
|
126
|
+
description: "Write access to your data",
|
|
127
|
+
icon: "pencil",
|
|
128
|
+
},
|
|
129
|
+
"photographs:read": {
|
|
130
|
+
scope: "photographs:read",
|
|
131
|
+
name: "View Photographs",
|
|
132
|
+
description: "Access and download your photo library",
|
|
133
|
+
icon: "image",
|
|
134
|
+
},
|
|
135
|
+
"photographs:write": {
|
|
136
|
+
scope: "photographs:write",
|
|
137
|
+
name: "Upload Photographs",
|
|
138
|
+
description: "Add new photos to your rolls",
|
|
139
|
+
icon: "upload",
|
|
140
|
+
},
|
|
141
|
+
"rolls:read": {
|
|
142
|
+
scope: "rolls:read",
|
|
143
|
+
name: "View Rolls",
|
|
144
|
+
description: "See your film rolls and collections",
|
|
145
|
+
icon: "film",
|
|
146
|
+
},
|
|
147
|
+
"rolls:write": {
|
|
148
|
+
scope: "rolls:write",
|
|
149
|
+
name: "Manage Rolls",
|
|
150
|
+
description: "Create and modify film rolls",
|
|
151
|
+
icon: "folder-plus",
|
|
152
|
+
},
|
|
153
|
+
profile: {
|
|
154
|
+
scope: "profile",
|
|
155
|
+
name: "View Profile",
|
|
156
|
+
description: "Access your name and email",
|
|
157
|
+
icon: "user",
|
|
158
|
+
},
|
|
159
|
+
admin: {
|
|
160
|
+
scope: "admin",
|
|
161
|
+
name: "Administrative Access",
|
|
162
|
+
description: "Full administrative access to your account",
|
|
163
|
+
icon: "shield",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Available scopes list for the scope selector
|
|
169
|
+
*/
|
|
170
|
+
export const AVAILABLE_OAUTH_SCOPES: OAuthScopeInfo[] = Object.values(OAUTH_SCOPE_DISPLAY);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Default grant types for new clients
|
|
174
|
+
*/
|
|
175
|
+
export const DEFAULT_GRANT_TYPES = ["authorization_code", "refresh_token"];
|