@cedarjs/cli 7.0.1-next.314 → 7.0.1-next.466
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/commands/deploy/baremetal/baremetalHandler.js +375 -76
- package/dist/commands/deploy/baremetal/monitors.js +66 -0
- package/dist/commands/deploy/baremetal.js +6 -0
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +63 -25
- package/dist/commands/dev/devHandler.js +13 -1
- package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +57 -14
- package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
- package/dist/commands/generate/directive/directive.js +15 -2
- package/dist/commands/generate/helpers.js +26 -0
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +3 -3
- package/dist/commands/generate/scaffold/scaffold.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +83 -44
- package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +7 -7
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +7 -6
- package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +3 -3
- package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/sdl/sdlHandler.js +8 -3
- package/dist/commands/generate/service/serviceHandler.js +32 -11
- package/dist/commands/generate/service/templates/service.ts.template +21 -8
- package/dist/commands/generate/service/templates/test.ts.template +7 -5
- package/dist/commands/jobsHandler.js +5 -3
- package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
- package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
- package/dist/commands/setup/deploy/helpers/index.js +3 -1
- package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +128 -20
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +25 -3
- package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
- package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
- package/dist/commands/setup/jobs/jobsHandler.js +2 -0
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +5 -2
- package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
- package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
- package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
- package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +1 -1
- package/dist/commands/setup/tenancy/signupCodemod.js +2 -1
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +10 -1
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +0 -2
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +0 -8
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +1 -1
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +1 -1
- package/dist/commands/setup/tenancy/tenancyHandler.js +1 -1
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +61 -81
- package/dist/commands/setup/uploads/gitignore.js +17 -0
- package/dist/commands/setup/uploads/serverFile.js +93 -27
- package/dist/commands/setup/uploads/templates/uploads.ts.template +14 -5
- package/dist/commands/setup/uploads/uploadsHandler.js +22 -1
- package/dist/lib/index.js +10 -6
- package/package.json +24 -24
- package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
1
2
|
import { Listr } from "listr2";
|
|
2
|
-
import pascalcase from "pascalcase";
|
|
3
3
|
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
4
4
|
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
5
|
-
import { pluralize } from "@cedarjs/utils/cedarPluralize";
|
|
6
5
|
import {
|
|
7
6
|
deleteFilesTask,
|
|
8
7
|
getPaths,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
import { verifyModelName } from "../../../lib/schemaHelpers.js";
|
|
14
13
|
import {
|
|
15
14
|
files,
|
|
15
|
+
getScaffoldLayoutNames,
|
|
16
16
|
routes as scaffoldRoutes,
|
|
17
17
|
splitPathAndModel
|
|
18
18
|
} from "../../generate/scaffold/scaffoldHandler.js";
|
|
@@ -23,45 +23,83 @@ const removeRoutesWithSet = async ({
|
|
|
23
23
|
}) => {
|
|
24
24
|
const routes = await scaffoldRoutes({ model, path, nestScaffoldByModel });
|
|
25
25
|
const routeNames = routes.map(extractRouteName);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const { importName } = getScaffoldLayoutNames(model, path);
|
|
27
|
+
return removeRoutesFromRouterTask(routeNames, importName);
|
|
28
|
+
};
|
|
29
|
+
const removeNamedImport = (code, source, name) => {
|
|
30
|
+
const program = parse(code, {
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
plugins: ["jsx", "typescript"]
|
|
33
|
+
}).program;
|
|
34
|
+
for (const statement of program.body) {
|
|
35
|
+
if (statement.type !== "ImportDeclaration" || statement.source.value !== source) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const specifiers = statement.specifiers;
|
|
39
|
+
const index = specifiers.findIndex(
|
|
40
|
+
(specifier2) => specifier2.type === "ImportSpecifier" && specifier2.imported.type === "Identifier" && specifier2.imported.name === name && specifier2.local.name === name
|
|
41
|
+
);
|
|
42
|
+
if (index === -1) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const specifier = specifiers[index];
|
|
46
|
+
let start;
|
|
47
|
+
let end;
|
|
48
|
+
if (specifiers.length === 1) {
|
|
49
|
+
start = statement.start ?? 0;
|
|
50
|
+
end = statement.end ?? 0;
|
|
51
|
+
if (code[end] === "\n") {
|
|
52
|
+
end++;
|
|
53
|
+
}
|
|
54
|
+
} else if (index === specifiers.length - 1) {
|
|
55
|
+
start = specifiers[index - 1].end ?? 0;
|
|
56
|
+
end = specifier.end ?? 0;
|
|
57
|
+
} else {
|
|
58
|
+
start = specifier.start ?? 0;
|
|
59
|
+
end = specifiers[index + 1].start ?? 0;
|
|
60
|
+
}
|
|
61
|
+
return code.slice(0, start) + code.slice(end);
|
|
62
|
+
}
|
|
63
|
+
return code;
|
|
29
64
|
};
|
|
30
65
|
const removeSetImport = () => {
|
|
31
66
|
const routesPath = getPaths().web.routes;
|
|
32
67
|
const routesContent = readFile(routesPath).toString();
|
|
33
|
-
|
|
68
|
+
const unusedNames = ["Set", "PrivateSet"].filter(
|
|
69
|
+
(name) => !new RegExp(`<${name}\\b`).test(routesContent)
|
|
70
|
+
);
|
|
71
|
+
if (unusedNames.length === 0) {
|
|
34
72
|
return "Skipping removal of Set import in Routes.{jsx,tsx}";
|
|
35
73
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
let newRoutesContent = routesContent;
|
|
75
|
+
try {
|
|
76
|
+
for (const name of unusedNames) {
|
|
77
|
+
newRoutesContent = removeNamedImport(
|
|
78
|
+
newRoutesContent,
|
|
79
|
+
"@cedarjs/router",
|
|
80
|
+
name
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return "Skipping removal of Set import, Routes.{jsx,tsx} couldn't be parsed";
|
|
85
|
+
}
|
|
86
|
+
if (newRoutesContent === routesContent) {
|
|
87
|
+
return "No unused Set or PrivateSet import found in Routes.{jsx,tsx}";
|
|
41
88
|
}
|
|
42
|
-
const [cedarRouterImport] = cedarRouterImportMatch;
|
|
43
|
-
const removedSetImport = cedarRouterImport.replace(/,*\s*Set,*/, "");
|
|
44
|
-
const newRoutesContent = routesContent.replace(
|
|
45
|
-
cedarRouterImport,
|
|
46
|
-
removedSetImport
|
|
47
|
-
);
|
|
48
89
|
writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
|
|
49
|
-
return "Removed Set
|
|
90
|
+
return "Removed unused Set and PrivateSet imports in Routes.{jsx,tsx}";
|
|
50
91
|
};
|
|
51
92
|
const removeLayoutImport = ({
|
|
52
93
|
model: name,
|
|
53
94
|
path: scaffoldPath = ""
|
|
54
95
|
}) => {
|
|
55
|
-
const
|
|
56
|
-
const pascalScaffoldPath = scaffoldPath === "" ? scaffoldPath : scaffoldPath.split("/").map(pascalcase).join("/") + "/";
|
|
57
|
-
const layoutName = `${pluralPascalName}Layout`;
|
|
58
|
-
const importLayout = `import ${pluralPascalName}Layout from 'src/layouts/${pascalScaffoldPath}${layoutName}'`;
|
|
96
|
+
const { importName, importPath } = getScaffoldLayoutNames(name, scaffoldPath);
|
|
59
97
|
const routesPath = getPaths().web.routes;
|
|
60
98
|
const routesContent = readFile(routesPath).toString();
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
""
|
|
99
|
+
const importLayout = new RegExp(
|
|
100
|
+
`\\s*import ${importName} from (['"])${importPath}\\1;?`
|
|
64
101
|
);
|
|
102
|
+
const newRoutesContent = routesContent.replace(importLayout, "");
|
|
65
103
|
writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
|
|
66
104
|
return "Removed layout import from Routes.{jsx,tsx}";
|
|
67
105
|
};
|
|
@@ -185,21 +185,33 @@ const handler = async ({
|
|
|
185
185
|
const rootPackageJson = JSON.parse(
|
|
186
186
|
fs.readFileSync(rootPackageJsonPath, "utf8")
|
|
187
187
|
);
|
|
188
|
+
const warnUdFallback = (reason) => {
|
|
189
|
+
console.warn(
|
|
190
|
+
c.warning(
|
|
191
|
+
`--ud has no effect: ${reason}. Falling back to classic dev-server mode.`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
};
|
|
188
195
|
const buildUnifiedDevCommand = () => {
|
|
189
196
|
if (!ud) {
|
|
190
197
|
return null;
|
|
191
198
|
}
|
|
192
199
|
if (streamingSsrEnabled) {
|
|
200
|
+
warnUdFallback("streaming SSR uses its own dev server");
|
|
193
201
|
return null;
|
|
194
202
|
}
|
|
195
203
|
if (!workspace.includes("api") || !workspace.includes("web")) {
|
|
204
|
+
warnUdFallback("the unified dev server needs both the api and web sides");
|
|
196
205
|
return null;
|
|
197
206
|
}
|
|
198
207
|
if (serverFile) {
|
|
208
|
+
warnUdFallback(
|
|
209
|
+
"the unified dev server does not support a custom server file"
|
|
210
|
+
);
|
|
199
211
|
return null;
|
|
200
212
|
}
|
|
201
213
|
if (!fs.existsSync(cedarPaths.api.src) || !fs.existsSync(cedarPaths.web.src)) {
|
|
202
|
-
|
|
214
|
+
warnUdFallback("api.src or web.src does not exist");
|
|
203
215
|
return null;
|
|
204
216
|
}
|
|
205
217
|
return [
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { terminalLink } from "termi-link";
|
|
2
2
|
import { getYargsDefaults, createHandler } from "../yargsCommandHelpers.js";
|
|
3
3
|
const command = "dbAuth";
|
|
4
|
-
const description = "Generate Login, Signup and
|
|
4
|
+
const description = "Generate Login, Signup, Forgot Password, Reset Password and Change Password pages for dbAuth";
|
|
5
5
|
const builder = (yargs) => {
|
|
6
|
-
yargs.option("skip-
|
|
6
|
+
yargs.option("skip-change", {
|
|
7
|
+
description: "Skip generating the Change Password page",
|
|
8
|
+
type: "boolean",
|
|
9
|
+
default: false
|
|
10
|
+
}).option("skip-forgot", {
|
|
7
11
|
description: "Skip generating the Forgot Password page",
|
|
8
12
|
type: "boolean",
|
|
9
13
|
default: false
|
|
@@ -18,12 +18,25 @@ import {
|
|
|
18
18
|
} from "../../../lib/index.js";
|
|
19
19
|
import { prepareForRollback } from "../../../lib/rollback.js";
|
|
20
20
|
import { templateForComponentFile } from "../yargsHandlerHelpers.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
function routesFor({
|
|
22
|
+
skipChange,
|
|
23
|
+
skipForgot,
|
|
24
|
+
skipLogin,
|
|
25
|
+
skipReset,
|
|
26
|
+
skipSignup
|
|
27
|
+
}) {
|
|
28
|
+
return [
|
|
29
|
+
!skipLogin && `<Route path="/login" page={LoginPage} name="login" />`,
|
|
30
|
+
!skipSignup && `<Route path="/signup" page={SignupPage} name="signup" />`,
|
|
31
|
+
!skipForgot && `<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />`,
|
|
32
|
+
!skipReset && `<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />`,
|
|
33
|
+
!skipChange && `<Route path="/change-password" page={ChangePasswordPage} name="changePassword" />`
|
|
34
|
+
].filter((route) => typeof route === "string");
|
|
35
|
+
}
|
|
36
|
+
function pascalCase(str) {
|
|
37
|
+
const camelCased = camelCase(str);
|
|
38
|
+
return camelCased.charAt(0).toUpperCase() + camelCased.slice(1);
|
|
39
|
+
}
|
|
27
40
|
function getPostInstallMessage(isDbAuthSetup2) {
|
|
28
41
|
return [
|
|
29
42
|
` ${c.warning("Pages created! But you're not done yet:")}
|
|
@@ -35,9 +48,11 @@ function getPostInstallMessage(isDbAuthSetup2) {
|
|
|
35
48
|
" navigate(routes.home())",
|
|
36
49
|
" }\n",
|
|
37
50
|
" and change the route to where you want them to go if the user is already",
|
|
38
|
-
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
|
|
39
|
-
" and
|
|
40
|
-
" those forms.\n",
|
|
51
|
+
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage,",
|
|
52
|
+
" ResetPasswordPage and ChangePasswordPage to change where the user redirects",
|
|
53
|
+
" to after submitting those forms.\n",
|
|
54
|
+
" ChangePasswordPage is for users who are already logged in. Link to it from",
|
|
55
|
+
" wherever your app lets users manage their account.\n",
|
|
41
56
|
!isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
|
|
42
57
|
app setup by running:
|
|
43
58
|
|
|
@@ -58,9 +73,11 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
|
|
|
58
73
|
" navigate(routes.home())",
|
|
59
74
|
" }\n",
|
|
60
75
|
" and change the route to where you want them to go if the user is already",
|
|
61
|
-
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
|
|
62
|
-
" and
|
|
63
|
-
" those forms.\n",
|
|
76
|
+
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage,",
|
|
77
|
+
" ResetPasswordPage and ChangePasswordPage to change where the user redirects",
|
|
78
|
+
" to after submitting those forms.\n",
|
|
79
|
+
" ChangePasswordPage is for users who are already logged in. Link to it from",
|
|
80
|
+
" wherever your app lets users manage their account.\n",
|
|
64
81
|
!isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
|
|
65
82
|
app setup by running:
|
|
66
83
|
|
|
@@ -71,6 +88,7 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
|
|
|
71
88
|
}
|
|
72
89
|
const files = async ({
|
|
73
90
|
typescript,
|
|
91
|
+
skipChange,
|
|
74
92
|
skipForgot,
|
|
75
93
|
skipLogin,
|
|
76
94
|
skipReset,
|
|
@@ -90,7 +108,8 @@ const files = async ({
|
|
|
90
108
|
usernameTitleCase: titleCase(usernameLabel),
|
|
91
109
|
passwordLowerCase: passwordLabel.toLowerCase(),
|
|
92
110
|
passwordCamelCase: camelCase(passwordLabel),
|
|
93
|
-
passwordTitleCase: titleCase(passwordLabel)
|
|
111
|
+
passwordTitleCase: titleCase(passwordLabel),
|
|
112
|
+
passwordPascalCase: pascalCase(passwordLabel)
|
|
94
113
|
};
|
|
95
114
|
if (!skipForgot) {
|
|
96
115
|
filesList.push(
|
|
@@ -131,6 +150,19 @@ const files = async ({
|
|
|
131
150
|
})
|
|
132
151
|
);
|
|
133
152
|
}
|
|
153
|
+
if (!skipChange) {
|
|
154
|
+
filesList.push(
|
|
155
|
+
await templateForComponentFile({
|
|
156
|
+
name: "ChangePassword",
|
|
157
|
+
suffix: "Page",
|
|
158
|
+
extension: typescript ? ".tsx" : ".jsx",
|
|
159
|
+
webPathSection: "pages",
|
|
160
|
+
generator: "dbAuth",
|
|
161
|
+
templatePath: "changePassword.tsx.template",
|
|
162
|
+
templateVars
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
}
|
|
134
166
|
if (!skipSignup) {
|
|
135
167
|
filesList.push(
|
|
136
168
|
await templateForComponentFile({
|
|
@@ -179,6 +211,7 @@ const tasks = ({
|
|
|
179
211
|
listr2,
|
|
180
212
|
force,
|
|
181
213
|
typescript,
|
|
214
|
+
skipChange,
|
|
182
215
|
skipForgot,
|
|
183
216
|
skipLogin,
|
|
184
217
|
skipReset,
|
|
@@ -289,6 +322,7 @@ const tasks = ({
|
|
|
289
322
|
task: async () => {
|
|
290
323
|
const filesObj = await files({
|
|
291
324
|
typescript,
|
|
325
|
+
skipChange,
|
|
292
326
|
skipForgot,
|
|
293
327
|
skipLogin,
|
|
294
328
|
skipReset,
|
|
@@ -305,7 +339,15 @@ const tasks = ({
|
|
|
305
339
|
{
|
|
306
340
|
title: "Adding routes...",
|
|
307
341
|
task: async () => {
|
|
308
|
-
addRoutesToRouterTask(
|
|
342
|
+
addRoutesToRouterTask(
|
|
343
|
+
routesFor({
|
|
344
|
+
skipChange,
|
|
345
|
+
skipForgot,
|
|
346
|
+
skipLogin,
|
|
347
|
+
skipReset,
|
|
348
|
+
skipSignup
|
|
349
|
+
})
|
|
350
|
+
);
|
|
309
351
|
}
|
|
310
352
|
},
|
|
311
353
|
{
|
|
@@ -335,6 +377,7 @@ const tasks = ({
|
|
|
335
377
|
const handler = async (yargs) => {
|
|
336
378
|
recordTelemetryAttributes({
|
|
337
379
|
command: "generate dbAuth",
|
|
380
|
+
skipChange: yargs.skipChange,
|
|
338
381
|
skipForgot: yargs.skipForgot,
|
|
339
382
|
skipLogin: yargs.skipLogin,
|
|
340
383
|
skipReset: yargs.skipReset,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Form,
|
|
5
|
+
Label,
|
|
6
|
+
PasswordField,
|
|
7
|
+
Submit,
|
|
8
|
+
FieldError,
|
|
9
|
+
} from '@cedarjs/forms'
|
|
10
|
+
import { navigate, routes } from '@cedarjs/router'
|
|
11
|
+
import { Metadata } from '@cedarjs/web'
|
|
12
|
+
import { toast, Toaster } from '@cedarjs/web/toast'
|
|
13
|
+
|
|
14
|
+
import { useAuth } from 'src/auth'
|
|
15
|
+
|
|
16
|
+
const ChangePasswordPage = () => {
|
|
17
|
+
const { loading, isAuthenticated, reauthenticate, changePassword } =
|
|
18
|
+
useAuth()
|
|
19
|
+
|
|
20
|
+
// Only a logged in user can change their password
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!loading && !isAuthenticated) {
|
|
23
|
+
navigate(routes.login())
|
|
24
|
+
}
|
|
25
|
+
}, [loading, isAuthenticated])
|
|
26
|
+
|
|
27
|
+
const current${passwordPascalCase}Ref = useRef<HTMLInputElement>(null)
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
current${passwordPascalCase}Ref.current?.focus()
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const onSubmit = async (data: Record<string, string>) => {
|
|
33
|
+
const response = await changePassword({
|
|
34
|
+
currentPassword: data.current${passwordPascalCase},
|
|
35
|
+
newPassword: data.new${passwordPascalCase},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (response.error) {
|
|
39
|
+
toast.error(response.error)
|
|
40
|
+
} else {
|
|
41
|
+
// The function `changePassword.handler` in api/src/functions/auth.js
|
|
42
|
+
// decides if the user stays logged in. `reauthenticate()` picks up
|
|
43
|
+
// whichever it was
|
|
44
|
+
toast.success('${passwordTitleCase} changed!')
|
|
45
|
+
await reauthenticate()
|
|
46
|
+
navigate(routes.home())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Metadata title="Change ${passwordTitleCase}" />
|
|
53
|
+
|
|
54
|
+
<main className="rw-main">
|
|
55
|
+
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
|
56
|
+
<div className="rw-scaffold rw-login-container">
|
|
57
|
+
<div className="rw-segment">
|
|
58
|
+
<header className="rw-segment-header">
|
|
59
|
+
<h2 className="rw-heading rw-heading-secondary">
|
|
60
|
+
Change ${passwordTitleCase}
|
|
61
|
+
</h2>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<div className="rw-segment-main">
|
|
65
|
+
<div className="rw-form-wrapper">
|
|
66
|
+
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
|
67
|
+
<Label
|
|
68
|
+
name="current${passwordPascalCase}"
|
|
69
|
+
className="rw-label"
|
|
70
|
+
errorClassName="rw-label rw-label-error"
|
|
71
|
+
>
|
|
72
|
+
Current ${passwordTitleCase}
|
|
73
|
+
</Label>
|
|
74
|
+
<PasswordField
|
|
75
|
+
name="current${passwordPascalCase}"
|
|
76
|
+
autoComplete="current-password"
|
|
77
|
+
className="rw-input"
|
|
78
|
+
errorClassName="rw-input rw-input-error"
|
|
79
|
+
ref={current${passwordPascalCase}Ref}
|
|
80
|
+
validation={{
|
|
81
|
+
required: {
|
|
82
|
+
value: true,
|
|
83
|
+
message: 'Current ${passwordTitleCase} is required',
|
|
84
|
+
},
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<FieldError
|
|
89
|
+
name="current${passwordPascalCase}"
|
|
90
|
+
className="rw-field-error"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<Label
|
|
94
|
+
name="new${passwordPascalCase}"
|
|
95
|
+
className="rw-label"
|
|
96
|
+
errorClassName="rw-label rw-label-error"
|
|
97
|
+
>
|
|
98
|
+
New ${passwordTitleCase}
|
|
99
|
+
</Label>
|
|
100
|
+
<PasswordField
|
|
101
|
+
name="new${passwordPascalCase}"
|
|
102
|
+
autoComplete="new-password"
|
|
103
|
+
className="rw-input"
|
|
104
|
+
errorClassName="rw-input rw-input-error"
|
|
105
|
+
validation={{
|
|
106
|
+
required: {
|
|
107
|
+
value: true,
|
|
108
|
+
message: 'New ${passwordTitleCase} is required',
|
|
109
|
+
},
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<FieldError
|
|
114
|
+
name="new${passwordPascalCase}"
|
|
115
|
+
className="rw-field-error"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<div className="rw-button-group">
|
|
119
|
+
<Submit className="rw-button rw-button-blue">Submit</Submit>
|
|
120
|
+
</div>
|
|
121
|
+
</Form>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</main>
|
|
127
|
+
</>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default ChangePasswordPage
|
|
@@ -17,7 +17,7 @@ import { useAuth } from 'src/auth'
|
|
|
17
17
|
const WELCOME_MESSAGE = 'Welcome back!'
|
|
18
18
|
const REDIRECT = routes.home()
|
|
19
19
|
|
|
20
|
-
const LoginPage = ({ type }) => {
|
|
20
|
+
const LoginPage = ({ type }: { type?: string }) => {
|
|
21
21
|
const {
|
|
22
22
|
isAuthenticated,
|
|
23
23
|
client: webAuthn,
|
|
@@ -27,12 +27,12 @@ const LoginPage = ({ type }) => {
|
|
|
27
27
|
} = useAuth()
|
|
28
28
|
const [shouldShowWebAuthn, setShouldShowWebAuthn] = useState(false)
|
|
29
29
|
const [showWebAuthn, setShowWebAuthn] = useState(
|
|
30
|
-
webAuthn
|
|
30
|
+
!!webAuthn?.isEnabled() && type !== 'password'
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
// should redirect right after login or wait to show the webAuthn prompts?
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
if (isAuthenticated && (!shouldShowWebAuthn || webAuthn
|
|
35
|
+
if (isAuthenticated && (!shouldShowWebAuthn || webAuthn?.isEnabled())) {
|
|
36
36
|
navigate(REDIRECT)
|
|
37
37
|
}
|
|
38
38
|
}, [isAuthenticated, shouldShowWebAuthn])
|
|
@@ -45,13 +45,13 @@ const LoginPage = ({ type }) => {
|
|
|
45
45
|
}, [loading, isAuthenticated])
|
|
46
46
|
|
|
47
47
|
// focus on the ${usernameLowerCase} field as soon as the page loads
|
|
48
|
-
const ${usernameCamelCase}Ref = useRef()
|
|
48
|
+
const ${usernameCamelCase}Ref = useRef<HTMLInputElement>(null)
|
|
49
49
|
useEffect(() => {
|
|
50
|
-
${usernameCamelCase}Ref.current
|
|
50
|
+
${usernameCamelCase}Ref.current?.focus()
|
|
51
51
|
}, [])
|
|
52
52
|
|
|
53
|
-
const onSubmit = async (data) => {
|
|
54
|
-
const webAuthnSupported = await webAuthn
|
|
53
|
+
const onSubmit = async (data: Record<string, string>) => {
|
|
54
|
+
const webAuthnSupported = !!(await webAuthn?.isSupported())
|
|
55
55
|
|
|
56
56
|
if (webAuthnSupported) {
|
|
57
57
|
setShouldShowWebAuthn(true)
|
|
@@ -75,30 +75,38 @@ const LoginPage = ({ type }) => {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const onAuthenticate = async () => {
|
|
78
|
+
if (!webAuthn) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
try {
|
|
79
83
|
await webAuthn.authenticate()
|
|
80
84
|
await reauthenticate()
|
|
81
85
|
toast.success(WELCOME_MESSAGE)
|
|
82
86
|
navigate(REDIRECT)
|
|
83
87
|
} catch (e) {
|
|
84
|
-
if (e.name === 'WebAuthnDeviceNotFoundError') {
|
|
88
|
+
if (e instanceof Error && e.name === 'WebAuthnDeviceNotFoundError') {
|
|
85
89
|
toast.error(
|
|
86
90
|
'Device not found, log in with ${usernameTitleCase}/${passwordTitleCase} to continue'
|
|
87
91
|
)
|
|
88
92
|
setShowWebAuthn(false)
|
|
89
93
|
} else {
|
|
90
|
-
toast.error(e.message)
|
|
94
|
+
toast.error(e instanceof Error ? e.message : String(e))
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
const onRegister = async () => {
|
|
100
|
+
if (!webAuthn) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
try {
|
|
97
105
|
await webAuthn.register()
|
|
98
106
|
toast.success(WELCOME_MESSAGE)
|
|
99
107
|
navigate(REDIRECT)
|
|
100
108
|
} catch (e) {
|
|
101
|
-
toast.error(e.message)
|
|
109
|
+
toast.error(e instanceof Error ? e.message : String(e))
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -200,7 +208,7 @@ const LoginPage = ({ type }) => {
|
|
|
200
208
|
|
|
201
209
|
const formToRender = () => {
|
|
202
210
|
if (showWebAuthn) {
|
|
203
|
-
if (webAuthn
|
|
211
|
+
if (webAuthn?.isEnabled()) {
|
|
204
212
|
return <AuthWebAuthnPrompt />
|
|
205
213
|
} else {
|
|
206
214
|
return <RegisterWebAuthnPrompt />
|
|
@@ -212,7 +220,7 @@ const LoginPage = ({ type }) => {
|
|
|
212
220
|
|
|
213
221
|
const linkToRender = () => {
|
|
214
222
|
if (showWebAuthn) {
|
|
215
|
-
if (webAuthn
|
|
223
|
+
if (webAuthn?.isEnabled()) {
|
|
216
224
|
return (
|
|
217
225
|
<div className="rw-login-link">
|
|
218
226
|
<span>or login with </span>{' '}
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createBuilder,
|
|
3
3
|
createCommand,
|
|
4
|
-
createHandler
|
|
4
|
+
createHandler,
|
|
5
|
+
getYargsDefaults
|
|
5
6
|
} from "../yargsCommandHelpers.js";
|
|
6
7
|
const command = createCommand("directive");
|
|
7
8
|
const description = "Generate a new GraphQL directive";
|
|
8
|
-
const builder = createBuilder({
|
|
9
|
+
const builder = createBuilder({
|
|
10
|
+
componentName: "directive",
|
|
11
|
+
optionsObj: () => ({
|
|
12
|
+
...getYargsDefaults(),
|
|
13
|
+
// No default value, so the handler prompts for a type when the option is
|
|
14
|
+
// omitted
|
|
15
|
+
type: {
|
|
16
|
+
description: "Directive type. If omitted, the generator prompts for one",
|
|
17
|
+
type: "string",
|
|
18
|
+
choices: ["validator", "transformer"]
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
});
|
|
9
22
|
const handler = createHandler("directive");
|
|
10
23
|
export {
|
|
11
24
|
builder,
|
|
@@ -37,11 +37,33 @@ const redactedModelFields = (fieldNames) => {
|
|
|
37
37
|
}
|
|
38
38
|
return sensitive;
|
|
39
39
|
};
|
|
40
|
+
const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
|
|
41
|
+
const requiredFieldsInUpdateInput = (model) => {
|
|
42
|
+
return inputFields(model).filter((field) => {
|
|
43
|
+
return field.isRequired && !field.isList && !field.isId;
|
|
44
|
+
}).map((field) => field.name);
|
|
45
|
+
};
|
|
46
|
+
const nullableJsonFieldsInInput = (model) => {
|
|
47
|
+
return inputFields(model).filter((field) => {
|
|
48
|
+
return field.type === "Json" && !field.isRequired && !field.isList;
|
|
49
|
+
}).map((field) => field.name);
|
|
50
|
+
};
|
|
51
|
+
const inputFields = (model) => {
|
|
52
|
+
const redactedFields = redactedModelFields(
|
|
53
|
+
model.fields.map((field) => field.name)
|
|
54
|
+
);
|
|
55
|
+
return model.fields.filter((field) => {
|
|
56
|
+
return field.kind !== "object" && !DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name) && !redactedFields.includes(field.name);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
40
59
|
const relationsForModel = (model) => {
|
|
41
60
|
return model?.fields.filter((f) => f.relationName).map((field) => {
|
|
42
61
|
return field.name;
|
|
43
62
|
});
|
|
44
63
|
};
|
|
64
|
+
const nullableRelationsForModel = (model) => {
|
|
65
|
+
return model.fields.filter((f) => f.kind === "object" && !f.isList && !f.isRequired).map((f) => f.name);
|
|
66
|
+
};
|
|
45
67
|
const intForeignKeysForModel = (model) => {
|
|
46
68
|
return model.fields.filter((f) => f.name.match(/Id$/) && f.type === "Int").map((f) => f.name);
|
|
47
69
|
};
|
|
@@ -74,14 +96,18 @@ const mapPrismaScalarToPagePropTsType = (scalarType) => {
|
|
|
74
96
|
return prismaScalarToTsType[scalarType] || "unknown";
|
|
75
97
|
};
|
|
76
98
|
export {
|
|
99
|
+
DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
77
100
|
SENSITIVE_FIELDS,
|
|
78
101
|
forcePluralizeWord,
|
|
79
102
|
intForeignKeysForModel,
|
|
80
103
|
mapPrismaScalarToPagePropTsType,
|
|
81
104
|
mapRouteParamTypeToTsType,
|
|
105
|
+
nullableJsonFieldsInInput,
|
|
106
|
+
nullableRelationsForModel,
|
|
82
107
|
pathName,
|
|
83
108
|
redactedModelFields,
|
|
84
109
|
relationsForModel,
|
|
85
110
|
removeGeneratorName,
|
|
111
|
+
requiredFieldsInUpdateInput,
|
|
86
112
|
validateName
|
|
87
113
|
};
|