@carlonicora/nextjs-jsonapi 3.23.0 → 3.24.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/{AssistantMessageInterface-CuaMDs2i.d.ts → AssistantMessageInterface-1Kbz8RVs.d.ts} +34 -1
- package/dist/{AssistantMessageInterface-Du3ShThp.d.mts → AssistantMessageInterface-CoxJyuFM.d.mts} +34 -1
- package/dist/{BlockNoteEditor-BUSOUPQ7.js → BlockNoteEditor-PTD2V4CH.js} +19 -19
- package/dist/{BlockNoteEditor-BUSOUPQ7.js.map → BlockNoteEditor-PTD2V4CH.js.map} +1 -1
- package/dist/{BlockNoteEditor-W42IQTGL.mjs → BlockNoteEditor-Q2WEDMKJ.mjs} +4 -4
- package/dist/{BlockNoteViewer-FQ23TZ3J.mjs → BlockNoteViewer-WO6GTDSI.mjs} +2 -2
- package/dist/{BlockNoteViewer-MCX53AR4.js → BlockNoteViewer-WOXEJ6RM.js} +3 -3
- package/dist/{BlockNoteViewer-MCX53AR4.js.map → BlockNoteViewer-WOXEJ6RM.js.map} +1 -1
- package/dist/billing/index.js +401 -401
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-O5MKEU7G.mjs → chunk-2O3SFHQ7.mjs} +36 -38
- package/dist/{chunk-O5MKEU7G.mjs.map → chunk-2O3SFHQ7.mjs.map} +1 -1
- package/dist/{chunk-FXWO5J2R.js → chunk-3D2MCVRZ.js} +7 -7
- package/dist/{chunk-FXWO5J2R.js.map → chunk-3D2MCVRZ.js.map} +1 -1
- package/dist/{chunk-VR65NRE4.js → chunk-3YJF7QY4.js} +1338 -1115
- package/dist/chunk-3YJF7QY4.js.map +1 -0
- package/dist/{chunk-GFLEFIAW.mjs → chunk-GJHKOG5L.mjs} +1399 -1176
- package/dist/chunk-GJHKOG5L.mjs.map +1 -0
- package/dist/{chunk-JZUBFAAY.mjs → chunk-HAI7G6MD.mjs} +2 -2
- package/dist/{chunk-YCHPWXHD.js → chunk-IV4DTYXN.js} +36 -38
- package/dist/chunk-IV4DTYXN.js.map +1 -0
- package/dist/client/index.d.mts +13 -2
- package/dist/client/index.d.ts +13 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +87 -28
- package/dist/components/index.d.ts +87 -28
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +7 -3
- package/dist/contexts/index.d.mts +11 -2
- package/dist/contexts/index.d.ts +11 -2
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +27 -5
- package/dist/core/index.d.ts +27 -5
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{oauth.interface-CD90ycYz.d.ts → oauth.interface-BZKZAPRw.d.ts} +8 -1
- package/dist/{oauth.interface-DRE3NAKc.d.mts → oauth.interface-nVwnzxJw.d.mts} +8 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/forms/__tests__/EditorSheet.test.tsx +5 -3
- package/src/core/utils/__tests__/translateResponse.test.ts +3 -1
- package/src/features/assistant/components/containers/AssistantContainer.tsx +105 -36
- package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +163 -1
- package/src/features/assistant/components/parts/AssistantComposer.tsx +38 -15
- package/src/features/assistant/components/parts/AssistantEmptyState.tsx +5 -3
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +26 -7
- package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +8 -0
- package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +7 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +91 -7
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +196 -0
- package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +23 -1
- package/src/features/assistant-message/components/EntityMentionLink.tsx +28 -0
- package/src/features/assistant-message/components/__tests__/EntityMentionLink.spec.tsx +58 -0
- package/src/features/assistant-message/components/index.ts +1 -0
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +1 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -2
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +3 -2
- package/src/features/oauth/components/OAuthClientForm.tsx +21 -4
- package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +63 -0
- package/src/features/oauth/components/consent/OAuthCompanySelector.tsx +54 -0
- package/src/features/oauth/components/consent/OAuthConsentActions.tsx +5 -2
- package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +10 -6
- package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +33 -13
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -1
- package/src/features/oauth/components/consent/__tests__/OAuthConsentHeader.spec.tsx +25 -0
- package/src/features/oauth/components/consent/__tests__/OAuthConsentScreen.spec.tsx +126 -0
- package/src/features/oauth/components/consent/index.ts +1 -0
- package/src/features/oauth/data/__tests__/oauth.service.spec.ts +103 -0
- package/src/features/oauth/data/oauth.service.ts +3 -13
- package/src/features/oauth/data/oauth.ts +30 -1
- package/src/features/oauth/hooks/useOAuthConsent.ts +34 -3
- package/src/features/oauth/interfaces/oauth.interface.ts +11 -25
- package/src/hooks/__tests__/useDataListRetriever.test.ts +1 -1
- package/dist/AssistantInterface-RvZzlcA_.d.ts +0 -36
- package/dist/AssistantInterface-jL56dyNc.d.mts +0 -36
- package/dist/chunk-GFLEFIAW.mjs.map +0 -1
- package/dist/chunk-VR65NRE4.js.map +0 -1
- package/dist/chunk-YCHPWXHD.js.map +0 -1
- /package/dist/{BlockNoteEditor-W42IQTGL.mjs.map → BlockNoteEditor-Q2WEDMKJ.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-FQ23TZ3J.mjs.map → BlockNoteViewer-WO6GTDSI.mjs.map} +0 -0
- /package/dist/{chunk-JZUBFAAY.mjs.map → chunk-HAI7G6MD.mjs.map} +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
4
|
+
import { configureI18n } from "../../../../i18n";
|
|
5
|
+
import { EntityMentionLink } from "../EntityMentionLink";
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
// The package Link resolves its inner component at runtime and throws if i18n
|
|
9
|
+
// was never configured (src/i18n/config.ts).
|
|
10
|
+
configureI18n({
|
|
11
|
+
useRouter: () => ({
|
|
12
|
+
push: vi.fn(),
|
|
13
|
+
replace: vi.fn(),
|
|
14
|
+
back: vi.fn(),
|
|
15
|
+
forward: vi.fn(),
|
|
16
|
+
refresh: vi.fn(),
|
|
17
|
+
prefetch: vi.fn(),
|
|
18
|
+
}),
|
|
19
|
+
useTranslations: () => (key: string) => key,
|
|
20
|
+
usePathname: () => "/",
|
|
21
|
+
Link: ({
|
|
22
|
+
href,
|
|
23
|
+
children,
|
|
24
|
+
prefetch: _prefetch,
|
|
25
|
+
...rest
|
|
26
|
+
}: {
|
|
27
|
+
href: string;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}) => (
|
|
31
|
+
<a href={href} {...rest}>
|
|
32
|
+
{children}
|
|
33
|
+
</a>
|
|
34
|
+
),
|
|
35
|
+
});
|
|
36
|
+
ModuleRegistry.register("Account" as any, { name: "accounts", pageUrl: "/accounts" } as any);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("EntityMentionLink", () => {
|
|
40
|
+
it("renders a link to the entity page for a registered module", () => {
|
|
41
|
+
render(<>{EntityMentionLink({ type: "accounts", id: "a1", alias: "Rossi" })}</>);
|
|
42
|
+
const link = screen.getByRole("link", { name: "Rossi" });
|
|
43
|
+
expect(link).toHaveAttribute("href", "/accounts/a1");
|
|
44
|
+
expect(link).toHaveTextContent("Rossi");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders through the package Link (locale-aware, link typography role)", () => {
|
|
48
|
+
render(<>{EntityMentionLink({ type: "accounts", id: "a1", alias: "Rossi" })}</>);
|
|
49
|
+
const link = screen.getByRole("link", { name: "Rossi" });
|
|
50
|
+
expect(link.className).toContain("text-primary font-medium");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders plain text with no link for an unknown type", () => {
|
|
54
|
+
render(<>{EntityMentionLink({ type: "unknown-things", id: "x1", alias: "Mystery" })}</>);
|
|
55
|
+
expect(screen.getByText("Mystery")).toBeInTheDocument();
|
|
56
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import Link from "next/link";
|
|
5
4
|
import type { ApiDataInterface } from "../../../../../core";
|
|
6
5
|
import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
|
|
7
6
|
import { usePageUrlGenerator } from "../../../../../hooks";
|
|
8
7
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
|
|
9
8
|
import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
|
|
10
9
|
import { useEntityLabel } from "./useEntityLabel";
|
|
10
|
+
import { Link } from "../../../../../shadcnui/custom/link";
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
citations: (ChunkInterface & ChunkRelationshipMeta)[];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import Link from "next/link";
|
|
5
4
|
import type { ApiDataInterface } from "../../../../../core";
|
|
6
5
|
import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
|
|
7
6
|
import { usePageUrlGenerator } from "../../../../../hooks";
|
|
8
7
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
|
|
9
8
|
import { useEntityLabel } from "./useEntityLabel";
|
|
9
|
+
import { Link } from "../../../../../shadcnui/custom/link";
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
references: ApiDataInterface[];
|
|
@@ -42,7 +42,7 @@ export function ReferencesTab({ references }: Props) {
|
|
|
42
42
|
return (
|
|
43
43
|
<TableRow key={`${ref.type}/${ref.id}`}>
|
|
44
44
|
<TableCell>
|
|
45
|
-
<Link href={href} target="_blank" rel="noopener noreferrer"
|
|
45
|
+
<Link href={href} target="_blank" rel="noopener noreferrer">
|
|
46
46
|
{ref.identifier}
|
|
47
47
|
</Link>
|
|
48
48
|
</TableCell>
|
|
@@ -37,7 +37,8 @@ describe("Logout", () => {
|
|
|
37
37
|
// We need to delete the original location first
|
|
38
38
|
// @ts-expect-error - deleting window.location for mocking purposes
|
|
39
39
|
delete window.location;
|
|
40
|
-
window.location
|
|
40
|
+
// The DOM lib types `window.location` as accessor-only; jsdom lets the test replace it.
|
|
41
|
+
(window as unknown as { location: Location }).location = {
|
|
41
42
|
...originalLocation,
|
|
42
43
|
get href() {
|
|
43
44
|
return windowLocationHref;
|
|
@@ -50,7 +51,7 @@ describe("Logout", () => {
|
|
|
50
51
|
|
|
51
52
|
afterEach(() => {
|
|
52
53
|
// Restore original location
|
|
53
|
-
window.location = originalLocation;
|
|
54
|
+
(window as unknown as { location: Location }).location = originalLocation;
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
describe("Scenario: Clears storage when storageKeys provided", () => {
|
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
} from "../../../shadcnui";
|
|
18
18
|
import { OAuthRedirectUriInput } from "./OAuthRedirectUriInput";
|
|
19
19
|
import { OAuthScopeSelector } from "./OAuthScopeSelector";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
OAuthClientCreateRequest,
|
|
22
|
+
OAuthClientInterface,
|
|
23
|
+
OAuthScopeInfo,
|
|
24
|
+
DEFAULT_GRANT_TYPES,
|
|
25
|
+
} from "../interfaces/oauth.interface";
|
|
21
26
|
|
|
22
27
|
export interface OAuthClientFormProps {
|
|
23
28
|
/** Existing client for edit mode (undefined = create mode) */
|
|
@@ -28,6 +33,10 @@ export interface OAuthClientFormProps {
|
|
|
28
33
|
onCancel: () => void;
|
|
29
34
|
/** Whether form is submitting */
|
|
30
35
|
isLoading?: boolean;
|
|
36
|
+
/** Scopes the app offers in the selector (defaults to AVAILABLE_OAUTH_SCOPES) */
|
|
37
|
+
availableScopes?: OAuthScopeInfo[];
|
|
38
|
+
/** Scopes preselected in create mode (edit mode uses client.allowedScopes) */
|
|
39
|
+
defaultScopes?: string[];
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
interface FormState {
|
|
@@ -47,14 +56,21 @@ interface FormErrors {
|
|
|
47
56
|
/**
|
|
48
57
|
* Form for creating or editing an OAuth client
|
|
49
58
|
*/
|
|
50
|
-
export function OAuthClientForm({
|
|
59
|
+
export function OAuthClientForm({
|
|
60
|
+
client,
|
|
61
|
+
onSubmit,
|
|
62
|
+
onCancel,
|
|
63
|
+
isLoading = false,
|
|
64
|
+
availableScopes,
|
|
65
|
+
defaultScopes = [],
|
|
66
|
+
}: OAuthClientFormProps) {
|
|
51
67
|
const isEditMode = !!client;
|
|
52
68
|
|
|
53
69
|
const [formState, setFormState] = useState<FormState>({
|
|
54
70
|
name: client?.name || "",
|
|
55
71
|
description: client?.description || "",
|
|
56
72
|
redirectUris: client?.redirectUris?.length ? client.redirectUris : [""],
|
|
57
|
-
allowedScopes: client
|
|
73
|
+
allowedScopes: client ? client.allowedScopes || [] : defaultScopes,
|
|
58
74
|
isConfidential: client?.isConfidential ?? true,
|
|
59
75
|
});
|
|
60
76
|
|
|
@@ -117,7 +133,7 @@ export function OAuthClientForm({ client, onSubmit, onCancel, isLoading = false
|
|
|
117
133
|
id="name"
|
|
118
134
|
value={formState.name}
|
|
119
135
|
onChange={(e) => setFormState((s) => ({ ...s, name: e.target.value }))}
|
|
120
|
-
placeholder="My
|
|
136
|
+
placeholder="My Application"
|
|
121
137
|
disabled={isLoading}
|
|
122
138
|
className={errors.name ? "border-destructive" : ""}
|
|
123
139
|
/>
|
|
@@ -149,6 +165,7 @@ export function OAuthClientForm({ client, onSubmit, onCancel, isLoading = false
|
|
|
149
165
|
<OAuthScopeSelector
|
|
150
166
|
value={formState.allowedScopes}
|
|
151
167
|
onChange={(scopes) => setFormState((s) => ({ ...s, allowedScopes: scopes }))}
|
|
168
|
+
availableScopes={availableScopes}
|
|
152
169
|
error={errors.allowedScopes}
|
|
153
170
|
disabled={isLoading}
|
|
154
171
|
/>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { OAuthClientForm } from "../OAuthClientForm";
|
|
4
|
+
import { OAUTH_SCOPE_DISPLAY, OAuthClientInterface } from "../../interfaces/oauth.interface";
|
|
5
|
+
|
|
6
|
+
const scopeCheckbox = (name: string) => screen.getByRole("checkbox", { name: new RegExp(name) });
|
|
7
|
+
|
|
8
|
+
const makeClient = (allowedScopes: string[]): OAuthClientInterface =>
|
|
9
|
+
({
|
|
10
|
+
name: "Existing app",
|
|
11
|
+
description: undefined,
|
|
12
|
+
redirectUris: ["https://example.com/callback"],
|
|
13
|
+
allowedScopes,
|
|
14
|
+
isConfidential: true,
|
|
15
|
+
}) as unknown as OAuthClientInterface;
|
|
16
|
+
|
|
17
|
+
describe("OAuthClientForm", () => {
|
|
18
|
+
it("renders only the scopes passed in availableScopes", () => {
|
|
19
|
+
render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} availableScopes={[OAUTH_SCOPE_DISPLAY.mcp]} />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText("MCP Server Access")).toBeTruthy();
|
|
22
|
+
expect(screen.queryByText("Read Access")).toBeNull();
|
|
23
|
+
expect(screen.queryByText("Write Access")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("preselects defaultScopes in create mode", () => {
|
|
27
|
+
render(
|
|
28
|
+
<OAuthClientForm
|
|
29
|
+
onSubmit={vi.fn()}
|
|
30
|
+
onCancel={vi.fn()}
|
|
31
|
+
availableScopes={[OAUTH_SCOPE_DISPLAY.read, OAUTH_SCOPE_DISPLAY.mcp]}
|
|
32
|
+
defaultScopes={["mcp"]}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("true");
|
|
37
|
+
expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("false");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("uses client.allowedScopes instead of defaultScopes in edit mode", () => {
|
|
41
|
+
render(
|
|
42
|
+
<OAuthClientForm
|
|
43
|
+
client={makeClient(["read"])}
|
|
44
|
+
onSubmit={vi.fn()}
|
|
45
|
+
onCancel={vi.fn()}
|
|
46
|
+
availableScopes={[OAUTH_SCOPE_DISPLAY.read, OAUTH_SCOPE_DISPLAY.mcp]}
|
|
47
|
+
defaultScopes={["mcp"]}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("true");
|
|
52
|
+
expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("false");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("offers the generic scope list by default, without Only35 scopes", () => {
|
|
56
|
+
render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} />);
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText("MCP Server Access")).toBeTruthy();
|
|
59
|
+
expect(screen.getByText("Read Access")).toBeTruthy();
|
|
60
|
+
expect(screen.queryByText("View Photographs")).toBeNull();
|
|
61
|
+
expect(screen.queryByText("View Rolls")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../shadcnui";
|
|
6
|
+
import { MicroLabel } from "../../../../components/typography";
|
|
7
|
+
|
|
8
|
+
export interface OAuthCompanySelectorProps {
|
|
9
|
+
/** The user's companies (studios) */
|
|
10
|
+
companies: Array<{ id: string; name: string }>;
|
|
11
|
+
/** The selected company id */
|
|
12
|
+
value: string | undefined;
|
|
13
|
+
/** Called with the newly selected company id */
|
|
14
|
+
onChange: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Studio (company) picker for the OAuth consent screen.
|
|
19
|
+
* Renders nothing when the user belongs to a single company.
|
|
20
|
+
*/
|
|
21
|
+
export function OAuthCompanySelector({ companies, value, onChange }: OAuthCompanySelectorProps) {
|
|
22
|
+
const t = useTranslations();
|
|
23
|
+
|
|
24
|
+
const items = useMemo<Record<string, string>>(
|
|
25
|
+
() => Object.fromEntries(companies.map((company) => [company.id, company.name])),
|
|
26
|
+
[companies],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (companies.length <= 1) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-3">
|
|
33
|
+
<MicroLabel as="h3">{t("oauth.consent.company_label")}</MicroLabel>
|
|
34
|
+
<Select
|
|
35
|
+
items={items}
|
|
36
|
+
value={value ?? null}
|
|
37
|
+
onValueChange={(selected) => {
|
|
38
|
+
if (selected) onChange(String(selected));
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<SelectTrigger className="w-full" aria-label={t("oauth.consent.company_label")}>
|
|
42
|
+
<SelectValue placeholder={t("oauth.consent.company_label")} />
|
|
43
|
+
</SelectTrigger>
|
|
44
|
+
<SelectContent>
|
|
45
|
+
{companies.map((company) => (
|
|
46
|
+
<SelectItem key={company.id} value={company.id}>
|
|
47
|
+
{company.name}
|
|
48
|
+
</SelectItem>
|
|
49
|
+
))}
|
|
50
|
+
</SelectContent>
|
|
51
|
+
</Select>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { Button } from "../../../../shadcnui";
|
|
4
5
|
|
|
5
6
|
export interface OAuthConsentActionsProps {
|
|
@@ -15,13 +16,15 @@ export interface OAuthConsentActionsProps {
|
|
|
15
16
|
* Action buttons for OAuth consent screen
|
|
16
17
|
*/
|
|
17
18
|
export function OAuthConsentActions({ onApprove, onDeny, isLoading = false }: OAuthConsentActionsProps) {
|
|
19
|
+
const t = useTranslations();
|
|
20
|
+
|
|
18
21
|
return (
|
|
19
22
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
20
23
|
<Button variant="outline" onClick={onDeny} disabled={isLoading} className="flex-1">
|
|
21
|
-
|
|
24
|
+
{t("oauth.consent.deny")}
|
|
22
25
|
</Button>
|
|
23
26
|
<Button onClick={onApprove} disabled={isLoading} className="flex-1">
|
|
24
|
-
{isLoading ? "
|
|
27
|
+
{isLoading ? t("oauth.consent.authorizing") : t("oauth.consent.authorize")}
|
|
25
28
|
</Button>
|
|
26
29
|
</div>
|
|
27
30
|
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Shield } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ContentTitle } from "../../../../components";
|
|
4
6
|
import { OAuthClientInterface } from "../../interfaces/oauth.interface";
|
|
5
7
|
|
|
6
8
|
export interface OAuthConsentHeaderProps {
|
|
@@ -8,15 +10,17 @@ export interface OAuthConsentHeaderProps {
|
|
|
8
10
|
client: OAuthClientInterface;
|
|
9
11
|
/** Optional logo URL override */
|
|
10
12
|
logoUrl?: string;
|
|
11
|
-
/** Application name
|
|
12
|
-
appName
|
|
13
|
+
/** Application name shown in the header */
|
|
14
|
+
appName: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Header component for OAuth consent screen
|
|
17
19
|
* Shows platform logo and requesting app information
|
|
18
20
|
*/
|
|
19
|
-
export function OAuthConsentHeader({ client, logoUrl, appName
|
|
21
|
+
export function OAuthConsentHeader({ client, logoUrl, appName }: OAuthConsentHeaderProps) {
|
|
22
|
+
const t = useTranslations();
|
|
23
|
+
|
|
20
24
|
return (
|
|
21
25
|
<div className="text-center space-y-4">
|
|
22
26
|
{/* Platform Logo */}
|
|
@@ -32,9 +36,9 @@ export function OAuthConsentHeader({ client, logoUrl, appName = "Only35" }: OAut
|
|
|
32
36
|
|
|
33
37
|
{/* Authorization Request */}
|
|
34
38
|
<div className="space-y-2">
|
|
35
|
-
<
|
|
36
|
-
<p className="text-muted-foreground">
|
|
37
|
-
|
|
39
|
+
<ContentTitle element={t("oauth.consent.title", { client: client.name })} className="mb-0 justify-center" />
|
|
40
|
+
<p className="text-muted-foreground text-sm">
|
|
41
|
+
{t("oauth.consent.wants_access", { client: client.name, appName })}
|
|
38
42
|
</p>
|
|
39
43
|
</div>
|
|
40
44
|
</div>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ExternalLink, AlertTriangle, Loader2 } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
4
5
|
import { Card, CardContent, CardFooter, Separator, Alert, AlertDescription } from "../../../../shadcnui";
|
|
5
6
|
import { OAuthConsentHeader } from "./OAuthConsentHeader";
|
|
6
7
|
import { OAuthScopeList } from "./OAuthScopeList";
|
|
7
8
|
import { OAuthConsentActions } from "./OAuthConsentActions";
|
|
9
|
+
import { OAuthCompanySelector } from "./OAuthCompanySelector";
|
|
8
10
|
import { useOAuthConsent } from "../../hooks/useOAuthConsent";
|
|
9
11
|
import { OAuthConsentRequest } from "../../interfaces/oauth.interface";
|
|
10
12
|
|
|
@@ -13,12 +15,17 @@ export interface OAuthConsentScreenProps {
|
|
|
13
15
|
params: OAuthConsentRequest;
|
|
14
16
|
/** Optional platform logo URL */
|
|
15
17
|
logoUrl?: string;
|
|
16
|
-
/** Platform name */
|
|
17
|
-
appName
|
|
18
|
+
/** Platform name, shown on the consent screen */
|
|
19
|
+
appName: string;
|
|
18
20
|
/** Terms of Service URL */
|
|
19
21
|
termsUrl?: string;
|
|
20
22
|
/** Privacy Policy URL */
|
|
21
23
|
privacyUrl?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The studio open in the host app's session, used as the default studio
|
|
26
|
+
* choice. Needed by apps that do not mount the package UserProvider.
|
|
27
|
+
*/
|
|
28
|
+
defaultCompanyId?: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/**
|
|
@@ -40,11 +47,16 @@ export interface OAuthConsentScreenProps {
|
|
|
40
47
|
export function OAuthConsentScreen({
|
|
41
48
|
params,
|
|
42
49
|
logoUrl,
|
|
43
|
-
appName
|
|
50
|
+
appName,
|
|
44
51
|
termsUrl = "/terms",
|
|
45
52
|
privacyUrl = "/privacy",
|
|
53
|
+
defaultCompanyId,
|
|
46
54
|
}: OAuthConsentScreenProps) {
|
|
47
|
-
const
|
|
55
|
+
const t = useTranslations();
|
|
56
|
+
const { clientInfo, isLoading, error, approve, deny, isSubmitting, companyId, setCompanyId } = useOAuthConsent(
|
|
57
|
+
params,
|
|
58
|
+
{ defaultCompanyId },
|
|
59
|
+
);
|
|
48
60
|
|
|
49
61
|
// Loading state
|
|
50
62
|
if (isLoading) {
|
|
@@ -53,7 +65,7 @@ export function OAuthConsentScreen({
|
|
|
53
65
|
<Card className="w-full max-w-md">
|
|
54
66
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
55
67
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
56
|
-
<p className="mt-4 text-muted-foreground">
|
|
68
|
+
<p className="mt-4 text-muted-foreground text-sm">{t("oauth.consent.loading")}</p>
|
|
57
69
|
</CardContent>
|
|
58
70
|
</Card>
|
|
59
71
|
</div>
|
|
@@ -68,9 +80,7 @@ export function OAuthConsentScreen({
|
|
|
68
80
|
<CardContent className="py-8">
|
|
69
81
|
<Alert variant="destructive">
|
|
70
82
|
<AlertTriangle className="h-4 w-4" />
|
|
71
|
-
<AlertDescription>
|
|
72
|
-
{error?.message || "Invalid authorization request. Please try again."}
|
|
73
|
-
</AlertDescription>
|
|
83
|
+
<AlertDescription>{error?.message || t("oauth.consent.invalid_request")}</AlertDescription>
|
|
74
84
|
</Alert>
|
|
75
85
|
</CardContent>
|
|
76
86
|
</Card>
|
|
@@ -79,6 +89,7 @@ export function OAuthConsentScreen({
|
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
const { client, scopes } = clientInfo;
|
|
92
|
+
const companies = clientInfo.companies ?? [];
|
|
82
93
|
|
|
83
94
|
return (
|
|
84
95
|
<div className="min-h-dvh flex items-center justify-center p-4 bg-muted/30">
|
|
@@ -94,11 +105,20 @@ export function OAuthConsentScreen({
|
|
|
94
105
|
|
|
95
106
|
<Separator />
|
|
96
107
|
|
|
108
|
+
{/* Studio */}
|
|
109
|
+
{companies.length > 1 && (
|
|
110
|
+
<>
|
|
111
|
+
<OAuthCompanySelector companies={companies} value={companyId} onChange={setCompanyId} />
|
|
112
|
+
|
|
113
|
+
<Separator />
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
|
|
97
117
|
{/* Redirect URI Notice */}
|
|
98
118
|
<div className="flex items-start gap-2 text-sm text-muted-foreground">
|
|
99
119
|
<ExternalLink className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
100
120
|
<div>
|
|
101
|
-
<span>
|
|
121
|
+
<span>{t("oauth.consent.redirect_notice")}</span>
|
|
102
122
|
<p className="tabular-nums text-xs mt-1 break-all">{params.redirectUri}</p>
|
|
103
123
|
</div>
|
|
104
124
|
</div>
|
|
@@ -110,13 +130,13 @@ export function OAuthConsentScreen({
|
|
|
110
130
|
{/* Footer */}
|
|
111
131
|
<CardFooter className="justify-center">
|
|
112
132
|
<p className="text-xs text-center text-muted-foreground">
|
|
113
|
-
|
|
133
|
+
{t("oauth.consent.agreement")}{" "}
|
|
114
134
|
<a href={termsUrl} className="hover:text-foreground" target="_blank" rel="noopener">
|
|
115
|
-
|
|
135
|
+
{t("oauth.consent.terms")}
|
|
116
136
|
</a>{" "}
|
|
117
|
-
and{" "}
|
|
137
|
+
{t("oauth.consent.and")}{" "}
|
|
118
138
|
<a href={privacyUrl} className="hover:text-foreground" target="_blank" rel="noopener">
|
|
119
|
-
|
|
139
|
+
{t("oauth.consent.privacy")}
|
|
120
140
|
</a>
|
|
121
141
|
.
|
|
122
142
|
</p>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { Eye, Pencil, Image, Upload, Film, FolderPlus, User, Shield, LucideIcon } from "lucide-react";
|
|
4
5
|
import { OAuthScopeInfo } from "../../interfaces/oauth.interface";
|
|
5
6
|
import { MicroLabel } from "../../../../components/typography";
|
|
@@ -25,13 +26,15 @@ const SCOPE_ICONS: Record<string, LucideIcon> = {
|
|
|
25
26
|
* List of requested OAuth scopes for consent display
|
|
26
27
|
*/
|
|
27
28
|
export function OAuthScopeList({ scopes }: OAuthScopeListProps) {
|
|
29
|
+
const t = useTranslations();
|
|
30
|
+
|
|
28
31
|
if (scopes.length === 0) {
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
return (
|
|
33
36
|
<div className="space-y-3">
|
|
34
|
-
<MicroLabel as="h3">
|
|
37
|
+
<MicroLabel as="h3">{t("oauth.consent.scopes_heading")}</MicroLabel>
|
|
35
38
|
<ul className="space-y-3">
|
|
36
39
|
{scopes.map((scope) => {
|
|
37
40
|
const IconComponent = scope.icon ? SCOPE_ICONS[scope.icon] : Eye;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { OAuthConsentHeader } from "../OAuthConsentHeader";
|
|
4
|
+
|
|
5
|
+
const CLIENT = { id: "client-1", name: "Claude" } as any;
|
|
6
|
+
|
|
7
|
+
describe("OAuthConsentHeader", () => {
|
|
8
|
+
it("renders the consent title through ContentTitle (page-title recipe), not a raw <h1>", () => {
|
|
9
|
+
const { container } = render(<OAuthConsentHeader client={CLIENT} appName="Test" />);
|
|
10
|
+
|
|
11
|
+
expect(container.querySelector("h1")).toBeNull();
|
|
12
|
+
const title = screen.getByText("oauth.consent.title");
|
|
13
|
+
expect(title.className).toContain("text-primary text-3xl font-semibold");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("keeps the title centred with no extra bottom margin", () => {
|
|
17
|
+
const { container } = render(<OAuthConsentHeader client={CLIENT} appName="Test" />);
|
|
18
|
+
|
|
19
|
+
const wrapper = screen.getByText("oauth.consent.title").parentElement?.parentElement as HTMLElement;
|
|
20
|
+
expect(wrapper.className).toContain("justify-center");
|
|
21
|
+
expect(wrapper.className).toContain("mb-0");
|
|
22
|
+
expect(wrapper.className).not.toContain("mb-4");
|
|
23
|
+
expect(container.firstElementChild?.className).toContain("text-center");
|
|
24
|
+
});
|
|
25
|
+
});
|