@percepta/create 4.1.16 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +24 -152
- package/dist/index.js.map +1 -1
- package/dist/{register-app-BeSQEsel.js → register-app-BtvxQeo0.js} +91 -1
- package/dist/register-app-BtvxQeo0.js.map +1 -0
- package/package.json +1 -1
- package/template-versions.json +2 -2
- package/templates/monorepo/README.md +28 -1
- package/templates/monorepo/auth/src/auth.ts +2 -4
- package/templates/monorepo/authentik/blueprints/local-dev.yaml +123 -0
- package/templates/monorepo/authentik/initdb/00-authentik.sql +5 -0
- package/templates/monorepo/docker-compose.yml +70 -0
- package/templates/webapp/AGENTS.md +3 -3
- package/templates/webapp/README.md +22 -33
- package/templates/webapp/agent-skills/oneshot.md +1 -1
- package/templates/webapp/e2e/rbac.spec.ts +28 -4
- package/templates/webapp/env.example.template +8 -12
- package/templates/webapp/scripts/seed.ts +14 -45
- package/templates/webapp/src/app/(auth)/auth/signin/SignInForm.tsx +59 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +3 -3
- package/templates/webapp/src/lib/auth/index.ts +1 -2
- package/dist/register-app-BeSQEsel.js.map +0 -1
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +0 -179
- package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +0 -135
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +0 -53
- package/templates/webapp/src/lib/auth/app-auth-mode.ts +0 -12
|
@@ -124,14 +124,38 @@ test.describe("RBAC access", () => {
|
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
// Apps authenticate via Authentik SSO (there is no password form), so sign-in
|
|
128
|
+
// drives Authentik's hosted login flow. `pnpm run setup` starts the monorepo's
|
|
129
|
+
// local Authentik and seeds these users (password "password") via its blueprint.
|
|
130
|
+
// The provider uses implicit consent, so there is no consent screen. Inputs are
|
|
131
|
+
// targeted by Authentik's stable field names; each stage is submitted via its
|
|
132
|
+
// submit button (Authentik's web-component form doesn't submit on Enter). The
|
|
133
|
+
// identification stage is awaited to detach before the password stage so the two
|
|
134
|
+
// stages don't race during the in-place re-render. (Verified end-to-end against
|
|
135
|
+
// a live local Authentik: each user yields a valid OAuth code + state.)
|
|
127
136
|
async function signIn(page: Page, email: string, callbackUrl: string) {
|
|
128
137
|
await page.goto(
|
|
129
138
|
`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`,
|
|
130
139
|
);
|
|
131
|
-
await page.
|
|
132
|
-
|
|
133
|
-
await page.
|
|
134
|
-
|
|
140
|
+
await page.getByRole("button", { name: /Continue with Authentik/i }).click();
|
|
141
|
+
|
|
142
|
+
await page.waitForURL(/\/\/localhost:9000\//);
|
|
143
|
+
const identifier = page.locator('input[name="uidField"]');
|
|
144
|
+
await identifier.waitFor({ state: "visible" });
|
|
145
|
+
await identifier.fill(email);
|
|
146
|
+
await page.locator('button[type="submit"]').first().click();
|
|
147
|
+
await identifier.waitFor({ state: "detached" });
|
|
148
|
+
|
|
149
|
+
const passwordField = page.locator('input[name="password"]');
|
|
150
|
+
await passwordField.waitFor({ state: "visible" });
|
|
151
|
+
await passwordField.fill(password);
|
|
152
|
+
await page.locator('button[type="submit"]').first().click();
|
|
153
|
+
|
|
154
|
+
// Back on the app once Authentik redirects through the OAuth callback.
|
|
155
|
+
await page.waitForURL(
|
|
156
|
+
(url) =>
|
|
157
|
+
!url.href.includes("localhost:9000") && url.pathname !== "/auth/signin",
|
|
158
|
+
);
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
async function expectNotFound(page: Page) {
|
|
@@ -6,19 +6,15 @@ APP_BASE_URL=http://localhost:3000
|
|
|
6
6
|
# App Database
|
|
7
7
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5434/__DB_NAME__
|
|
8
8
|
|
|
9
|
-
# Authentication (Better Auth)
|
|
10
|
-
# App auth setup selected by @percepta/create: __AUTH_MODE_LABEL__
|
|
11
|
-
# Defaults to the workspace auth setup unless this app was scaffolded with an override.
|
|
12
|
-
AUTH_MODE=__AUTH_MODE__
|
|
9
|
+
# Authentication (Better Auth via Authentik SSO)
|
|
13
10
|
BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# OKTA_ISSUER=
|
|
11
|
+
# These point at the monorepo's local Authentik container (brought up by
|
|
12
|
+
# `pnpm run setup`) and the shared local OIDC client provisioned by its
|
|
13
|
+
# blueprint. Deployed environments override them with the per-app Authentik
|
|
14
|
+
# provider's issuer/credentials.
|
|
15
|
+
AUTHENTIK_ISSUER=http://localhost:9000/application/o/mosaic-local/
|
|
16
|
+
AUTHENTIK_CLIENT_ID=mosaic-local-client
|
|
17
|
+
AUTHENTIK_CLIENT_SECRET=mosaic-local-secret
|
|
22
18
|
|
|
23
19
|
# Shared Auth Database
|
|
24
20
|
# Deployed apps should set this from the customer monorepo auth database Secret.
|
|
@@ -11,42 +11,37 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
12
|
import * as nextEnvModule from "@next/env";
|
|
13
13
|
import type { SubjectRef } from "@percepta/access-control";
|
|
14
|
-
import {
|
|
15
|
-
ensurePerceptaAuthModeEnv,
|
|
16
|
-
type PerceptaAuthMode,
|
|
17
|
-
} from "@percepta/auth/better-auth";
|
|
18
14
|
|
|
19
15
|
const nextEnv =
|
|
20
16
|
(nextEnvModule as { default?: typeof nextEnvModule }).default ??
|
|
21
17
|
nextEnvModule;
|
|
22
18
|
|
|
19
|
+
// Local users mirror the accounts in the monorepo's Authentik seed blueprint
|
|
20
|
+
// (same emails, password "password"). Seeding creates the local user row +
|
|
21
|
+
// SpiceDB grants; the first Authentik sign-in links to it by email.
|
|
23
22
|
const SEEDED_USERS = [
|
|
24
23
|
{
|
|
25
24
|
access: "customer_admin",
|
|
26
25
|
email: "customer-admin@example.com",
|
|
27
26
|
name: "Customer Admin",
|
|
28
|
-
password: "password",
|
|
29
27
|
role: "admin",
|
|
30
28
|
},
|
|
31
29
|
{
|
|
32
30
|
access: "app_admin",
|
|
33
31
|
email: "app-admin@example.com",
|
|
34
32
|
name: "App Admin",
|
|
35
|
-
password: "password",
|
|
36
33
|
role: "admin",
|
|
37
34
|
},
|
|
38
35
|
{
|
|
39
36
|
access: "app_user",
|
|
40
37
|
email: "app-user@example.com",
|
|
41
38
|
name: "App User",
|
|
42
|
-
password: "password",
|
|
43
39
|
role: "user",
|
|
44
40
|
},
|
|
45
41
|
{
|
|
46
42
|
access: "none",
|
|
47
43
|
email: "non-user@example.com",
|
|
48
44
|
name: "App Non User",
|
|
49
|
-
password: "password",
|
|
50
45
|
role: "user",
|
|
51
46
|
},
|
|
52
47
|
] as const;
|
|
@@ -61,13 +56,8 @@ interface AdminCreateUserApi {
|
|
|
61
56
|
}): Promise<{ user: { id: string } }>;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
const DEFAULT_AUTH_MODE = "__AUTH_MODE__" satisfies PerceptaAuthMode;
|
|
65
|
-
|
|
66
59
|
async function main(): Promise<void> {
|
|
67
60
|
nextEnv.loadEnvConfig(process.cwd());
|
|
68
|
-
const authMode = ensurePerceptaAuthModeEnv({
|
|
69
|
-
defaultAuthMode: DEFAULT_AUTH_MODE,
|
|
70
|
-
});
|
|
71
61
|
// oxlint-disable-next-line typescript/no-explicit-any
|
|
72
62
|
(globalThis as any).AsyncLocalStorage = AsyncLocalStorage;
|
|
73
63
|
|
|
@@ -107,42 +97,21 @@ async function main(): Promise<void> {
|
|
|
107
97
|
"Seed user already exists.",
|
|
108
98
|
);
|
|
109
99
|
} else {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// The admin plugin can create local access principals when this app
|
|
122
|
-
// starts with an external OAuth provider instead of credentials.
|
|
123
|
-
const adminApi = auth.api as typeof auth.api & AdminCreateUserApi;
|
|
124
|
-
const res = await adminApi.createUser({
|
|
125
|
-
body: {
|
|
126
|
-
email: seededUser.email,
|
|
127
|
-
name: seededUser.name,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
userId = res.user.id;
|
|
131
|
-
}
|
|
132
|
-
|
|
100
|
+
// Apps authenticate via Authentik, so create the local user row with the
|
|
101
|
+
// admin plugin (no password). SpiceDB grants attach to this row, and the
|
|
102
|
+
// first Authentik sign-in links the OIDC identity to it by email.
|
|
103
|
+
const adminApi = auth.api as typeof auth.api & AdminCreateUserApi;
|
|
104
|
+
const res = await adminApi.createUser({
|
|
105
|
+
body: {
|
|
106
|
+
email: seededUser.email,
|
|
107
|
+
name: seededUser.name,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
userId = res.user.id;
|
|
133
111
|
logger.info(
|
|
134
112
|
{ safe: { email: seededUser.email, userId } },
|
|
135
113
|
"Seed user created.",
|
|
136
114
|
);
|
|
137
|
-
if (authMode === "username-password") {
|
|
138
|
-
logger.info(
|
|
139
|
-
{
|
|
140
|
-
safe: { email: seededUser.email },
|
|
141
|
-
unsafe: { password: seededUser.password },
|
|
142
|
-
},
|
|
143
|
-
"Seed user password configured.",
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
115
|
}
|
|
147
116
|
|
|
148
117
|
await authDb
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@percepta/design";
|
|
4
|
+
import { ArrowRight } from "lucide-react";
|
|
5
|
+
import { useSearchParams } from "next/navigation";
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { authClient } from "../../../../lib/auth-client";
|
|
9
|
+
|
|
10
|
+
export function SignInForm() {
|
|
11
|
+
const searchParams = useSearchParams();
|
|
12
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
13
|
+
|
|
14
|
+
const callbackUrl = searchParams.get("callbackUrl") ?? "/";
|
|
15
|
+
|
|
16
|
+
const signIn = useCallback(async (): Promise<void> => {
|
|
17
|
+
setIsSubmitting(true);
|
|
18
|
+
try {
|
|
19
|
+
const { error } = await authClient.signIn.oauth2({
|
|
20
|
+
providerId: "authentik",
|
|
21
|
+
callbackURL: callbackUrl,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (error != null) {
|
|
25
|
+
toast.error(error.message ?? "Unable to sign in with Authentik.");
|
|
26
|
+
}
|
|
27
|
+
} finally {
|
|
28
|
+
setIsSubmitting(false);
|
|
29
|
+
}
|
|
30
|
+
}, [callbackUrl]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="grid gap-8">
|
|
34
|
+
<div className="grid gap-3">
|
|
35
|
+
<p className="app-auth-kicker">
|
|
36
|
+
<span>01</span>
|
|
37
|
+
<span>Sign in</span>
|
|
38
|
+
</p>
|
|
39
|
+
<h1 className="app-auth-title">Welcome back.</h1>
|
|
40
|
+
<p className="app-auth-copy text-sm">
|
|
41
|
+
Use your Authentik account to continue.
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="app-auth-form">
|
|
45
|
+
<div className="flex justify-end">
|
|
46
|
+
<Button
|
|
47
|
+
className="app-auth-submit"
|
|
48
|
+
type="button"
|
|
49
|
+
loading={isSubmitting}
|
|
50
|
+
onClick={signIn}
|
|
51
|
+
>
|
|
52
|
+
<span>Continue with Authentik</span>
|
|
53
|
+
<ArrowRight aria-hidden={true} />
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import { redirect } from "next/navigation";
|
|
3
3
|
import { Suspense } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getServerSession } from "../../../../lib/auth";
|
|
5
|
+
import { SignInForm } from "./SignInForm";
|
|
6
6
|
|
|
7
7
|
export const metadata: Metadata = {
|
|
8
8
|
title: "Sign In — __APP_TITLE__",
|
|
@@ -21,7 +21,7 @@ export default async function SignInPage() {
|
|
|
21
21
|
<p className="text-center text-sm text-muted-foreground">Loading…</p>
|
|
22
22
|
}
|
|
23
23
|
>
|
|
24
|
-
<
|
|
24
|
+
<SignInForm />
|
|
25
25
|
</Suspense>
|
|
26
26
|
);
|
|
27
27
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { auth, type BetterAuthSession } from "@__REPO_NAME__/auth";
|
|
2
2
|
import { headers } from "next/headers";
|
|
3
|
-
import { AUTH_MODE, type AuthMode } from "./app-auth-mode";
|
|
4
3
|
|
|
5
|
-
export {
|
|
4
|
+
export { auth, type BetterAuthSession };
|
|
6
5
|
|
|
7
6
|
export async function getServerSession() {
|
|
8
7
|
return auth.api.getSession({
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"register-app-BeSQEsel.js","names":[],"sources":["../src/commands/infra/register-app.ts"],"sourcesContent":["import path from \"node:path\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { isMap, isSeq, parseDocument } from \"yaml\";\nimport {\n toKebabCase,\n toSnakeCase,\n toTitleCase,\n} from \"../../utils/case-converters.js\";\nimport { detectMonorepo } from \"../../utils/detect-monorepo.js\";\nimport { validateProjectName } from \"../../utils/validate.js\";\nimport { readWorkspaceManifest } from \"../../utils/workspace-manifest.js\";\nimport {\n createInfraGitHubApi,\n createOrUpdateInfraPullRequestFiles,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n type InfraPullRequestFile,\n resolveGitHubToken,\n} from \"./github.js\";\n\nconst OS_POSTGRESQL_TERRAFORM_SUFFIX = \"postgresql-terraform\";\nconst LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS = \"os-postgresql-terraform\";\nconst OS_POSTGRESQL_TERRAFORM_SERVICES = new Set([\n \"os-postgresql-terraform-aws\",\n \"os-postgresql-terraform-azure\",\n]);\nconst OS_BLUEPRINT_INPUT_GROUPS = [\n {\n name: \"general\",\n displayName: \"General\",\n description: \"Shared OS infrastructure settings.\",\n },\n {\n name: \"applications\",\n displayName: \"Applications\",\n description: \"Generated OS webapp settings.\",\n },\n {\n name: \"aws_postgresql\",\n displayName: \"AWS PostgreSQL\",\n description: \"AWS Aurora PostgreSQL settings.\",\n condition: '{{ eq EnvironmentProviderType \"aws\" }}',\n },\n {\n name: \"azure_postgresql\",\n displayName: \"Azure PostgreSQL\",\n description: \"Azure PostgreSQL Flexible Server settings.\",\n condition: '{{ eq EnvironmentProviderType \"azure\" }}',\n },\n];\n\nexport interface RegisterAppResult {\n appName: string;\n blueprintName: string;\n blueprintPath: string;\n branchName: string;\n customerSlug: string;\n pullRequestUrl: string | null;\n repository: typeof INFRA_REPOSITORY;\n status: \"already_registered\" | \"created_pr\" | \"updated_pr\";\n servicePath: string;\n targetPath: string;\n}\n\nexport async function registerApp(\n appNameInput: string,\n args: {\n cwd?: string;\n github?: InfraGitHubApi;\n } = {},\n): Promise<RegisterAppResult> {\n const appName = normalizeAppName(appNameInput);\n const cwd = args.cwd ?? process.cwd();\n const monorepoContext = await detectMonorepo(cwd);\n if (!monorepoContext.found || !monorepoContext.rootDir) {\n throw new Error(\n \"Run this command from a Mosaic customer monorepo with a .mosaic-workspace.json file.\",\n );\n }\n\n const workspaceManifest = await readWorkspaceManifest(\n monorepoContext.rootDir,\n );\n const customerSlug = workspaceManifest?.customerSlug;\n if (!customerSlug) {\n throw new Error(\n \".mosaic-workspace.json is missing customerSlug. Recreate the monorepo with a current @percepta/create.\",\n );\n }\n\n const github = args.github ?? createInfraGitHubApi(resolveGitHubToken());\n const blueprintName = `${customerSlug}-os`;\n const branchName = `blueberry/register-${customerSlug}-${appName}`;\n const blueprintPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n const servicePath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"services\",\n `${appName}.service.yaml`,\n ].join(\"/\");\n\n const mainBlueprintFile = await github.getFile(\n blueprintPath,\n INFRA_BASE_BRANCH,\n );\n if (!mainBlueprintFile) {\n throw new Error(\n `${blueprintPath} does not exist in ${INFRA_REPOSITORY}. Run \\`pnpm mosaic infra register-os-blueprint\\` and merge that infra PR first.`,\n );\n }\n\n const mainServiceFile = await github.getFile(servicePath, INFRA_BASE_BRANCH);\n const serviceContent =\n mainServiceFile == null\n ? await readLocalServiceDefinition(monorepoContext.rootDir, appName)\n : null;\n const blueprintContent = registerAppInBlueprint(\n mainBlueprintFile.content,\n appName,\n );\n\n const files: InfraPullRequestFile[] = [];\n if (blueprintContent !== mainBlueprintFile.content) {\n files.push({\n baseFileSha: mainBlueprintFile.sha,\n content: blueprintContent,\n message: `Register ${appName} in ${blueprintName}`,\n path: blueprintPath,\n });\n }\n if (serviceContent != null) {\n files.push({\n content: serviceContent,\n message: `Register ${appName} service`,\n path: servicePath,\n });\n }\n\n if (files.length === 0) {\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n servicePath,\n targetPath: blueprintPath,\n };\n }\n\n const pullRequest = await createOrUpdateInfraPullRequestFiles({\n branchName,\n github,\n files,\n title: `Register ${appName} app`,\n body: [\n `Registers the ${appName} service and deployment in ${blueprintName}.`,\n \"\",\n \"Generated by `mosaic infra register-app`.\",\n ].join(\"\\n\"),\n });\n\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n servicePath,\n targetPath: blueprintPath,\n };\n}\n\nexport async function registerAppCommand(appName: string): Promise<void> {\n try {\n const result = await registerApp(appName);\n\n if (result.status === \"already_registered\") {\n console.log(\n chalk.green(\"✔\"),\n `${result.appName} is already registered in ${result.repository} at`,\n chalk.cyan(result.targetPath),\n );\n return;\n }\n\n const verb =\n result.status === \"created_pr\" ? \"Created\" : \"Updated existing\";\n console.log(\n chalk.green(\"✔\"),\n `${verb} infra PR for ${result.appName}:`,\n chalk.cyan(result.pullRequestUrl),\n );\n } catch (error) {\n console.error(chalk.red(\"Error:\"), (error as Error).message);\n process.exit(1);\n }\n}\n\nexport function addAppDatabaseToBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: false,\n appInputs: false,\n });\n}\n\nexport function registerAppInBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: true,\n appInputs: true,\n });\n}\n\nfunction updateBlueprint(\n blueprintContent: string,\n appName: string,\n options: {\n appDatabase: boolean;\n appInstallation: boolean;\n appInputs: boolean;\n },\n): string {\n const document = parseDocument(blueprintContent);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid OS blueprint YAML: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const spec = document.get(\"spec\", true);\n if (!isMap(spec)) {\n throw new Error(\"OS blueprint must include a spec map.\");\n }\n\n let changed = false;\n const inputs = spec.get(\"inputs\", true);\n if (!isSeq(inputs)) {\n throw new Error(\"OS blueprint spec.inputs must be a sequence.\");\n }\n\n changed = ensureInputGroups(document, spec) || changed;\n\n if (options.appInputs) {\n changed =\n addAppInput(document, inputs, renderIngressDomainInput()) || changed;\n changed =\n addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n }\n\n if (options.appDatabase) {\n changed = addAppDatabase(document, inputs, appName) || changed;\n }\n\n if (options.appInstallation) {\n const installations = spec.get(\"installations\", true);\n if (!isSeq(installations)) {\n throw new Error(\"OS blueprint spec.installations must be a sequence.\");\n }\n const postgresqlInstallationName = getOsPostgresqlInstallationName(\n getBlueprintName(document),\n );\n changed =\n ensureOsPostgresqlInstallationAlias(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n ensureAppInstallationPostgresqlOutputRefs(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n addAppInstallation(\n document,\n installations,\n appName,\n postgresqlInstallationName,\n ) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\n}\n\nfunction ensureInputGroups(\n document: ReturnType<typeof parseDocument>,\n spec: {\n get(key: string, keepScalar?: true): unknown;\n set(key: string, value: unknown): void;\n },\n): boolean {\n let changed = false;\n const inputGroups = spec.get(\"inputGroups\", true);\n\n if (inputGroups == null) {\n spec.set(\"inputGroups\", document.createNode(OS_BLUEPRINT_INPUT_GROUPS));\n return true;\n }\n\n if (!isSeq(inputGroups)) {\n throw new Error(\"OS blueprint spec.inputGroups must be a sequence.\");\n }\n\n for (const group of OS_BLUEPRINT_INPUT_GROUPS) {\n const exists = inputGroups.items.some(\n (item) => isMap(item) && item.get(\"name\") === group.name,\n );\n if (exists) continue;\n\n inputGroups.add(document.createNode(group));\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureOsPostgresqlInstallationAlias(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n\n const service = installation.get(\"service\");\n if (\n typeof service !== \"string\" ||\n !OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n ) {\n continue;\n }\n\n if (installation.get(\"name\") === postgresqlInstallationName) continue;\n\n installation.set(\"name\", postgresqlInstallationName);\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureAppInstallationPostgresqlOutputRefs(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n if (isOsPostgresqlInstallation(installation)) continue;\n\n const env = installation.get(\"env\", true);\n if (!isSeq(env)) continue;\n\n for (const envVar of env.items) {\n if (!isMap(envVar)) continue;\n\n const valueFromOutput = envVar.get(\"valueFromOutput\", true);\n if (!isMap(valueFromOutput)) continue;\n if (\n valueFromOutput.get(\"serviceInstallation\") !==\n LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS\n ) {\n continue;\n }\n\n const outputName = valueFromOutput.get(\"name\");\n if (\n typeof outputName !== \"string\" ||\n (outputName !== \"auth_database_url\" &&\n !outputName.startsWith(\"app_database_urls.\"))\n ) {\n continue;\n }\n\n valueFromOutput.set(\"serviceInstallation\", postgresqlInstallationName);\n changed = true;\n }\n }\n\n return changed;\n}\n\nfunction isOsPostgresqlInstallation(installation: {\n get(key: string, keepScalar?: true): unknown;\n}): boolean {\n const service = installation.get(\"service\");\n return (\n typeof service === \"string\" && OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n );\n}\n\nfunction getBlueprintName(document: ReturnType<typeof parseDocument>): string {\n const metadata = document.get(\"metadata\", true);\n if (!isMap(metadata)) {\n throw new Error(\"OS blueprint must include a metadata map.\");\n }\n\n const name = metadata.get(\"name\");\n if (typeof name !== \"string\" || name.length === 0) {\n throw new Error(\"OS blueprint metadata.name must be a non-empty string.\");\n }\n\n return name;\n}\n\nfunction getOsPostgresqlInstallationName(blueprintName: string): string {\n return `${blueprintName}-${OS_POSTGRESQL_TERRAFORM_SUFFIX}`;\n}\n\nfunction addAppInput(\n document: ReturnType<typeof parseDocument>,\n inputs: { add(value: unknown): void; items: unknown[] },\n input: Record<string, unknown> & { name: string },\n): boolean {\n if (\n inputs.items.some((item) => isMap(item) && item.get(\"name\") === input.name)\n ) {\n return false;\n }\n\n inputs.add(document.createNode(input));\n return true;\n}\n\nfunction addAppDatabase(\n document: ReturnType<typeof parseDocument>,\n inputs: { items: unknown[] },\n appName: string,\n): boolean {\n const appDatabasesInput = inputs.items.find(\n (item) => isMap(item) && item.get(\"name\") === \"app_databases\",\n );\n if (!isMap(appDatabasesInput)) {\n throw new Error(\"OS blueprint must include an app_databases input.\");\n }\n\n const defaultValue = appDatabasesInput.get(\"default\", true);\n if (!isMap(defaultValue)) {\n throw new Error(\"OS blueprint app_databases default must be a map.\");\n }\n\n if (defaultValue.has(appName)) return false;\n\n defaultValue.flow = false;\n const appDatabaseValue = document.createNode({\n schema_name: toSnakeCase(appName),\n });\n defaultValue.set(appName, appDatabaseValue);\n return true;\n}\n\nfunction addAppInstallation(\n document: ReturnType<typeof parseDocument>,\n installations: { add(value: unknown): void; items: unknown[] },\n appName: string,\n postgresqlInstallationName: string,\n): boolean {\n if (\n installations.items.some(\n (item) => isMap(item) && item.get(\"service\") === appName,\n )\n ) {\n return false;\n }\n\n installations.add(\n document.createNode({\n service: appName,\n env: renderAppInstallationEnv(appName, postgresqlInstallationName),\n config: renderAppInstallationConfig(appName),\n }),\n );\n return true;\n}\n\nfunction renderIngressDomainInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: ingressDomainInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Ingress Domain\",\n description: \"Shared ingress domain for generated OS webapps.\",\n default: '{{ default \"example.local\" .ryvn.env.state.public_domain.name }}',\n };\n}\n\nfunction renderBetterAuthSecretInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: betterAuthSecretInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: `${toTitleCase(appName)} Better Auth Secret`,\n description: `Generated Better Auth signing secret for ${appName}.`,\n hidden: true,\n generated: {\n type: \"random-bytes\",\n length: 32,\n },\n };\n}\n\nfunction renderLangfusePublicKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfusePublicKeyInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Langfuse Public Key\",\n description:\n \"Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export.\",\n default: \"\",\n };\n}\n\nfunction renderLangfuseSecretKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfuseSecretKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Langfuse Secret Key\",\n description:\n \"Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export.\",\n condition: `{{ ne (input \"${langfusePublicKeyInputName()}\") \"\" }}`,\n };\n}\n\nfunction renderInngestEventKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestEventKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Event Key\",\n description: \"Shared Inngest event key for generated OS webapps.\",\n };\n}\n\nfunction renderInngestSigningKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestSigningKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Signing Key\",\n description: \"Shared Inngest signing key for generated OS webapps.\",\n };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n postgresqlInstallationName: string,\n): Array<Record<string, unknown>> {\n const appInternalEndpoint = `http://${appName}-web-server.{{ .ryvn.env.name }}.svc.cluster.local:3000/api/inngest`;\n\n return [\n {\n key: \"DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: `app_database_urls.${appName}`,\n },\n },\n {\n key: \"AUTH_DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: \"auth_database_url\",\n },\n },\n {\n key: \"DATABASE_SCHEMA\",\n value: toSnakeCase(appName),\n },\n {\n key: \"INGRESS_DOMAIN\",\n valueFromInput: {\n name: ingressDomainInputName(),\n },\n },\n {\n key: \"APP_BASE_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"BETTER_AUTH_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"DEPLOYMENT_ENVIRONMENT\",\n value: \"{{ .ryvn.env.name }}\",\n },\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"INNGEST_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"inngest_base_url\",\n },\n },\n {\n key: \"INNGEST_EVENT_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestEventKeyInputName(),\n },\n },\n {\n key: \"INNGEST_SIGNING_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestSigningKeyInputName(),\n },\n },\n {\n key: \"INNGEST_APP_URL\",\n value: appInternalEndpoint,\n },\n {\n key: \"INNGEST_SERVE_HOST\",\n value: appInternalEndpoint,\n },\n {\n key: \"LANGFUSE_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"langfuse_internal_url\",\n },\n },\n {\n key: \"LANGFUSE_PUBLIC_KEY\",\n valueFromInput: {\n name: langfusePublicKeyInputName(),\n },\n },\n {\n key: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(),\n },\n },\n {\n key: \"OTEL_EXPORTER_OTLP_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"otel_exporter_otlp_endpoint\",\n },\n },\n {\n key: \"SPICEDB_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_endpoint\",\n },\n },\n {\n key: \"SPICEDB_PRESHARED_KEY\",\n isSecret: true,\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_preshared_key\",\n },\n },\n {\n key: \"SPICEDB_INSECURE\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_insecure\",\n },\n },\n ];\n}\n\nfunction renderAppInstallationConfig(appName: string): string {\n const appHost = `${appName}.{{ input \"${ingressDomainInputName()}\" }}`;\n\n return [\n \"replicaCount: 1\",\n \"\",\n \"service:\",\n \" port: 3000\",\n \"\",\n \"livenessEnabled: true\",\n \"readinessEnabled: true\",\n \"startupEnabled: true\",\n \"\",\n \"resources:\",\n \" requests:\",\n ' cpu: \"100m\"',\n \" memory: 256Mi\",\n \" limits:\",\n ' cpu: \"500m\"',\n \" memory: 512Mi\",\n \"\",\n \"ingress:\",\n \" enabled: true\",\n \" className: external-nginx\",\n \" annotations:\",\n \" cert-manager.io/cluster-issuer: external-issuer\",\n ' nginx.ingress.kubernetes.io/ssl-redirect: \"true\"',\n \" hosts:\",\n ` - host: '${appHost}'`,\n \" paths:\",\n \" - path: /\",\n \" pathType: Prefix\",\n \" tls:\",\n ` - secretName: ${appName}-tls`,\n \" hosts:\",\n ` - '${appHost}'`,\n \"\",\n ].join(\"\\n\");\n}\n\nasync function readLocalServiceDefinition(\n monorepoRoot: string,\n appName: string,\n): Promise<string> {\n const serviceDefinitionPath = path.join(\n monorepoRoot,\n \"packages\",\n appName,\n \"deploy\",\n \"ryvn\",\n `${appName}.service.yaml`,\n );\n if (!(await fs.pathExists(serviceDefinitionPath))) {\n throw new Error(\n `${serviceDefinitionPath} does not exist. Add the app's Ryvn service definition before registering it in infra.`,\n );\n }\n\n const content = await fs.readFile(serviceDefinitionPath, \"utf-8\");\n validateLocalServiceDefinition(content, appName, serviceDefinitionPath);\n return content.endsWith(\"\\n\") ? content : `${content}\\n`;\n}\n\nfunction validateLocalServiceDefinition(\n content: string,\n appName: string,\n serviceDefinitionPath: string,\n): void {\n const document = parseDocument(content);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid Ryvn service YAML at ${serviceDefinitionPath}: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const service = document.toJS() as {\n kind?: unknown;\n metadata?: { name?: unknown };\n };\n if (service.kind !== \"Service\" || service.metadata?.name !== appName) {\n throw new Error(\n `${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`,\n );\n }\n}\n\nfunction ingressDomainInputName(): string {\n return \"ingress_domain\";\n}\n\nfunction betterAuthSecretInputName(appName: string): string {\n return `${toSnakeCase(appName)}_better_auth_secret`;\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_key\";\n}\n\nfunction inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction normalizeAppName(appNameInput: string): string {\n const appName = toKebabCase(appNameInput);\n const validation = validateProjectName(appName);\n if (!validation.valid) {\n throw new Error(`Invalid app name: ${validation.error}`);\n }\n return appName;\n}\n"],"mappings":"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ACE;EACF,SAAS;CACX;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aACE;EACF,WAAW,iBAAiB,2BAA2B,EAAE;CAC3D;AACF;AAEA,SAAS,6BAEP;CACA,OAAO;EACL,MAAM,yBAAyB;EAC/B,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,yBACP,SACA,4BACgC;CAChC,MAAM,sBAAsB,UAAU,QAAQ;CAE9C,OAAO;EACL;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM,qBAAqB;GAC7B;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,OAAO,YAAY,OAAO;EAC5B;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,uBAAuB,EAC/B;EACF;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,OAAO,EACzC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,yBAAyB,EACjC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;CACF;AACF;AAEA,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,UAAU,GAAG,QAAQ,aAAa,uBAAuB,EAAE;CAEjE,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAgB,QAAQ;EACxB;EACA;EACA;EACA;EACA,qBAAqB,QAAQ;EAC7B;EACA,cAAc,QAAQ;EACtB;CACF,EAAE,KAAK,IAAI;AACb;AAEA,eAAe,2BACb,cACA,SACiB;CACjB,MAAM,wBAAwB,KAAK,KACjC,cACA,YACA,SACA,UACA,QACA,GAAG,QAAQ,cACb;CACA,IAAI,CAAE,MAAM,GAAG,WAAW,qBAAqB,GAC7C,MAAM,IAAI,MACR,GAAG,sBAAsB,uFAC3B;CAGF,MAAM,UAAU,MAAM,GAAG,SAAS,uBAAuB,OAAO;CAChE,+BAA+B,SAAS,SAAS,qBAAqB;CACtE,OAAO,QAAQ,SAAS,IAAI,IAAI,UAAU,GAAG,QAAQ;AACvD;AAEA,SAAS,+BACP,SACA,SACA,uBACM;CACN,MAAM,WAAW,cAAc,OAAO;CACtC,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,gCAAgC,sBAAsB,IAAI,SAAS,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAAI,GACnH;CAGF,MAAM,UAAU,SAAS,KAAK;CAI9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,UAAU,SAAS,SAC3D,MAAM,IAAI,MACR,GAAG,sBAAsB,iDAAiD,QAAQ,EACpF;AAEJ;AAEA,SAAS,yBAAiC;CACxC,OAAO;AACT;AAEA,SAAS,0BAA0B,SAAyB;CAC1D,OAAO,GAAG,YAAY,OAAO,EAAE;AACjC;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,2BAAmC;CAC1C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,YAAY;CACxC,MAAM,aAAa,oBAAoB,OAAO;CAC9C,IAAI,CAAC,WAAW,OACd,MAAM,IAAI,MAAM,qBAAqB,WAAW,OAAO;CAEzD,OAAO;AACT"}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
-
import { Button, Input } from "@percepta/design";
|
|
5
|
-
import { ArrowRight } from "lucide-react";
|
|
6
|
-
import Link from "next/link";
|
|
7
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
8
|
-
import React, { useCallback, useState } from "react";
|
|
9
|
-
import { Controller, useForm } from "react-hook-form";
|
|
10
|
-
import { toast } from "sonner";
|
|
11
|
-
import z from "zod";
|
|
12
|
-
import { FormItem } from "../../../../components/form/FormItem";
|
|
13
|
-
import { IS_DEV } from "../../../../config/isDev";
|
|
14
|
-
import { authClient } from "../../../../lib/auth-client";
|
|
15
|
-
|
|
16
|
-
const CREDENTIALS_SCHEMA = z.object({
|
|
17
|
-
email: z.string().min(1, "Email is required"),
|
|
18
|
-
password: z.string().min(1, "Password is required"),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
type Credentials = z.infer<typeof CREDENTIALS_SCHEMA>;
|
|
22
|
-
type AuthMode = "username-password" | "google" | "okta";
|
|
23
|
-
|
|
24
|
-
// Defaults are empty in production so deployed instances don't suggest seeded
|
|
25
|
-
// credentials. In dev we prefill with the seeded user from `pnpm db:seed` so a
|
|
26
|
-
// fresh scaffold is one click away from authed.
|
|
27
|
-
const DEFAULTS: Credentials = IS_DEV
|
|
28
|
-
? { email: "app-admin@example.com", password: "password" }
|
|
29
|
-
: { email: "", password: "" };
|
|
30
|
-
|
|
31
|
-
export function CredentialsSignInForm({ authMode }: { authMode: AuthMode }) {
|
|
32
|
-
const router = useRouter();
|
|
33
|
-
const searchParams = useSearchParams();
|
|
34
|
-
const [isProviderSubmitting, setIsProviderSubmitting] = useState(false);
|
|
35
|
-
|
|
36
|
-
const {
|
|
37
|
-
control,
|
|
38
|
-
formState: { isSubmitting },
|
|
39
|
-
handleSubmit,
|
|
40
|
-
} = useForm<Credentials>({
|
|
41
|
-
resolver: zodResolver(CREDENTIALS_SCHEMA),
|
|
42
|
-
defaultValues: DEFAULTS,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const callbackUrl = searchParams.get("callbackUrl") ?? "/";
|
|
46
|
-
const usesCredentials = authMode === "username-password";
|
|
47
|
-
const providerName = authMode === "google" ? "Google" : "Okta";
|
|
48
|
-
|
|
49
|
-
const submit = useCallback(
|
|
50
|
-
async ({ email, password }: Credentials): Promise<void> => {
|
|
51
|
-
const { error } = await authClient.signIn.email({
|
|
52
|
-
email,
|
|
53
|
-
password,
|
|
54
|
-
callbackURL: callbackUrl,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (error != null) {
|
|
58
|
-
toast.error(
|
|
59
|
-
error.message ??
|
|
60
|
-
"Invalid email or password. Please check your credentials and try again.",
|
|
61
|
-
);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
router.push(callbackUrl);
|
|
66
|
-
router.refresh();
|
|
67
|
-
},
|
|
68
|
-
[callbackUrl, router],
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const signInWithProvider = useCallback(async (): Promise<void> => {
|
|
72
|
-
setIsProviderSubmitting(true);
|
|
73
|
-
try {
|
|
74
|
-
const { error } =
|
|
75
|
-
authMode === "google"
|
|
76
|
-
? await authClient.signIn.social({
|
|
77
|
-
provider: "google",
|
|
78
|
-
callbackURL: callbackUrl,
|
|
79
|
-
})
|
|
80
|
-
: await authClient.signIn.oauth2({
|
|
81
|
-
providerId: "okta",
|
|
82
|
-
callbackURL: callbackUrl,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (error != null) {
|
|
86
|
-
toast.error(error.message ?? `Unable to sign in with ${providerName}.`);
|
|
87
|
-
}
|
|
88
|
-
} finally {
|
|
89
|
-
setIsProviderSubmitting(false);
|
|
90
|
-
}
|
|
91
|
-
}, [authMode, callbackUrl, providerName]);
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div className="grid gap-8">
|
|
95
|
-
<div className="grid gap-3">
|
|
96
|
-
<p className="app-auth-kicker">
|
|
97
|
-
<span>01</span>
|
|
98
|
-
<span>Sign in</span>
|
|
99
|
-
</p>
|
|
100
|
-
<h1 className="app-auth-title">Welcome back.</h1>
|
|
101
|
-
{usesCredentials ? (
|
|
102
|
-
<p className="app-auth-copy text-sm">
|
|
103
|
-
Need an account?{" "}
|
|
104
|
-
<Link
|
|
105
|
-
className="app-auth-link"
|
|
106
|
-
href={`/auth/signup?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
107
|
-
>
|
|
108
|
-
Create one
|
|
109
|
-
</Link>
|
|
110
|
-
</p>
|
|
111
|
-
) : (
|
|
112
|
-
<p className="app-auth-copy text-sm">
|
|
113
|
-
Use your {providerName} account to continue.
|
|
114
|
-
</p>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
{usesCredentials ? (
|
|
118
|
-
<form className="app-auth-form" onSubmit={handleSubmit(submit)}>
|
|
119
|
-
<div className="app-auth-fields">
|
|
120
|
-
<Controller
|
|
121
|
-
control={control}
|
|
122
|
-
name="email"
|
|
123
|
-
render={({ field, fieldState }) => (
|
|
124
|
-
<FormItem
|
|
125
|
-
className="app-auth-field"
|
|
126
|
-
label="Email"
|
|
127
|
-
fieldState={fieldState}
|
|
128
|
-
>
|
|
129
|
-
<Input {...field} type="email" autoComplete="email" />
|
|
130
|
-
</FormItem>
|
|
131
|
-
)}
|
|
132
|
-
/>
|
|
133
|
-
<Controller
|
|
134
|
-
control={control}
|
|
135
|
-
name="password"
|
|
136
|
-
render={({ field, fieldState }) => (
|
|
137
|
-
<FormItem
|
|
138
|
-
className="app-auth-field"
|
|
139
|
-
label="Password"
|
|
140
|
-
fieldState={fieldState}
|
|
141
|
-
>
|
|
142
|
-
<Input
|
|
143
|
-
{...field}
|
|
144
|
-
type="password"
|
|
145
|
-
autoComplete="current-password"
|
|
146
|
-
/>
|
|
147
|
-
</FormItem>
|
|
148
|
-
)}
|
|
149
|
-
/>
|
|
150
|
-
</div>
|
|
151
|
-
<div className="flex justify-end">
|
|
152
|
-
<Button
|
|
153
|
-
className="app-auth-submit"
|
|
154
|
-
type="submit"
|
|
155
|
-
loading={isSubmitting}
|
|
156
|
-
>
|
|
157
|
-
<span>Sign In</span>
|
|
158
|
-
<ArrowRight aria-hidden={true} />
|
|
159
|
-
</Button>
|
|
160
|
-
</div>
|
|
161
|
-
</form>
|
|
162
|
-
) : (
|
|
163
|
-
<div className="app-auth-form">
|
|
164
|
-
<div className="flex justify-end">
|
|
165
|
-
<Button
|
|
166
|
-
className="app-auth-submit"
|
|
167
|
-
type="button"
|
|
168
|
-
loading={isProviderSubmitting}
|
|
169
|
-
onClick={signInWithProvider}
|
|
170
|
-
>
|
|
171
|
-
<span>Continue with {providerName}</span>
|
|
172
|
-
<ArrowRight aria-hidden={true} />
|
|
173
|
-
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
-
import { Button, Input } from "@percepta/design";
|
|
5
|
-
import { ArrowRight } from "lucide-react";
|
|
6
|
-
import Link from "next/link";
|
|
7
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
8
|
-
import React, { useCallback } from "react";
|
|
9
|
-
import { Controller, useForm } from "react-hook-form";
|
|
10
|
-
import { toast } from "sonner";
|
|
11
|
-
import z from "zod";
|
|
12
|
-
import { FormItem } from "../../../../components/form/FormItem";
|
|
13
|
-
import { authClient } from "../../../../lib/auth-client";
|
|
14
|
-
|
|
15
|
-
const CREDENTIALS_SCHEMA = z.object({
|
|
16
|
-
name: z.string().min(1, "Name is required"),
|
|
17
|
-
email: z.string().email("Enter a valid email address"),
|
|
18
|
-
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
type Credentials = z.infer<typeof CREDENTIALS_SCHEMA>;
|
|
22
|
-
|
|
23
|
-
export function CredentialsSignUpForm() {
|
|
24
|
-
const router = useRouter();
|
|
25
|
-
const searchParams = useSearchParams();
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
control,
|
|
29
|
-
formState: { isSubmitting },
|
|
30
|
-
handleSubmit,
|
|
31
|
-
} = useForm<Credentials>({
|
|
32
|
-
resolver: zodResolver(CREDENTIALS_SCHEMA),
|
|
33
|
-
defaultValues: {
|
|
34
|
-
name: "",
|
|
35
|
-
email: "",
|
|
36
|
-
password: "",
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const callbackUrl = searchParams.get("callbackUrl") ?? "/";
|
|
41
|
-
|
|
42
|
-
const submit = useCallback(
|
|
43
|
-
async ({ name, email, password }: Credentials): Promise<void> => {
|
|
44
|
-
const { error } = await authClient.signUp.email({
|
|
45
|
-
name,
|
|
46
|
-
email,
|
|
47
|
-
password,
|
|
48
|
-
callbackURL: callbackUrl,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (error != null) {
|
|
52
|
-
toast.error(error.message ?? "Unable to create account.");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
router.push(callbackUrl);
|
|
57
|
-
router.refresh();
|
|
58
|
-
},
|
|
59
|
-
[callbackUrl, router],
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="grid gap-8">
|
|
64
|
-
<div className="grid gap-3">
|
|
65
|
-
<p className="app-auth-kicker">
|
|
66
|
-
<span>01</span>
|
|
67
|
-
<span>Request access</span>
|
|
68
|
-
</p>
|
|
69
|
-
<h1 className="app-auth-title">Create account.</h1>
|
|
70
|
-
<p className="app-auth-copy text-sm">
|
|
71
|
-
Already have an account?{" "}
|
|
72
|
-
<Link
|
|
73
|
-
className="app-auth-link"
|
|
74
|
-
href={`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
75
|
-
>
|
|
76
|
-
Sign in
|
|
77
|
-
</Link>
|
|
78
|
-
</p>
|
|
79
|
-
</div>
|
|
80
|
-
<form className="app-auth-form" onSubmit={handleSubmit(submit)}>
|
|
81
|
-
<div className="app-auth-fields">
|
|
82
|
-
<Controller
|
|
83
|
-
control={control}
|
|
84
|
-
name="name"
|
|
85
|
-
render={({ field, fieldState }) => (
|
|
86
|
-
<FormItem
|
|
87
|
-
className="app-auth-field"
|
|
88
|
-
label="Name"
|
|
89
|
-
fieldState={fieldState}
|
|
90
|
-
>
|
|
91
|
-
<Input {...field} autoComplete="name" />
|
|
92
|
-
</FormItem>
|
|
93
|
-
)}
|
|
94
|
-
/>
|
|
95
|
-
<Controller
|
|
96
|
-
control={control}
|
|
97
|
-
name="email"
|
|
98
|
-
render={({ field, fieldState }) => (
|
|
99
|
-
<FormItem
|
|
100
|
-
className="app-auth-field"
|
|
101
|
-
label="Email"
|
|
102
|
-
fieldState={fieldState}
|
|
103
|
-
>
|
|
104
|
-
<Input {...field} type="email" autoComplete="email" />
|
|
105
|
-
</FormItem>
|
|
106
|
-
)}
|
|
107
|
-
/>
|
|
108
|
-
<Controller
|
|
109
|
-
control={control}
|
|
110
|
-
name="password"
|
|
111
|
-
render={({ field, fieldState }) => (
|
|
112
|
-
<FormItem
|
|
113
|
-
className="app-auth-field"
|
|
114
|
-
label="Password"
|
|
115
|
-
fieldState={fieldState}
|
|
116
|
-
>
|
|
117
|
-
<Input {...field} type="password" autoComplete="new-password" />
|
|
118
|
-
</FormItem>
|
|
119
|
-
)}
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="flex justify-end">
|
|
123
|
-
<Button
|
|
124
|
-
className="app-auth-submit"
|
|
125
|
-
type="submit"
|
|
126
|
-
loading={isSubmitting}
|
|
127
|
-
>
|
|
128
|
-
<span>Create Account</span>
|
|
129
|
-
<ArrowRight aria-hidden={true} />
|
|
130
|
-
</Button>
|
|
131
|
-
</div>
|
|
132
|
-
</form>
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
}
|