@canva/cli 0.0.1-beta.1 → 0.0.1-beta.10
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/README.md +184 -108
- package/cli.js +446 -359
- package/lib/cjs/index.cjs +392 -0
- package/lib/esm/index.esm +392 -0
- package/package.json +11 -2
- package/templates/base/backend/routers/oauth.ts +393 -0
- package/templates/base/eslint.config.mjs +5 -277
- package/templates/base/package.json +26 -23
- package/templates/base/scripts/copy-env.ts +10 -0
- package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/base/utils/use_add_element.ts +48 -0
- package/templates/base/utils/use_feature_support.ts +28 -0
- package/templates/base/webpack.config.cjs +3 -1
- package/templates/common/.gitignore.template +5 -6
- package/templates/common/.nvmrc +1 -0
- package/templates/common/.prettierrc +21 -0
- package/templates/common/.vscode/extensions.json +6 -0
- package/templates/common/README.md +4 -74
- package/templates/common/conf/eslint-general.mjs +303 -0
- package/templates/common/conf/eslint-i18n.mjs +41 -0
- package/templates/common/conf/eslint-local-i18n-rules/index.mjs +181 -0
- package/templates/common/jest.config.mjs +29 -2
- package/templates/common/jest.setup.ts +19 -0
- package/templates/common/utils/backend/base_backend/create.ts +104 -0
- package/templates/common/utils/table_wrapper.ts +477 -0
- package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
- package/templates/common/utils/use_add_element.ts +48 -0
- package/templates/common/utils/use_feature_support.ts +28 -0
- package/templates/common/utils/use_overlay_hook.ts +74 -0
- package/templates/common/utils/use_selection_hook.ts +37 -0
- package/templates/dam/backend/server.ts +0 -7
- package/templates/dam/eslint.config.mjs +6 -275
- package/templates/dam/package.json +43 -35
- package/templates/dam/scripts/copy-env.ts +10 -0
- package/templates/dam/src/app.tsx +2 -135
- package/templates/dam/webpack.config.cjs +3 -1
- package/templates/gen_ai/README.md +1 -1
- package/templates/gen_ai/backend/routers/image.ts +3 -3
- package/templates/gen_ai/backend/server.ts +0 -7
- package/templates/gen_ai/eslint.config.mjs +5 -277
- package/templates/gen_ai/package.json +46 -38
- package/templates/gen_ai/scripts/copy-env.ts +10 -0
- package/templates/gen_ai/src/api/api.ts +1 -39
- package/templates/gen_ai/src/app.tsx +16 -10
- package/templates/gen_ai/src/components/footer.messages.ts +0 -5
- package/templates/gen_ai/src/components/footer.tsx +2 -16
- package/templates/gen_ai/src/components/image_grid.tsx +9 -7
- package/templates/gen_ai/src/components/index.ts +0 -1
- package/templates/gen_ai/src/components/prompt_input.tsx +2 -0
- package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
- package/templates/gen_ai/src/context/app_context.tsx +4 -26
- package/templates/gen_ai/src/context/context.messages.ts +1 -12
- package/templates/gen_ai/src/home.tsx +13 -0
- package/templates/gen_ai/src/index.tsx +2 -18
- package/templates/gen_ai/src/routes/routes.tsx +2 -2
- package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/gen_ai/webpack.config.cjs +3 -1
- package/templates/hello_world/eslint.config.mjs +5 -277
- package/templates/hello_world/package.json +46 -28
- package/templates/hello_world/scripts/copy-env.ts +10 -0
- package/templates/hello_world/src/app.tsx +25 -3
- package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
- package/templates/hello_world/src/tests/app.tests.tsx +86 -0
- package/templates/hello_world/utils/use_add_element.ts +48 -0
- package/templates/hello_world/utils/use_feature_support.ts +28 -0
- package/templates/hello_world/webpack.config.cjs +3 -1
- package/templates/dam/backend/database/database.ts +0 -42
- package/templates/dam/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/src/components/logged_in_status.tsx +0 -44
- package/templates/gen_ai/src/services/auth.tsx +0 -31
- package/templates/gen_ai/src/services/index.ts +0 -1
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/index.ts +0 -0
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -0
|
@@ -19,6 +19,8 @@ const MIN_INPUT_ROWS = 3;
|
|
|
19
19
|
/**
|
|
20
20
|
* Array of example prompts that could be used to generate interesting pictures with an AI.
|
|
21
21
|
* Consider fetching these prompts from a server or API call for dynamic and varied content.
|
|
22
|
+
* These would need to be localised, but that is left out here as the method would depend on
|
|
23
|
+
* the specific implementation or API used.
|
|
22
24
|
*/
|
|
23
25
|
const examplePrompts: string[] = [
|
|
24
26
|
"Cats ruling a parallel universe",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
|
2
|
+
import { TestAppUiProvider } from "@canva/app-ui-kit";
|
|
3
|
+
import { TestAppI18nProvider } from "@canva/app-i18n-kit";
|
|
4
|
+
import type { RenderResult } from "@testing-library/react";
|
|
5
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { RemainingCredits } from "../remaining_credits";
|
|
8
|
+
import { requestOpenExternalUrl } from "@canva/platform";
|
|
9
|
+
|
|
10
|
+
function renderInTestProvider(node: React.ReactNode): RenderResult {
|
|
11
|
+
return render(
|
|
12
|
+
// In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`
|
|
13
|
+
<TestAppI18nProvider>
|
|
14
|
+
<TestAppUiProvider>{node}</TestAppUiProvider>,
|
|
15
|
+
</TestAppI18nProvider>,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// This test demonstrates how to test code that uses functions from the Canva Apps SDK
|
|
20
|
+
// For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
|
|
21
|
+
describe("Remaining Credit Tests", () => {
|
|
22
|
+
const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should call requestOpenExternalUrl when the link is clicked", () => {
|
|
29
|
+
// assert that the mock is in the expected clean state
|
|
30
|
+
expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
|
|
31
|
+
|
|
32
|
+
const result = renderInTestProvider(<RemainingCredits />);
|
|
33
|
+
|
|
34
|
+
// get a reference to the link to purchase more credits
|
|
35
|
+
const purchaseMoreLink = result.getByRole("button");
|
|
36
|
+
|
|
37
|
+
// programmatically simulate clicking the button
|
|
38
|
+
fireEvent.click(purchaseMoreLink);
|
|
39
|
+
|
|
40
|
+
// we expect that requestOpenExternalUrl has been called
|
|
41
|
+
expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createContext, useEffect, useState } from "react";
|
|
2
2
|
import { useIntl } from "react-intl";
|
|
3
|
-
import type { ImageType
|
|
4
|
-
import {
|
|
3
|
+
import type { ImageType } from "src/api";
|
|
4
|
+
import { getRemainingCredits } from "src/api";
|
|
5
5
|
import { ContextMessages as Messages } from "./context.messages";
|
|
6
6
|
|
|
7
7
|
export interface AppContextType {
|
|
8
|
-
loggedInState: LoggedInState;
|
|
9
|
-
setLoggedInState: (value: LoggedInState) => void;
|
|
10
8
|
appError: string;
|
|
11
9
|
setAppError: (value: string) => void;
|
|
12
10
|
creditsError: string;
|
|
@@ -28,8 +26,6 @@ export interface AppContextType {
|
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
export const AppContext = createContext<AppContextType>({
|
|
31
|
-
loggedInState: "not_authenticated",
|
|
32
|
-
setLoggedInState: () => {},
|
|
33
29
|
appError: "",
|
|
34
30
|
setAppError: () => {},
|
|
35
31
|
creditsError: "",
|
|
@@ -65,8 +61,6 @@ export const ContextProvider = ({
|
|
|
65
61
|
}: {
|
|
66
62
|
children: React.ReactNode;
|
|
67
63
|
}): JSX.Element => {
|
|
68
|
-
const [loggedInState, setLoggedInState] =
|
|
69
|
-
useState<LoggedInState>("not_authenticated");
|
|
70
64
|
const [appError, setAppError] = useState<string>("");
|
|
71
65
|
const [loadingApp, setLoadingApp] = useState<boolean>(true); // set to true to prevent ui flash on load
|
|
72
66
|
const [isLoadingImages, setIsLoadingImages] = useState<boolean>(false);
|
|
@@ -95,17 +89,6 @@ export const ContextProvider = ({
|
|
|
95
89
|
// eslint-disable-next-line no-console
|
|
96
90
|
console.error("Error fetching remaining credits:", error);
|
|
97
91
|
}
|
|
98
|
-
|
|
99
|
-
// Fetch login status
|
|
100
|
-
try {
|
|
101
|
-
checkAuthenticationStatus();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
setAppError(
|
|
104
|
-
intl.formatMessage(Messages.appErrorGetLoggedInStatusFailed),
|
|
105
|
-
);
|
|
106
|
-
// eslint-disable-next-line no-console
|
|
107
|
-
console.error("Error fetching login status:", error);
|
|
108
|
-
}
|
|
109
92
|
} catch (error) {
|
|
110
93
|
setAppError(intl.formatMessage(Messages.appErrorGeneral));
|
|
111
94
|
// eslint-disable-next-line no-console
|
|
@@ -125,13 +108,10 @@ export const ContextProvider = ({
|
|
|
125
108
|
return;
|
|
126
109
|
}
|
|
127
110
|
|
|
128
|
-
const errorMessage =
|
|
129
|
-
loggedInState === "authenticated"
|
|
130
|
-
? intl.formatMessage(Messages.alertNotEnoughCreditsLoggedIn)
|
|
131
|
-
: intl.formatMessage(Messages.alertNotEnoughCreditsLoggedOut);
|
|
111
|
+
const errorMessage = intl.formatMessage(Messages.alertNotEnoughCredits);
|
|
132
112
|
|
|
133
113
|
setCreditsError(errorMessage);
|
|
134
|
-
}, [loadingApp, remainingCredits
|
|
114
|
+
}, [loadingApp, remainingCredits]);
|
|
135
115
|
|
|
136
116
|
const setPromptInputHandler = (value: string) => {
|
|
137
117
|
if (
|
|
@@ -148,8 +128,6 @@ export const ContextProvider = ({
|
|
|
148
128
|
};
|
|
149
129
|
|
|
150
130
|
const value: AppContextType = {
|
|
151
|
-
loggedInState,
|
|
152
|
-
setLoggedInState,
|
|
153
131
|
appError,
|
|
154
132
|
setAppError,
|
|
155
133
|
creditsError,
|
|
@@ -12,11 +12,6 @@ export const ContextMessages = defineMessages({
|
|
|
12
12
|
description:
|
|
13
13
|
"A message to indicate that there was a failure to get the number of credits the user has",
|
|
14
14
|
},
|
|
15
|
-
appErrorGetLoggedInStatusFailed: {
|
|
16
|
-
defaultMessage: "Retrieving logged in status has failed.",
|
|
17
|
-
description:
|
|
18
|
-
"A message to indicate that due to an unexpected problem, the app was unable to determine if the user is logged in",
|
|
19
|
-
},
|
|
20
15
|
|
|
21
16
|
/** Messages related to prompts and user input validation. */
|
|
22
17
|
promptMissingErrorMessage: {
|
|
@@ -26,16 +21,10 @@ export const ContextMessages = defineMessages({
|
|
|
26
21
|
},
|
|
27
22
|
|
|
28
23
|
/** Messages related to credits, including their availability and purchasing options. */
|
|
29
|
-
|
|
24
|
+
alertNotEnoughCredits: {
|
|
30
25
|
defaultMessage:
|
|
31
26
|
"You don’t have enough credits left to generate an image. Please purchase more.",
|
|
32
27
|
description:
|
|
33
28
|
"A message to indicate that the user doesn't have enough credits to generate an image, and will need to buy more to continue",
|
|
34
29
|
},
|
|
35
|
-
alertNotEnoughCreditsLoggedOut: {
|
|
36
|
-
defaultMessage:
|
|
37
|
-
"You don’t have enough credits left to generate an image. Please sign up or log in to purchase more.",
|
|
38
|
-
description:
|
|
39
|
-
"A message to indicate that the user doesn't have enough credits to generate an image, and will need to sign up or log in to buy more",
|
|
40
|
-
},
|
|
41
30
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
import { Rows } from "@canva/app-ui-kit";
|
|
3
|
+
import { Footer } from "./components";
|
|
4
|
+
import * as styles from "styles/components.css";
|
|
5
|
+
|
|
6
|
+
export const Home = () => (
|
|
7
|
+
<div className={styles.scrollContainer}>
|
|
8
|
+
<Rows spacing="3u">
|
|
9
|
+
<Outlet />
|
|
10
|
+
<Footer />
|
|
11
|
+
</Rows>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import { createRoot } from "react-dom/client";
|
|
2
|
-
import { createHashRouter, RouterProvider } from "react-router-dom";
|
|
3
|
-
import { ErrorBoundary } from "react-error-boundary";
|
|
4
|
-
import { AppUiProvider } from "@canva/app-ui-kit";
|
|
5
|
-
import { routes } from "./routes";
|
|
6
|
-
import { ErrorPage } from "./pages";
|
|
7
|
-
import { ContextProvider } from "./context";
|
|
8
2
|
import "@canva/app-ui-kit/styles.css";
|
|
9
|
-
import {
|
|
3
|
+
import { App } from "./app";
|
|
10
4
|
|
|
11
5
|
const root = createRoot(document.getElementById("root") as Element);
|
|
12
6
|
|
|
13
7
|
function render() {
|
|
14
|
-
root.render(
|
|
15
|
-
<AppI18nProvider>
|
|
16
|
-
<AppUiProvider>
|
|
17
|
-
<ErrorBoundary fallback={<ErrorPage />}>
|
|
18
|
-
<ContextProvider>
|
|
19
|
-
<RouterProvider router={createHashRouter(routes)} />
|
|
20
|
-
</ContextProvider>
|
|
21
|
-
</ErrorBoundary>
|
|
22
|
-
</AppUiProvider>
|
|
23
|
-
</AppI18nProvider>,
|
|
24
|
-
);
|
|
8
|
+
root.render(<App />);
|
|
25
9
|
}
|
|
26
10
|
|
|
27
11
|
render();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Home } from "src/home";
|
|
2
2
|
import { ErrorPage, GeneratePage, ResultsPage } from "src/pages";
|
|
3
3
|
|
|
4
4
|
export enum Paths {
|
|
@@ -9,7 +9,7 @@ export enum Paths {
|
|
|
9
9
|
export const routes = [
|
|
10
10
|
{
|
|
11
11
|
path: Paths.HOME,
|
|
12
|
-
element: <
|
|
12
|
+
element: <Home />,
|
|
13
13
|
errorElement: <ErrorPage />,
|
|
14
14
|
children: [
|
|
15
15
|
{
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import * as debug from "debug";
|
|
3
|
+
import type { Request, Response, NextFunction } from "express";
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
import Express from "express-serve-static-core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prefix your start command with `DEBUG=express:middleware:bearer` to enable debug logging
|
|
9
|
+
* for this middleware
|
|
10
|
+
*/
|
|
11
|
+
const debugLogger = debug("express:middleware:bearer");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Augment the Express request context to include the appId/userId/brandId fields decoded
|
|
15
|
+
* from the JWT.
|
|
16
|
+
*/
|
|
17
|
+
declare module "express-serve-static-core" {
|
|
18
|
+
export interface Request {
|
|
19
|
+
user_id: string;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sendUnauthorizedResponse = (res: Response, message?: string) =>
|
|
24
|
+
res.status(401).json({ error: "unauthorized", message });
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An Express.js middleware verifying a Bearer token.
|
|
28
|
+
* This middleware extracts the token from the `Authorization` header.
|
|
29
|
+
*
|
|
30
|
+
* @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
|
|
31
|
+
* @returns An Express.js middleware for verifying and decoding JWTs.
|
|
32
|
+
*/
|
|
33
|
+
export function createBearerMiddleware(
|
|
34
|
+
tokenToUser: (access_token: string) => Promise<string | undefined>,
|
|
35
|
+
getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
|
|
36
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
37
|
+
return async (req, res, next) => {
|
|
38
|
+
try {
|
|
39
|
+
debugLogger(`processing token for '${req.url}'`);
|
|
40
|
+
|
|
41
|
+
const token = await getTokenFromRequest(req);
|
|
42
|
+
const user = await tokenToUser(token);
|
|
43
|
+
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new AuthorizationError("Token is invalid");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
req.user_id = user;
|
|
49
|
+
|
|
50
|
+
next();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e instanceof AuthorizationError) {
|
|
53
|
+
return sendUnauthorizedResponse(res, e.message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
next(e);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
|
|
62
|
+
|
|
63
|
+
export const getTokenFromHttpHeader: GetTokenFromRequest = (
|
|
64
|
+
req: Request,
|
|
65
|
+
): string => {
|
|
66
|
+
// The names of a HTTP header bearing the JWT, and a scheme
|
|
67
|
+
const headerName = "Authorization";
|
|
68
|
+
const schemeName = "Bearer";
|
|
69
|
+
|
|
70
|
+
const header = req.header(headerName);
|
|
71
|
+
if (!header) {
|
|
72
|
+
throw new AuthorizationError(`Missing the "${headerName}" header`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
|
|
76
|
+
console.trace(
|
|
77
|
+
`jwtMiddleware: failed to match token in "${headerName}" header`,
|
|
78
|
+
);
|
|
79
|
+
throw new AuthorizationError(
|
|
80
|
+
`Missing a "${schemeName}" token in the "${headerName}" header`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
|
|
85
|
+
|
|
86
|
+
return token;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A class representing JWT validation errors in the JWT middleware.
|
|
91
|
+
* The error message provided to the constructor will be forwarded to the
|
|
92
|
+
* API consumer trying to access a JWT-protected endpoint.
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
export class AuthorizationError extends Error {
|
|
96
|
+
constructor(message: string) {
|
|
97
|
+
super(message);
|
|
98
|
+
|
|
99
|
+
Object.setPrototypeOf(this, AuthorizationError.prototype);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createBearerMiddleware } from "./bearer_middleware";
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
2
|
+
import type { NextFunction, Request, Response } from "express";
|
|
3
|
+
import type {
|
|
4
|
+
createBearerMiddleware,
|
|
5
|
+
GetTokenFromRequest,
|
|
6
|
+
} from "../bearer_middleware";
|
|
7
|
+
|
|
8
|
+
type Middleware = (req: Request, res: Response, next: NextFunction) => void;
|
|
9
|
+
|
|
10
|
+
describe("createBearerMiddleware", () => {
|
|
11
|
+
let fakeGetTokenFromRequest: jest.MockedFn<GetTokenFromRequest>;
|
|
12
|
+
let verify: jest.MockedFn<(token: string) => Promise<string | undefined>>;
|
|
13
|
+
|
|
14
|
+
let req: Request;
|
|
15
|
+
let res: Response;
|
|
16
|
+
let next: jest.MockedFn<() => void>;
|
|
17
|
+
|
|
18
|
+
let AuthorizationError: typeof Error;
|
|
19
|
+
let createBearerMiddlewareFn: typeof createBearerMiddleware;
|
|
20
|
+
let bearerMiddleware: Middleware;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.resetAllMocks();
|
|
24
|
+
jest.resetModules();
|
|
25
|
+
|
|
26
|
+
fakeGetTokenFromRequest = jest.fn();
|
|
27
|
+
verify = jest.fn();
|
|
28
|
+
|
|
29
|
+
const middlewareModule = require("../bearer_middleware");
|
|
30
|
+
createBearerMiddlewareFn = middlewareModule.createBearerMiddleware;
|
|
31
|
+
AuthorizationError = middlewareModule.AuthorizationError;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("When called", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
req = {
|
|
37
|
+
header: (_name: string) => undefined,
|
|
38
|
+
} as Request;
|
|
39
|
+
|
|
40
|
+
res = {
|
|
41
|
+
status: jest.fn().mockReturnThis(),
|
|
42
|
+
json: jest.fn().mockReturnThis(),
|
|
43
|
+
send: jest.fn().mockReturnThis(),
|
|
44
|
+
} as unknown as Response;
|
|
45
|
+
|
|
46
|
+
next = jest.fn();
|
|
47
|
+
|
|
48
|
+
bearerMiddleware = createBearerMiddlewareFn(
|
|
49
|
+
verify,
|
|
50
|
+
fakeGetTokenFromRequest,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("When `getTokenFromRequest` throws an exception ('Fake error')", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
fakeGetTokenFromRequest.mockRejectedValue(
|
|
57
|
+
new AuthorizationError("Fake error"),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it(`Does not call next() and returns HTTP 401 with error = "unauthorized" and message = "Fake error"`, async () => {
|
|
62
|
+
expect.assertions(8);
|
|
63
|
+
|
|
64
|
+
expect(fakeGetTokenFromRequest).not.toHaveBeenCalled();
|
|
65
|
+
await bearerMiddleware(req, res, next);
|
|
66
|
+
|
|
67
|
+
expect(fakeGetTokenFromRequest).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(fakeGetTokenFromRequest).toHaveBeenLastCalledWith(req);
|
|
69
|
+
|
|
70
|
+
expect(res.status).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(res.status).toHaveBeenLastCalledWith(401);
|
|
72
|
+
|
|
73
|
+
expect(res.json).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(res.json).toHaveBeenLastCalledWith({
|
|
75
|
+
error: "unauthorized",
|
|
76
|
+
message: "Fake error",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(next).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("When the middleware cannot verify the token", () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
fakeGetTokenFromRequest.mockReturnValue("TOKEN");
|
|
86
|
+
|
|
87
|
+
verify.mockImplementation(() => Promise.resolve(undefined));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it(`Does not call next() and returns HTTP 401 with error = "unauthorized" and message = "Token is invalid"`, async () => {
|
|
91
|
+
expect.assertions(5);
|
|
92
|
+
|
|
93
|
+
await bearerMiddleware(req, res, next);
|
|
94
|
+
|
|
95
|
+
expect(res.status).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(res.status).toHaveBeenLastCalledWith(401);
|
|
97
|
+
|
|
98
|
+
expect(res.json).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(res.json).toHaveBeenLastCalledWith({
|
|
100
|
+
error: "unauthorized",
|
|
101
|
+
message: "Token is invalid",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(next).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("getTokenFromHttpHeader", () => {
|
|
111
|
+
let getHeader: jest.MockedFn<(name: string) => string | undefined>;
|
|
112
|
+
let req: Request;
|
|
113
|
+
let getTokenFromHttpHeader: (req: Request) => string;
|
|
114
|
+
let AuthorizationError: typeof Error;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
getHeader = jest.fn();
|
|
118
|
+
req = {
|
|
119
|
+
header: (name: string) => getHeader(name),
|
|
120
|
+
} as Request;
|
|
121
|
+
|
|
122
|
+
const bearerMiddlewareModule = require("../bearer_middleware");
|
|
123
|
+
getTokenFromHttpHeader = bearerMiddlewareModule.getTokenFromHttpHeader;
|
|
124
|
+
AuthorizationError = bearerMiddlewareModule.AuthorizationError;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("When the 'Authorization' header is missing", () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
getHeader.mockReturnValue(undefined);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it(`Throws a AuthorizationError with message = 'Missing the "Authorization" header'`, async () => {
|
|
133
|
+
expect.assertions(3);
|
|
134
|
+
|
|
135
|
+
expect(() => getTokenFromHttpHeader(req)).toThrow(
|
|
136
|
+
new AuthorizationError('Missing the "Authorization" header'),
|
|
137
|
+
);
|
|
138
|
+
expect(getHeader).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(getHeader).toHaveBeenLastCalledWith("Authorization");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("When the 'Authorization' header doesn't have a Bearer scheme", () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
getHeader.mockReturnValue("Beerer FAKE_TOKEN");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it(`Throws a AuthorizationError with message = 'Missing a "Bearer" token in the "Authorization" header''`, async () => {
|
|
149
|
+
expect.assertions(3);
|
|
150
|
+
|
|
151
|
+
expect(() => getTokenFromHttpHeader(req)).toThrow(
|
|
152
|
+
new AuthorizationError(
|
|
153
|
+
'Missing a "Bearer" token in the "Authorization" header',
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
expect(getHeader).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(getHeader).toHaveBeenLastCalledWith("Authorization");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("When the 'Authorization' Bearer scheme header doesn't have a token", () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
getHeader.mockReturnValue("Bearer ");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it(`Throws a AuthorizationError with message = 'Missing a "Bearer" token in the "Authorization" header'`, async () => {
|
|
167
|
+
expect.assertions(3);
|
|
168
|
+
|
|
169
|
+
expect(() => getTokenFromHttpHeader(req)).toThrow(
|
|
170
|
+
new AuthorizationError(
|
|
171
|
+
'Missing a "Bearer" token in the "Authorization" header',
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
expect(getHeader).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(getHeader).toHaveBeenLastCalledWith("Authorization");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("When the 'Authorization' Bearer scheme header has a token", () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
getHeader.mockReturnValue("Bearer TOKEN");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it(`Returns the token`, async () => {
|
|
185
|
+
expect.assertions(3);
|
|
186
|
+
|
|
187
|
+
expect(getTokenFromHttpHeader(req)).toEqual("TOKEN");
|
|
188
|
+
expect(getHeader).toHaveBeenCalledTimes(1);
|
|
189
|
+
expect(getHeader).toHaveBeenLastCalledWith("Authorization");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -4,6 +4,7 @@ const TerserPlugin = require("terser-webpack-plugin");
|
|
|
4
4
|
const { DefinePlugin, optimize } = require("webpack");
|
|
5
5
|
const chalk = require("chalk");
|
|
6
6
|
const { transform } = require("@formatjs/ts-transformer");
|
|
7
|
+
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
*
|
|
@@ -173,7 +174,8 @@ function buildConfig({
|
|
|
173
174
|
}),
|
|
174
175
|
// Apps can only submit a single JS file via the developer portal
|
|
175
176
|
new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
|
176
|
-
|
|
177
|
+
mode === "development" && new ReactRefreshWebpackPlugin(),
|
|
178
|
+
].filter(Boolean),
|
|
177
179
|
...buildDevConfig(devConfig),
|
|
178
180
|
};
|
|
179
181
|
}
|