@cedarjs/cli 7.1.0 → 7.1.1-next.469
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/build/buildHandler.js +2 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/check.js +2 -1
- package/dist/commands/deploy/baremetal/baremetalHandler.js +364 -73
- package/dist/commands/deploy/baremetal/monitors.js +66 -0
- package/dist/commands/deploy/baremetal.js +7 -1
- package/dist/commands/deploy/flightcontrolHandler.js +1 -1
- package/dist/commands/deploy/helpers/deployHandler.js +1 -1
- package/dist/commands/deploy/netlify.js +1 -1
- package/dist/commands/deploy/renderHandler.js +1 -1
- package/dist/commands/deploy/serverlessHandler.js +2 -1
- package/dist/commands/deploy/vercel.js +1 -1
- package/dist/commands/destroy/handlerHelpers.js +1 -1
- package/dist/commands/destroy/page/pageHandler.js +2 -1
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +65 -26
- package/dist/commands/destroy/sdl/sdlHandler.js +2 -1
- package/dist/commands/dev/devHandler.js +3 -4
- package/dist/commands/dev/packageWatchCommands.js +8 -3
- package/dist/commands/dev.js +1 -1
- package/dist/commands/execHandler.js +2 -1
- package/dist/commands/experimental/live-queries/liveQueries.js +1 -1
- package/dist/commands/experimental/live-queries/liveQueriesHandler.js +3 -3
- package/dist/commands/experimental/setupInngest.js +1 -1
- package/dist/commands/experimental/setupInngestHandler.js +1 -1
- package/dist/commands/experimental/setupOpentelemetry.js +1 -1
- package/dist/commands/experimental/setupOpentelemetryHandler.js +2 -1
- package/dist/commands/experimental/setupReactCompiler.js +1 -1
- package/dist/commands/experimental/setupReactCompilerHandler.js +4 -4
- package/dist/commands/experimental/setupRsc.js +1 -1
- package/dist/commands/experimental/setupRscHandler.js +2 -1
- package/dist/commands/experimental/setupStreamingSsr.js +1 -1
- package/dist/commands/experimental/setupStreamingSsrHandler.js +3 -2
- package/dist/commands/generate/cell/cell.js +20 -0
- package/dist/commands/generate/cell/cellHandler.js +68 -20
- package/dist/commands/generate/cell/templates/cell.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/cellFragment.tsx.template +43 -0
- package/dist/commands/generate/cell/templates/cellList.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +23 -0
- package/dist/commands/generate/cell/templates/test.js.template +1 -1
- package/dist/commands/generate/cell/templates/testFragment.js.template +30 -0
- package/dist/commands/generate/dataMigration/dataMigration.js +2 -1
- package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +80 -17
- package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
- package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +164 -0
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
- package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +147 -0
- package/dist/commands/generate/directive/directiveHandler.js +2 -1
- package/dist/commands/generate/function/functionHandler.js +2 -1
- package/dist/commands/generate/helpers.js +26 -0
- package/dist/commands/generate/job/jobHandler.js +2 -1
- package/dist/commands/generate/model/modelHandler.js +2 -1
- package/dist/commands/generate/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/filesTask.js +9 -12
- package/dist/commands/generate/package/packageHandler.js +3 -4
- package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/page/pageHandler.js +2 -1
- package/dist/commands/generate/realtime/realtime.js +1 -1
- package/dist/commands/generate/realtime/realtimeHandler.js +2 -9
- 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 +4 -4
- package/dist/commands/generate/scaffold/scaffold.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +93 -49
- package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +5 -7
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +10 -9
- package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +11 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +16 -4
- package/dist/commands/generate/script/scriptHandler.js +2 -1
- package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/sdl/sdlHandler.js +10 -4
- package/dist/commands/generate/secret/secret.js +1 -1
- package/dist/commands/generate/service/serviceHandler.js +79 -49
- package/dist/commands/generate/service/templates/service.ts.template +40 -13
- package/dist/commands/generate/service/templates/test.ts.template +8 -6
- package/dist/commands/generate/yargsCommandHelpers.js +1 -1
- package/dist/commands/generate/yargsHandlerHelpers.js +2 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/commands/jobsHandler.js +5 -3
- package/dist/commands/lint.js +1 -1
- package/dist/commands/prerenderHandler.js +11 -4
- package/dist/commands/prismaHandler.js +7 -2
- package/dist/commands/record/init.js +1 -1
- package/dist/commands/serve.js +2 -1
- package/dist/commands/setup/auth/auth.js +2 -4
- package/dist/commands/setup/cache/cache.js +1 -1
- package/dist/commands/setup/cache/cacheHandler.js +2 -1
- 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/database/postgres.js +1 -1
- package/dist/commands/setup/database/postgresHandler.js +3 -1
- package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +130 -21
- package/dist/commands/setup/deploy/providers/coherence.js +1 -1
- package/dist/commands/setup/deploy/providers/coherenceHandler.js +7 -6
- package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/renderHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/serverlessHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/universalDeployHandler.js +4 -6
- package/dist/commands/setup/deploy/providers/vercelHandler.js +2 -1
- package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
- package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
- package/dist/commands/setup/docker/docker.js +1 -1
- package/dist/commands/setup/docker/dockerHandler.js +2 -1
- package/dist/commands/setup/generator/generator.js +1 -1
- package/dist/commands/setup/generator/generatorHandler.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragments.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragmentsHandler.js +3 -5
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +2 -1
- package/dist/commands/setup/i18n/i18n.js +1 -1
- package/dist/commands/setup/i18n/i18nHandler.js +1 -1
- package/dist/commands/setup/jobs/jobs.js +1 -1
- package/dist/commands/setup/jobs/jobsHandler.js +4 -1
- package/dist/commands/setup/mailer/mailer.js +1 -1
- package/dist/commands/setup/mailer/mailerHandler.js +2 -1
- package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
- package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +13 -9
- 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/neon/neon.js +1 -1
- package/dist/commands/setup/neon/neonHandler.js +5 -9
- package/dist/commands/setup/package/package.js +1 -1
- package/dist/commands/setup/package/packageHandler.js +4 -4
- package/dist/commands/setup/realtime/addRealtimeToGraphql.js +1 -1
- package/dist/commands/setup/realtime/realtime.js +1 -1
- package/dist/commands/setup/realtime/realtimeHandler.js +4 -16
- 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/countdown/countdown.ts.template +1 -1
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +2 -2
- package/dist/commands/setup/server-file/serverFile.js +1 -1
- package/dist/commands/setup/server-file/serverFileHandler.js +2 -1
- package/dist/commands/setup/tenancy/authCodemod.js +66 -0
- package/dist/commands/setup/tenancy/dbCodemod.js +64 -0
- package/dist/commands/setup/tenancy/graphqlCodemod.js +88 -0
- package/dist/commands/setup/tenancy/preflight.js +49 -0
- package/dist/commands/setup/tenancy/schemaPrisma.js +81 -0
- package/dist/commands/setup/tenancy/signupCodemod.js +154 -0
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +72 -0
- package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -0
- package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +39 -0
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +57 -0
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +89 -0
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +381 -0
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +394 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +35 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +25 -0
- package/dist/commands/setup/tenancy/tenancy.js +30 -0
- package/dist/commands/setup/tenancy/tenancyHandler.js +425 -0
- package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/mantineHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +63 -82
- package/dist/commands/setup/uploads/gitignore.js +17 -0
- package/dist/commands/setup/uploads/schemaPrisma.js +41 -0
- package/dist/commands/setup/uploads/serverFile.js +217 -0
- package/dist/commands/setup/uploads/templates/requireUploadToken.ts.template +15 -0
- package/dist/commands/setup/uploads/templates/srcLibUploads.ts.template +23 -24
- package/dist/commands/setup/uploads/templates/uploads.sdl.ts.template +79 -0
- package/dist/commands/setup/uploads/templates/uploads.ts.template +98 -0
- package/dist/commands/setup/uploads/templates/withDataUri.ts.template +14 -0
- package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +16 -0
- package/dist/commands/setup/uploads/uploads.js +49 -7
- package/dist/commands/setup/uploads/uploadsHandler.js +277 -69
- package/dist/commands/setup.js +2 -1
- package/dist/commands/studio.js +1 -1
- package/dist/commands/test/testHandler.js +25 -71
- package/dist/commands/test/viteVersionCheck.js +75 -0
- package/dist/commands/test.js +10 -18
- package/dist/commands/type-checkHandler.js +1 -1
- package/dist/commands/upgrade/preUpgradeScripts.js +3 -3
- package/dist/commands/upgrade/upgrade.js +1 -1
- package/dist/commands/upgrade/upgradeHandler.js +2 -1
- package/dist/index.js +5 -8
- package/dist/lib/exit.js +1 -1
- package/dist/lib/index.js +16 -31
- package/dist/lib/loadEnvFiles.js +1 -1
- package/dist/lib/plugin.js +1 -1
- package/dist/lib/test.js +1 -1
- package/dist/lib/updateCheck.js +4 -4
- package/dist/middleware/checkNodeVersion.js +4 -4
- package/package.json +27 -27
- package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
- package/dist/commands/setup/uploads/dbCodemod.js +0 -28
- package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
- package/dist/commands/test/testHandlerEsm.js +0 -109
- package/dist/commands/testEsm.js +0 -41
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ${fragmentName} } from 'types/graphql'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CellSuccessProps,<% if (afterQuery || isEmpty) { %>
|
|
5
|
+
DataObject,<% } %>
|
|
6
|
+
} from '@cedarjs/web'
|
|
7
|
+
|
|
8
|
+
// A parent Cell spreads this fragment by name in its own QUERY, and passes
|
|
9
|
+
// the matching field down as the `${camelName}` prop - no import needed,
|
|
10
|
+
// just render <${pascalName}Cell />:
|
|
11
|
+
//
|
|
12
|
+
// query FindSomething($id: Int!) {
|
|
13
|
+
// something(id: $id) {
|
|
14
|
+
// id
|
|
15
|
+
// ...${fragmentName}
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// <${pascalName}Cell ${camelName}={something} />
|
|
20
|
+
export const FRAGMENT = gql`
|
|
21
|
+
fragment ${fragmentName} on ${fragmentOnType} {
|
|
22
|
+
id
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
<% if (isEmpty) { %>
|
|
26
|
+
export const isEmpty = (
|
|
27
|
+
data: DataObject,
|
|
28
|
+
{ isDataEmpty }: { isDataEmpty: (data: DataObject) => boolean },
|
|
29
|
+
) => {
|
|
30
|
+
return isDataEmpty(data)
|
|
31
|
+
}
|
|
32
|
+
<% } %><% if (afterQuery) { %>
|
|
33
|
+
export const afterQuery = (data: DataObject): DataObject => {
|
|
34
|
+
return data
|
|
35
|
+
}
|
|
36
|
+
<% } %>
|
|
37
|
+
export const Empty = () => <div>Empty</div>
|
|
38
|
+
|
|
39
|
+
export const Success = ({
|
|
40
|
+
${camelName},
|
|
41
|
+
}: CellSuccessProps<{ ${camelName}: ${fragmentName} }>) => {
|
|
42
|
+
return <div>{JSON.stringify(${camelName})}</div>
|
|
43
|
+
}
|
|
@@ -3,7 +3,9 @@ import type { ${operationName}, ${operationName}Variables } from 'types/graphql
|
|
|
3
3
|
import type {
|
|
4
4
|
CellSuccessProps,
|
|
5
5
|
CellFailureProps,
|
|
6
|
-
TypedDocumentNode
|
|
6
|
+
TypedDocumentNode,<% if (beforeQuery) { %>
|
|
7
|
+
CellBeforeQueryResult,<% } %><% if (afterQuery || isEmpty) { %>
|
|
8
|
+
DataObject,<% } %>
|
|
7
9
|
} from '@cedarjs/web'
|
|
8
10
|
|
|
9
11
|
export const QUERY: TypedDocumentNode<
|
|
@@ -16,7 +18,24 @@ export const QUERY: TypedDocumentNode<
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
`
|
|
19
|
-
|
|
21
|
+
<% if (beforeQuery) { %>
|
|
22
|
+
export const beforeQuery = (
|
|
23
|
+
props: ${operationName}Variables,
|
|
24
|
+
): CellBeforeQueryResult<${operationName}Variables> => {
|
|
25
|
+
return { variables: props, fetchPolicy: 'cache-and-network' }
|
|
26
|
+
}
|
|
27
|
+
<% } %><% if (isEmpty) { %>
|
|
28
|
+
export const isEmpty = (
|
|
29
|
+
data: DataObject,
|
|
30
|
+
{ isDataEmpty }: { isDataEmpty: (data: DataObject) => boolean },
|
|
31
|
+
) => {
|
|
32
|
+
return isDataEmpty(data)
|
|
33
|
+
}
|
|
34
|
+
<% } %><% if (afterQuery) { %>
|
|
35
|
+
export const afterQuery = (data: DataObject): DataObject => {
|
|
36
|
+
return data
|
|
37
|
+
}
|
|
38
|
+
<% } %>
|
|
20
39
|
export const Loading = () => <div>Loading...</div>
|
|
21
40
|
|
|
22
41
|
export const Empty = () => <div>Empty</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import { Empty, Success } from './${pascalName}Cell'
|
|
4
|
+
import { standard } from './${pascalName}Cell.mock'
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Cells/${pascalName}Cell',
|
|
8
|
+
tags: ['autodocs']
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
|
|
13
|
+
export const empty: StoryObj<typeof Empty> = {
|
|
14
|
+
render: () => {
|
|
15
|
+
return Empty ? <Empty /> : <></>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const success: StoryObj<typeof Success> = {
|
|
20
|
+
render: (args) => {
|
|
21
|
+
return Success ? <Success {...standard()} {...args} /> : <></>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -7,7 +7,7 @@ import { standard } from './${pascalName}Cell.mock'
|
|
|
7
7
|
// and can fail without adjustments, e.g. Float and DateTime types.
|
|
8
8
|
// Please refer to the RedwoodJS Testing Docs:
|
|
9
9
|
// https://cedarjs.com/docs/testing#testing-cells
|
|
10
|
-
// https://cedarjs.com/docs/testing#
|
|
10
|
+
// https://cedarjs.com/docs/testing#expect-type-considerations
|
|
11
11
|
|
|
12
12
|
describe('${pascalName}Cell', () => {
|
|
13
13
|
it('renders Loading successfully', () => {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render } from '@cedarjs/testing/web'
|
|
2
|
+
|
|
3
|
+
import { Empty, Success } from './${pascalName}Cell'
|
|
4
|
+
import { standard } from './${pascalName}Cell.mock'
|
|
5
|
+
|
|
6
|
+
// Generated boilerplate tests do not account for all circumstances
|
|
7
|
+
// and can fail without adjustments, e.g. Float and DateTime types.
|
|
8
|
+
// Please refer to the RedwoodJS Testing Docs:
|
|
9
|
+
// https://cedarjs.com/docs/testing#testing-cells
|
|
10
|
+
// https://cedarjs.com/docs/testing#expect-type-considerations
|
|
11
|
+
|
|
12
|
+
describe('${pascalName}Cell', () => {
|
|
13
|
+
it('renders Empty successfully', async () => {
|
|
14
|
+
expect(() => {
|
|
15
|
+
render(<Empty />)
|
|
16
|
+
}).not.toThrow()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// When you're ready to test the actual output of your component render
|
|
20
|
+
// you could test that, for example, certain text is present:
|
|
21
|
+
//
|
|
22
|
+
// 1. import { screen } from '@cedarjs/testing/web'
|
|
23
|
+
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
|
|
24
|
+
|
|
25
|
+
it('renders Success successfully', async () => {
|
|
26
|
+
expect(() => {
|
|
27
|
+
render(<Success ${camelName}={standard().${camelName}} />)
|
|
28
|
+
}).not.toThrow()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -2,8 +2,9 @@ import path from "node:path";
|
|
|
2
2
|
import { paramCase } from "change-case";
|
|
3
3
|
import { Listr } from "listr2";
|
|
4
4
|
import { terminalLink } from "termi-link";
|
|
5
|
-
import {
|
|
5
|
+
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
6
6
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
7
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
7
8
|
import { getDataMigrationsPath } from "@cedarjs/project-config";
|
|
8
9
|
import {
|
|
9
10
|
generateTemplate,
|
|
@@ -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
|
|
@@ -4,9 +4,10 @@ import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
|
|
|
4
4
|
import { camelCase } from "camel-case";
|
|
5
5
|
import { Listr } from "listr2";
|
|
6
6
|
import { titleCase } from "title-case";
|
|
7
|
-
import {
|
|
7
|
+
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
8
8
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
9
9
|
import { runBinSync } from "@cedarjs/cli-helpers/packageManager/exec";
|
|
10
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
10
11
|
import {
|
|
11
12
|
addRoutesToRouterTask,
|
|
12
13
|
addScaffoldImport,
|
|
@@ -17,12 +18,25 @@ import {
|
|
|
17
18
|
} from "../../../lib/index.js";
|
|
18
19
|
import { prepareForRollback } from "../../../lib/rollback.js";
|
|
19
20
|
import { templateForComponentFile } from "../yargsHandlerHelpers.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|
|
26
40
|
function getPostInstallMessage(isDbAuthSetup2) {
|
|
27
41
|
return [
|
|
28
42
|
` ${c.warning("Pages created! But you're not done yet:")}
|
|
@@ -34,9 +48,11 @@ function getPostInstallMessage(isDbAuthSetup2) {
|
|
|
34
48
|
" navigate(routes.home())",
|
|
35
49
|
" }\n",
|
|
36
50
|
" and change the route to where you want them to go if the user is already",
|
|
37
|
-
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
|
|
38
|
-
" and
|
|
39
|
-
" 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",
|
|
40
56
|
!isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
|
|
41
57
|
app setup by running:
|
|
42
58
|
|
|
@@ -57,9 +73,11 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
|
|
|
57
73
|
" navigate(routes.home())",
|
|
58
74
|
" }\n",
|
|
59
75
|
" and change the route to where you want them to go if the user is already",
|
|
60
|
-
" logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
|
|
61
|
-
" and
|
|
62
|
-
" 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",
|
|
63
81
|
!isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
|
|
64
82
|
app setup by running:
|
|
65
83
|
|
|
@@ -70,24 +88,28 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
|
|
|
70
88
|
}
|
|
71
89
|
const files = async ({
|
|
72
90
|
typescript,
|
|
91
|
+
skipChange,
|
|
73
92
|
skipForgot,
|
|
74
93
|
skipLogin,
|
|
75
94
|
skipReset,
|
|
76
95
|
skipSignup,
|
|
77
96
|
webauthn,
|
|
97
|
+
oauth,
|
|
78
98
|
usernameLabel,
|
|
79
99
|
passwordLabel
|
|
80
100
|
}) => {
|
|
81
101
|
const filesList = [];
|
|
82
102
|
usernameLabel = usernameLabel || "username";
|
|
83
103
|
passwordLabel = passwordLabel || "password";
|
|
104
|
+
const oauthEnabled = oauth ?? isOAuthEnabled();
|
|
84
105
|
const templateVars = {
|
|
85
106
|
usernameLowerCase: usernameLabel.toLowerCase(),
|
|
86
107
|
usernameCamelCase: camelCase(usernameLabel),
|
|
87
108
|
usernameTitleCase: titleCase(usernameLabel),
|
|
88
109
|
passwordLowerCase: passwordLabel.toLowerCase(),
|
|
89
110
|
passwordCamelCase: camelCase(passwordLabel),
|
|
90
|
-
passwordTitleCase: titleCase(passwordLabel)
|
|
111
|
+
passwordTitleCase: titleCase(passwordLabel),
|
|
112
|
+
passwordPascalCase: pascalCase(passwordLabel)
|
|
91
113
|
};
|
|
92
114
|
if (!skipForgot) {
|
|
93
115
|
filesList.push(
|
|
@@ -110,7 +132,7 @@ const files = async ({
|
|
|
110
132
|
extension: typescript ? ".tsx" : ".jsx",
|
|
111
133
|
webPathSection: "pages",
|
|
112
134
|
generator: "dbAuth",
|
|
113
|
-
templatePath: webauthn
|
|
135
|
+
templatePath: loginTemplatePath(webauthn, oauthEnabled),
|
|
114
136
|
templateVars
|
|
115
137
|
})
|
|
116
138
|
);
|
|
@@ -128,6 +150,19 @@ const files = async ({
|
|
|
128
150
|
})
|
|
129
151
|
);
|
|
130
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
|
+
}
|
|
131
166
|
if (!skipSignup) {
|
|
132
167
|
filesList.push(
|
|
133
168
|
await templateForComponentFile({
|
|
@@ -136,7 +171,7 @@ const files = async ({
|
|
|
136
171
|
extension: typescript ? ".tsx" : ".jsx",
|
|
137
172
|
webPathSection: "pages",
|
|
138
173
|
generator: "dbAuth",
|
|
139
|
-
templatePath: "signup.tsx.template",
|
|
174
|
+
templatePath: oauthEnabled ? "signup.oauth.tsx.template" : "signup.tsx.template",
|
|
140
175
|
templateVars
|
|
141
176
|
})
|
|
142
177
|
);
|
|
@@ -176,6 +211,7 @@ const tasks = ({
|
|
|
176
211
|
listr2,
|
|
177
212
|
force,
|
|
178
213
|
typescript,
|
|
214
|
+
skipChange,
|
|
179
215
|
skipForgot,
|
|
180
216
|
skipLogin,
|
|
181
217
|
skipReset,
|
|
@@ -286,6 +322,7 @@ const tasks = ({
|
|
|
286
322
|
task: async () => {
|
|
287
323
|
const filesObj = await files({
|
|
288
324
|
typescript,
|
|
325
|
+
skipChange,
|
|
289
326
|
skipForgot,
|
|
290
327
|
skipLogin,
|
|
291
328
|
skipReset,
|
|
@@ -302,7 +339,15 @@ const tasks = ({
|
|
|
302
339
|
{
|
|
303
340
|
title: "Adding routes...",
|
|
304
341
|
task: async () => {
|
|
305
|
-
addRoutesToRouterTask(
|
|
342
|
+
addRoutesToRouterTask(
|
|
343
|
+
routesFor({
|
|
344
|
+
skipChange,
|
|
345
|
+
skipForgot,
|
|
346
|
+
skipLogin,
|
|
347
|
+
skipReset,
|
|
348
|
+
skipSignup
|
|
349
|
+
})
|
|
350
|
+
);
|
|
306
351
|
}
|
|
307
352
|
},
|
|
308
353
|
{
|
|
@@ -332,6 +377,7 @@ const tasks = ({
|
|
|
332
377
|
const handler = async (yargs) => {
|
|
333
378
|
recordTelemetryAttributes({
|
|
334
379
|
command: "generate dbAuth",
|
|
380
|
+
skipChange: yargs.skipChange,
|
|
335
381
|
skipForgot: yargs.skipForgot,
|
|
336
382
|
skipLogin: yargs.skipLogin,
|
|
337
383
|
skipReset: yargs.skipReset,
|
|
@@ -378,6 +424,23 @@ function isWebAuthnEnabled() {
|
|
|
378
424
|
);
|
|
379
425
|
return webPackageJson.includes('"@simplewebauthn/browser": ');
|
|
380
426
|
}
|
|
427
|
+
function isOAuthEnabled() {
|
|
428
|
+
const apiPackageJsonPath = path.join(getPaths().base, "api", "package.json");
|
|
429
|
+
if (!fs.existsSync(apiPackageJsonPath)) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const apiPackageJson = fs.readFileSync(apiPackageJsonPath, "utf-8");
|
|
433
|
+
return apiPackageJson.includes('"@cedarjs/auth-dbauth-oauth": ');
|
|
434
|
+
}
|
|
435
|
+
function loginTemplatePath(webauthn, oauth) {
|
|
436
|
+
if (webauthn) {
|
|
437
|
+
return "login.webAuthn.tsx.template";
|
|
438
|
+
}
|
|
439
|
+
if (oauth) {
|
|
440
|
+
return "login.oauth.tsx.template";
|
|
441
|
+
}
|
|
442
|
+
return "login.tsx.template";
|
|
443
|
+
}
|
|
381
444
|
export {
|
|
382
445
|
files,
|
|
383
446
|
handler
|
|
@@ -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
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Form,
|
|
5
|
+
Label,
|
|
6
|
+
TextField,
|
|
7
|
+
PasswordField,
|
|
8
|
+
Submit,
|
|
9
|
+
FieldError,
|
|
10
|
+
} from '@cedarjs/forms'
|
|
11
|
+
import { Link, navigate, routes } from '@cedarjs/router'
|
|
12
|
+
import { Metadata } from '@cedarjs/web'
|
|
13
|
+
import { toast, Toaster } from '@cedarjs/web/toast'
|
|
14
|
+
|
|
15
|
+
import { getOAuthUrl, getOAuthError } from '@cedarjs/auth-dbauth-web/oauth'
|
|
16
|
+
|
|
17
|
+
import { useAuth } from 'src/auth'
|
|
18
|
+
|
|
19
|
+
const LoginPage = () => {
|
|
20
|
+
const { isAuthenticated, logIn } = useAuth()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (isAuthenticated) {
|
|
24
|
+
navigate(routes.home())
|
|
25
|
+
}
|
|
26
|
+
}, [isAuthenticated])
|
|
27
|
+
|
|
28
|
+
// Surface an error that the OAuth provider redirect sent back, e.g.
|
|
29
|
+
// after the user cancels the provider's consent screen.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const oauthError = getOAuthError(window.location.search)
|
|
32
|
+
|
|
33
|
+
if (oauthError) {
|
|
34
|
+
toast.error('Could not log in: ' + oauthError)
|
|
35
|
+
}
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const ${usernameCamelCase}Ref = useRef<HTMLInputElement>(null)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
${usernameCamelCase}Ref.current?.focus()
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
const onSubmit = async (data: Record<string, string>) => {
|
|
44
|
+
const response = await logIn({ username: data.${usernameCamelCase}, password: data.${passwordCamelCase} })
|
|
45
|
+
|
|
46
|
+
if (response.message) {
|
|
47
|
+
toast(response.message)
|
|
48
|
+
} else if (response.error) {
|
|
49
|
+
toast.error(response.error)
|
|
50
|
+
} else {
|
|
51
|
+
toast.success('Welcome back!')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Metadata title="Login" />
|
|
58
|
+
|
|
59
|
+
<main className="rw-main">
|
|
60
|
+
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
|
61
|
+
<div className="rw-scaffold rw-login-container">
|
|
62
|
+
<div className="rw-segment">
|
|
63
|
+
<header className="rw-segment-header">
|
|
64
|
+
<h2 className="rw-heading rw-heading-secondary">Login</h2>
|
|
65
|
+
</header>
|
|
66
|
+
|
|
67
|
+
<div className="rw-segment-main">
|
|
68
|
+
<div className="rw-form-wrapper">
|
|
69
|
+
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
|
70
|
+
<Label
|
|
71
|
+
name="${usernameCamelCase}"
|
|
72
|
+
className="rw-label"
|
|
73
|
+
errorClassName="rw-label rw-label-error"
|
|
74
|
+
>
|
|
75
|
+
${usernameTitleCase}
|
|
76
|
+
</Label>
|
|
77
|
+
<TextField
|
|
78
|
+
name="${usernameCamelCase}"
|
|
79
|
+
className="rw-input"
|
|
80
|
+
errorClassName="rw-input rw-input-error"
|
|
81
|
+
ref={${usernameCamelCase}Ref}
|
|
82
|
+
validation={{
|
|
83
|
+
required: {
|
|
84
|
+
value: true,
|
|
85
|
+
message: '${usernameTitleCase} is required',
|
|
86
|
+
},
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<FieldError name="${usernameCamelCase}" className="rw-field-error" />
|
|
91
|
+
|
|
92
|
+
<Label
|
|
93
|
+
name="${passwordCamelCase}"
|
|
94
|
+
className="rw-label"
|
|
95
|
+
errorClassName="rw-label rw-label-error"
|
|
96
|
+
>
|
|
97
|
+
${passwordTitleCase}
|
|
98
|
+
</Label>
|
|
99
|
+
<PasswordField
|
|
100
|
+
name="${passwordCamelCase}"
|
|
101
|
+
className="rw-input"
|
|
102
|
+
errorClassName="rw-input rw-input-error"
|
|
103
|
+
autoComplete="current-password"
|
|
104
|
+
validation={{
|
|
105
|
+
required: {
|
|
106
|
+
value: true,
|
|
107
|
+
message: '${passwordTitleCase} is required',
|
|
108
|
+
},
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<div className="rw-forgot-link">
|
|
113
|
+
<Link
|
|
114
|
+
to={routes.forgotPassword()}
|
|
115
|
+
className="rw-forgot-link"
|
|
116
|
+
>
|
|
117
|
+
Forgot ${passwordTitleCase}?
|
|
118
|
+
</Link>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<FieldError name="${passwordCamelCase}" className="rw-field-error" />
|
|
122
|
+
|
|
123
|
+
<div className="rw-button-group">
|
|
124
|
+
<Submit className="rw-button rw-button-blue">Login</Submit>
|
|
125
|
+
</div>
|
|
126
|
+
</Form>
|
|
127
|
+
|
|
128
|
+
{/*
|
|
129
|
+
* OAuth login buttons. Delete the ones for providers you
|
|
130
|
+
* haven't configured in `cedar setup auth dbAuth --oauth`.
|
|
131
|
+
* Each link is a full-page navigation to the provider's
|
|
132
|
+
* authorize route — it must not be turned into a fetch/XHR
|
|
133
|
+
* call.
|
|
134
|
+
*/}
|
|
135
|
+
<div className="rw-button-group">
|
|
136
|
+
<a
|
|
137
|
+
href={getOAuthUrl('google', { flow: 'login' })}
|
|
138
|
+
className="rw-button rw-button-blue"
|
|
139
|
+
>
|
|
140
|
+
Continue with Google
|
|
141
|
+
</a>
|
|
142
|
+
<a
|
|
143
|
+
href={getOAuthUrl('github', { flow: 'login' })}
|
|
144
|
+
className="rw-button rw-button-blue"
|
|
145
|
+
>
|
|
146
|
+
Continue with GitHub
|
|
147
|
+
</a>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="rw-login-link">
|
|
153
|
+
<span>Don't have an account?</span>{' '}
|
|
154
|
+
<Link to={routes.signup()} className="rw-link">
|
|
155
|
+
Sign up!
|
|
156
|
+
</Link>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</main>
|
|
160
|
+
</>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default LoginPage
|