@percepta/create 3.0.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/README.md +93 -0
- package/dist/chunk-GEVZERMP.js +108 -0
- package/dist/chunk-R4FWPE4A.js +49 -0
- package/dist/chunk-WMJT7CB5.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +974 -0
- package/dist/init-Z4VGBHAK.js +96 -0
- package/dist/status-MITGDLTT.js +76 -0
- package/dist/sync-J4SFZHDX.js +136 -0
- package/dist/upstream-AQI7P4EU.js +144 -0
- package/package.json +58 -0
- package/template-versions.json +4 -0
- package/templates/library/README.md +30 -0
- package/templates/library/eslint.config.js +10 -0
- package/templates/library/gitignore.template +18 -0
- package/templates/library/package.json.template +29 -0
- package/templates/library/src/index.ts +9 -0
- package/templates/library/tsconfig.json +19 -0
- package/templates/monorepo/README.md +41 -0
- package/templates/monorepo/eslint.config.js +10 -0
- package/templates/monorepo/gitignore.template +31 -0
- package/templates/monorepo/npmrc.template +4 -0
- package/templates/monorepo/package.json.template +25 -0
- package/templates/monorepo/packages/.gitkeep +0 -0
- package/templates/monorepo/pnpm-workspace.yaml +2 -0
- package/templates/monorepo/tsconfig.json +16 -0
- package/templates/webapp/.claude/commands/sync.md +19 -0
- package/templates/webapp/.claude/commands/upstream.md +17 -0
- package/templates/webapp/.dockerignore +59 -0
- package/templates/webapp/.gitattributes +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
- package/templates/webapp/.github/workflows/ci.yml +149 -0
- package/templates/webapp/.node-version +2 -0
- package/templates/webapp/.prettierrc.mjs +5 -0
- package/templates/webapp/AGENTS.md +240 -0
- package/templates/webapp/Dockerfile +64 -0
- package/templates/webapp/README.md +200 -0
- package/templates/webapp/agent-skills/database.md +140 -0
- package/templates/webapp/agent-skills/deploy.md +94 -0
- package/templates/webapp/agent-skills/inngest.md +147 -0
- package/templates/webapp/agent-skills/langfuse.md +117 -0
- package/templates/webapp/agent-skills/oneshot.md +216 -0
- package/templates/webapp/agent-skills/ryvn.md +25 -0
- package/templates/webapp/deploy/README.md +39 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
- package/templates/webapp/docker-compose.yml +19 -0
- package/templates/webapp/drizzle.config.ts +30 -0
- package/templates/webapp/env.example.template +44 -0
- package/templates/webapp/eslint.config.mjs +52 -0
- package/templates/webapp/gitignore.template +53 -0
- package/templates/webapp/next.config.ts +8 -0
- package/templates/webapp/npmrc.template +4 -0
- package/templates/webapp/package.json.template +122 -0
- package/templates/webapp/postcss.config.mjs +5 -0
- package/templates/webapp/scripts/create-user.ts +47 -0
- package/templates/webapp/scripts/migrate.ts +18 -0
- package/templates/webapp/scripts/seed.ts +62 -0
- package/templates/webapp/scripts/setup-database.ts +57 -0
- package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
- package/templates/webapp/scripts/start.sh +52 -0
- package/templates/webapp/src/app/(app)/layout.tsx +21 -0
- package/templates/webapp/src/app/(app)/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
- package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/webapp/src/app/api/healthz/route.ts +10 -0
- package/templates/webapp/src/app/api/inngest/route.ts +31 -0
- package/templates/webapp/src/app/api/readyz/route.ts +31 -0
- package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
- package/templates/webapp/src/app/favicon.ico +0 -0
- package/templates/webapp/src/app/global-error.tsx +27 -0
- package/templates/webapp/src/app/layout.tsx +18 -0
- package/templates/webapp/src/components/FaroProvider.tsx +37 -0
- package/templates/webapp/src/components/Header.tsx +70 -0
- package/templates/webapp/src/components/Providers.tsx +45 -0
- package/templates/webapp/src/components/form/FormItem.tsx +82 -0
- package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
- package/templates/webapp/src/config/getEnvConfig.ts +62 -0
- package/templates/webapp/src/config/isDev.ts +7 -0
- package/templates/webapp/src/drizzle/db.ts +28 -0
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
- package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
- package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
- package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
- package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
- package/templates/webapp/src/drizzle/schema/index.ts +4 -0
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
- package/templates/webapp/src/instrumentation.ts +35 -0
- package/templates/webapp/src/lib/auth/index.ts +85 -0
- package/templates/webapp/src/lib/auth-client.ts +6 -0
- package/templates/webapp/src/lib/trpc.ts +15 -0
- package/templates/webapp/src/server/api/root.ts +5 -0
- package/templates/webapp/src/server/trpc.ts +61 -0
- package/templates/webapp/src/services/AuthContextService.ts +63 -0
- package/templates/webapp/src/services/DatabaseService.ts +54 -0
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
- package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
- package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
- package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
- package/templates/webapp/src/services/observability/initFaro.ts +22 -0
- package/templates/webapp/src/startup-checks.ts +32 -0
- package/templates/webapp/src/styles/globals.css +27 -0
- package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
- package/templates/webapp/src/utils/cn.ts +6 -0
- package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
- package/templates/webapp/terraform/README.md +147 -0
- package/templates/webapp/terraform/deploy.sh +97 -0
- package/templates/webapp/terraform/main.tf +101 -0
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
- package/templates/webapp/terraform/modules/networking/main.tf +118 -0
- package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
- package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
- package/templates/webapp/terraform/modules/rds/main.tf +227 -0
- package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
- package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
- package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
- package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
- package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
- package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
- package/templates/webapp/terraform/outputs.tf +102 -0
- package/templates/webapp/terraform/providers.tf +32 -0
- package/templates/webapp/terraform/terraform.tfvars.example +65 -0
- package/templates/webapp/terraform/variables.tf +129 -0
- package/templates/webapp/tsconfig.json +14 -0
- package/templates/webapp/vitest.config.ts +9 -0
- package/templates/webapp/vitest.setup.ts +5 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GetSecretValueCommand,
|
|
5
|
+
SecretsManagerClient,
|
|
6
|
+
} from "@aws-sdk/client-secrets-manager";
|
|
7
|
+
import { loadEnvConfig } from "@next/env";
|
|
8
|
+
import { Pool } from "pg";
|
|
9
|
+
import { getEnvConfig } from "../src/config/getEnvConfig";
|
|
10
|
+
|
|
11
|
+
interface ReadonlyCredentials {
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
database: string;
|
|
17
|
+
engine: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getReadonlyCredentials(): Promise<ReadonlyCredentials> {
|
|
21
|
+
const client = new SecretsManagerClient({});
|
|
22
|
+
|
|
23
|
+
// Get the secret name from environment
|
|
24
|
+
// Example format: rds-{app}-readonly-{name}-{environment}-*
|
|
25
|
+
const { READONLY_SECRET_NAME: secretName } = getEnvConfig();
|
|
26
|
+
if (secretName == null) {
|
|
27
|
+
throw new Error("READONLY_SECRET_NAME environment variable not set");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const command = new GetSecretValueCommand({ SecretId: secretName });
|
|
31
|
+
const response = await client.send(command);
|
|
32
|
+
|
|
33
|
+
if (!response.SecretString) {
|
|
34
|
+
throw new Error("Secret value is empty");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return JSON.parse(response.SecretString) as ReadonlyCredentials;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function setupReadonlyUser(): Promise<number> {
|
|
41
|
+
console.log("🔐 Fetching readonly user credentials from Secrets Manager...");
|
|
42
|
+
|
|
43
|
+
let credentials: ReadonlyCredentials;
|
|
44
|
+
try {
|
|
45
|
+
credentials = await getReadonlyCredentials();
|
|
46
|
+
console.log(`✅ Retrieved credentials for user: ${credentials.username}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(
|
|
49
|
+
"❌ Failed to retrieve credentials from Secrets Manager:",
|
|
50
|
+
error,
|
|
51
|
+
);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Connect as master user
|
|
56
|
+
const {
|
|
57
|
+
DATABASE_HOST: host,
|
|
58
|
+
DATABASE_PORT: port,
|
|
59
|
+
DATABASE_USERNAME: user,
|
|
60
|
+
DATABASE_PASSWORD: password,
|
|
61
|
+
DATABASE_NAME: database,
|
|
62
|
+
DATABASE_USE_SSL: useSSL,
|
|
63
|
+
} = getEnvConfig();
|
|
64
|
+
const masterPool = new Pool({
|
|
65
|
+
host,
|
|
66
|
+
port,
|
|
67
|
+
user,
|
|
68
|
+
password,
|
|
69
|
+
database,
|
|
70
|
+
ssl: useSSL ? { rejectUnauthorized: false } : false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let client;
|
|
74
|
+
try {
|
|
75
|
+
client = await masterPool.connect();
|
|
76
|
+
console.log("✅ Connected to database as master user");
|
|
77
|
+
|
|
78
|
+
// Check if readonly user already exists
|
|
79
|
+
const userExistsResult = await client.query(
|
|
80
|
+
"SELECT 1 FROM pg_roles WHERE rolname = $1",
|
|
81
|
+
[credentials.username],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
let userCreated = false;
|
|
85
|
+
if (userExistsResult.rows.length === 0) {
|
|
86
|
+
// Create the readonly user
|
|
87
|
+
console.log(`📝 Creating user: ${credentials.username}`);
|
|
88
|
+
await client.query(
|
|
89
|
+
`
|
|
90
|
+
CREATE USER ${credentials.username} WITH LOGIN PASSWORD $1
|
|
91
|
+
`,
|
|
92
|
+
[credentials.password],
|
|
93
|
+
);
|
|
94
|
+
userCreated = true;
|
|
95
|
+
console.log(`✅ User ${credentials.username} created`);
|
|
96
|
+
} else {
|
|
97
|
+
// Update password to ensure it matches Secrets Manager
|
|
98
|
+
console.log(
|
|
99
|
+
`📝 Updating password for existing user: ${credentials.username}`,
|
|
100
|
+
);
|
|
101
|
+
await client.query(
|
|
102
|
+
`
|
|
103
|
+
ALTER USER ${credentials.username} WITH PASSWORD $1
|
|
104
|
+
`,
|
|
105
|
+
[credentials.password],
|
|
106
|
+
);
|
|
107
|
+
console.log(`✅ Password updated for ${credentials.username}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Grant CONNECT privilege on database
|
|
111
|
+
await client.query(`
|
|
112
|
+
GRANT CONNECT ON DATABASE ${database} TO ${credentials.username}
|
|
113
|
+
`);
|
|
114
|
+
console.log("✅ Granted CONNECT on database");
|
|
115
|
+
|
|
116
|
+
// Get all schemas (excluding system schemas)
|
|
117
|
+
const schemasResult = await client.query<{ schema_name: string }>(`
|
|
118
|
+
SELECT schema_name
|
|
119
|
+
FROM information_schema.schemata
|
|
120
|
+
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
for (const row of schemasResult.rows) {
|
|
124
|
+
const schemaName = row.schema_name;
|
|
125
|
+
console.log(`📝 Configuring access for schema: ${schemaName}`);
|
|
126
|
+
|
|
127
|
+
// Grant USAGE on schema
|
|
128
|
+
await client.query(`
|
|
129
|
+
GRANT USAGE ON SCHEMA ${schemaName} TO ${credentials.username}
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
// Grant SELECT on all existing tables in schema
|
|
133
|
+
await client.query(`
|
|
134
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA ${schemaName} TO ${credentials.username}
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
// Set default privileges for future tables in schema
|
|
138
|
+
// Grant on tables created by the master user
|
|
139
|
+
await client.query(`
|
|
140
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName}
|
|
141
|
+
GRANT SELECT ON TABLES TO ${credentials.username}
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
console.log(`✅ Configured access for schema: ${schemaName}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Test the readonly user connection
|
|
148
|
+
console.log("🧪 Testing readonly user connection...");
|
|
149
|
+
const readonlyPool = new Pool({
|
|
150
|
+
host,
|
|
151
|
+
port,
|
|
152
|
+
user: credentials.username,
|
|
153
|
+
password: credentials.password,
|
|
154
|
+
database,
|
|
155
|
+
ssl: useSSL ? { rejectUnauthorized: false } : false,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const readonlyClient = await readonlyPool.connect();
|
|
160
|
+
await readonlyClient.query("SELECT 1");
|
|
161
|
+
readonlyClient.release();
|
|
162
|
+
console.log("✅ Readonly user connection test successful");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("⚠️ Readonly user connection test failed:", error);
|
|
165
|
+
return 1;
|
|
166
|
+
} finally {
|
|
167
|
+
await readonlyPool.end();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log("✅ Readonly user setup completed successfully");
|
|
171
|
+
|
|
172
|
+
// Return exit code 2 if user already existed (idempotent case)
|
|
173
|
+
// Return exit code 0 if user was created
|
|
174
|
+
return userCreated ? 0 : 2;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error("❌ Error setting up readonly user:", error);
|
|
177
|
+
return 1;
|
|
178
|
+
} finally {
|
|
179
|
+
if (client) {
|
|
180
|
+
client.release();
|
|
181
|
+
}
|
|
182
|
+
await masterPool.end();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function main(): Promise<void> {
|
|
187
|
+
loadEnvConfig(process.cwd());
|
|
188
|
+
|
|
189
|
+
const exitCode = await setupReadonlyUser();
|
|
190
|
+
process.exit(exitCode);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
void main();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Check if database connection variables are set
|
|
2
|
+
if [ -z "$DATABASE_HOST" ] || [ -z "$DATABASE_USERNAME" ] || [ -z "$DATABASE_NAME" ]; then
|
|
3
|
+
echo "⚠️ Database connection not configured. Skipping migration."
|
|
4
|
+
echo "Required environment variables: DATABASE_HOST, DATABASE_USERNAME, DATABASE_NAME"
|
|
5
|
+
echo "❌ Error: Missing required database environment variables."
|
|
6
|
+
exit 1
|
|
7
|
+
else
|
|
8
|
+
echo "Database configuration found:"
|
|
9
|
+
echo " HOST: $DATABASE_HOST"
|
|
10
|
+
echo " USER: $DATABASE_USERNAME"
|
|
11
|
+
echo " DATABASE: $DATABASE_NAME"
|
|
12
|
+
echo " SSL: ${DATABASE_USE_SSL:-false}"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Run database migrations only if database is configured
|
|
16
|
+
echo "Running database migrations..."
|
|
17
|
+
if pnpm db:setup-and-migrate; then
|
|
18
|
+
echo "✅ Database migrations completed successfully"
|
|
19
|
+
else
|
|
20
|
+
echo "❌ Database migration failed. App will start anyway."
|
|
21
|
+
echo "Check your database configuration and connectivity."
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Setup readonly database user for EDW (only if READONLY_SECRET_NAME is set)
|
|
25
|
+
if [ -n "$READONLY_SECRET_NAME" ]; then
|
|
26
|
+
echo "Setting up readonly database user for EDW..."
|
|
27
|
+
if pnpm db:setup-readonly; then
|
|
28
|
+
echo "✅ Readonly user setup completed"
|
|
29
|
+
else
|
|
30
|
+
EXIT_CODE=$?
|
|
31
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
32
|
+
echo "ℹ️ Readonly user already exists and is configured correctly"
|
|
33
|
+
else
|
|
34
|
+
echo "⚠️ Failed to setup readonly user. Check logs for details."
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
else
|
|
38
|
+
echo "ℹ️ READONLY_SECRET_NAME not set, skipping readonly user setup"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ] && [ -n "$ADMIN_NAME" ]; then
|
|
42
|
+
echo "Creating admin user..."
|
|
43
|
+
if pnpm exec tsx scripts/create-user.ts "$ADMIN_USERNAME" "$ADMIN_PASSWORD" --name "$ADMIN_NAME"; then
|
|
44
|
+
echo "✅ Admin user created (or already exists)."
|
|
45
|
+
else
|
|
46
|
+
echo "⚠️ Failed to create admin user."
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Start the Next.js application
|
|
51
|
+
echo "Starting Next.js server on port ${PORT:-3000}..."
|
|
52
|
+
exec pnpm start
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Header from "../../components/Header";
|
|
3
|
+
|
|
4
|
+
export default function AppLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<Header />
|
|
12
|
+
<main>
|
|
13
|
+
<div className="py-8">
|
|
14
|
+
<div className="mx-auto max-w-6xl">
|
|
15
|
+
<div className="rounded-lg bg-white p-8">{children}</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</main>
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { auth } from "../../lib/auth";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: "__APP_TITLE__",
|
|
8
|
+
description: "__APP_TITLE__",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default async function HomePage() {
|
|
12
|
+
const session = await auth.api.getSession({
|
|
13
|
+
headers: await headers(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (session?.user == null) {
|
|
17
|
+
redirect("/auth/signin");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-8">
|
|
22
|
+
<h1 className="text-center text-2xl font-bold text-foreground">
|
|
23
|
+
Welcome to __APP_TITLE__
|
|
24
|
+
</h1>
|
|
25
|
+
<p className="mx-auto max-w-xl text-center text-sm text-muted-foreground">
|
|
26
|
+
You are logged in. Start building your application!
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { Button, Input } from "@percepta/design";
|
|
5
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
|
+
import { Controller, useForm } from "react-hook-form";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import z from "zod";
|
|
10
|
+
import { FormItem } from "../../../../components/form/FormItem";
|
|
11
|
+
import { authClient } from "../../../../lib/auth-client";
|
|
12
|
+
import { IS_DEV } from "../../../../config/isDev";
|
|
13
|
+
|
|
14
|
+
const CREDENTIALS_SCHEMA = z.object({
|
|
15
|
+
email: z.string().min(1, "Email is required"),
|
|
16
|
+
password: z.string().min(1, "Password is required"),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type Credentials = z.infer<typeof CREDENTIALS_SCHEMA>;
|
|
20
|
+
|
|
21
|
+
// Defaults are empty in production so deployed instances don't suggest seeded
|
|
22
|
+
// credentials. In dev we prefill with the seeded user from `pnpm db:seed` so a
|
|
23
|
+
// fresh scaffold is one click away from authed.
|
|
24
|
+
const DEFAULTS: Credentials = IS_DEV
|
|
25
|
+
? { email: "admin@example.com", password: "password" }
|
|
26
|
+
: { email: "", password: "" };
|
|
27
|
+
|
|
28
|
+
export function CredentialsSignInForm() {
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const searchParams = useSearchParams();
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
control,
|
|
34
|
+
formState: { isSubmitting },
|
|
35
|
+
handleSubmit,
|
|
36
|
+
} = useForm<Credentials>({
|
|
37
|
+
resolver: zodResolver(CREDENTIALS_SCHEMA),
|
|
38
|
+
defaultValues: DEFAULTS,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const callbackUrl = searchParams.get("callbackUrl") ?? "/";
|
|
42
|
+
|
|
43
|
+
const submit = useCallback(
|
|
44
|
+
async ({ email, password }: Credentials): Promise<void> => {
|
|
45
|
+
const { error } = await authClient.signIn.email({
|
|
46
|
+
email,
|
|
47
|
+
password,
|
|
48
|
+
callbackURL: callbackUrl,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (error != null) {
|
|
52
|
+
toast.error(
|
|
53
|
+
error.message ??
|
|
54
|
+
"Invalid email or password. Please check your credentials and try again.",
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
router.push(callbackUrl);
|
|
60
|
+
router.refresh();
|
|
61
|
+
},
|
|
62
|
+
[callbackUrl, router],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-8">
|
|
67
|
+
<div className="text-center">
|
|
68
|
+
<h1 className="text-2xl font-bold text-foreground">Sign In</h1>
|
|
69
|
+
</div>
|
|
70
|
+
<form className="space-y-6" onSubmit={handleSubmit(submit)}>
|
|
71
|
+
<div className="space-y-4">
|
|
72
|
+
<Controller
|
|
73
|
+
control={control}
|
|
74
|
+
name="email"
|
|
75
|
+
render={({ field, fieldState }) => (
|
|
76
|
+
<FormItem label="Email" fieldState={fieldState}>
|
|
77
|
+
<Input {...field} type="email" autoComplete="email" />
|
|
78
|
+
</FormItem>
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
<Controller
|
|
82
|
+
control={control}
|
|
83
|
+
name="password"
|
|
84
|
+
render={({ field, fieldState }) => (
|
|
85
|
+
<FormItem label="Password" fieldState={fieldState}>
|
|
86
|
+
<Input
|
|
87
|
+
{...field}
|
|
88
|
+
type="password"
|
|
89
|
+
autoComplete="current-password"
|
|
90
|
+
/>
|
|
91
|
+
</FormItem>
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex justify-end">
|
|
96
|
+
<Button type="submit" loading={isSubmitting}>
|
|
97
|
+
Sign In
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { Suspense } from "react";
|
|
5
|
+
import { auth } from "../../../../lib/auth";
|
|
6
|
+
import { CredentialsSignInForm } from "./CredentialsSignInForm";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: "Sign In — __APP_TITLE__",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default async function SignInPage() {
|
|
13
|
+
const session = await auth.api.getSession({
|
|
14
|
+
headers: await headers(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (session?.user != null) {
|
|
18
|
+
redirect("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Suspense
|
|
23
|
+
fallback={
|
|
24
|
+
<p className="text-center text-sm text-muted-foreground">Loading…</p>
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<CredentialsSignInForm />
|
|
28
|
+
</Suspense>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function AuthLayout({
|
|
4
|
+
children,
|
|
5
|
+
}: {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4">
|
|
10
|
+
<div className="w-full max-w-md rounded-lg border border-border bg-card p-8 shadow-sm">
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { serve } from "inngest/next";
|
|
2
|
+
import { compact } from "lodash-es";
|
|
3
|
+
import { type InngestFunctionCollection } from "../../../services/inngest/InngestFunctionCollection";
|
|
4
|
+
import { InngestService } from "../../../services/inngest/InngestService";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
const functionCollections: InngestFunctionCollection[] = compact([]);
|
|
9
|
+
|
|
10
|
+
// InngestService.create() reads env vars and throws if INNGEST_BASE_URL is
|
|
11
|
+
// unset. Defer initialization until request time so `next build` can compile
|
|
12
|
+
// this route without those vars present.
|
|
13
|
+
type Handlers = ReturnType<typeof serve>;
|
|
14
|
+
|
|
15
|
+
let cachedHandlers: Handlers | undefined;
|
|
16
|
+
|
|
17
|
+
function getHandlers(): Handlers {
|
|
18
|
+
if (cachedHandlers == null) {
|
|
19
|
+
const inngestService = InngestService.create();
|
|
20
|
+
cachedHandlers = serve({
|
|
21
|
+
client: inngestService.client,
|
|
22
|
+
functions: functionCollections.flatMap(({ functions }) => functions),
|
|
23
|
+
signingKey: inngestService.signingKey,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return cachedHandlers;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const GET: Handlers["GET"] = (...args) => getHandlers().GET(...args);
|
|
30
|
+
export const POST: Handlers["POST"] = (...args) => getHandlers().POST(...args);
|
|
31
|
+
export const PUT: Handlers["PUT"] = (...args) => getHandlers().PUT(...args);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getLogger } from "../../../services/logger/AppLogger";
|
|
2
|
+
import { withAppRouterRequestContext } from "../../../services/logger/withRequestContext";
|
|
3
|
+
import { checkStartup } from "../../../startup-checks";
|
|
4
|
+
import { syncInngestApp } from "../../../utils/syncInngestApp";
|
|
5
|
+
|
|
6
|
+
let hasSynced = false;
|
|
7
|
+
|
|
8
|
+
const handler = withAppRouterRequestContext(async (): Promise<Response> => {
|
|
9
|
+
const isHealthy = await checkStartup();
|
|
10
|
+
if (!isHealthy) {
|
|
11
|
+
return new Response(null, { status: 503 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!hasSynced) {
|
|
15
|
+
try {
|
|
16
|
+
await syncInngestApp();
|
|
17
|
+
hasSynced = true;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
getLogger().warn(
|
|
20
|
+
undefined,
|
|
21
|
+
"Failed to sync with Inngest Server during readiness",
|
|
22
|
+
error,
|
|
23
|
+
);
|
|
24
|
+
return new Response(null, { status: 503 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Response(null, { status: 200 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { handler as GET };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
2
|
+
import { appRouter } from "../../../../server/api/root";
|
|
3
|
+
import { createContext } from "../../../../server/trpc";
|
|
4
|
+
import { getLogger } from "../../../../services/logger/AppLogger";
|
|
5
|
+
import { withAppRouterRequestContext } from "../../../../services/logger/withRequestContext";
|
|
6
|
+
|
|
7
|
+
const handler = withAppRouterRequestContext(
|
|
8
|
+
(req: Request): Promise<Response> => {
|
|
9
|
+
return fetchRequestHandler({
|
|
10
|
+
endpoint: "/api/trpc",
|
|
11
|
+
req,
|
|
12
|
+
router: appRouter,
|
|
13
|
+
createContext,
|
|
14
|
+
onError: ({ error }) => {
|
|
15
|
+
getLogger().error(undefined, "tRPC error.", error);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export { handler as GET, handler as POST };
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export default function GlobalError({
|
|
4
|
+
error,
|
|
5
|
+
reset,
|
|
6
|
+
}: {
|
|
7
|
+
error: Error & { digest?: string };
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}) {
|
|
10
|
+
try {
|
|
11
|
+
const { faro } = require("@grafana/faro-web-sdk");
|
|
12
|
+
faro.api?.pushError(error);
|
|
13
|
+
} catch {
|
|
14
|
+
// Faro may not be initialized yet — don't let reporting break the error page
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body suppressHydrationWarning>
|
|
20
|
+
<div style={{ padding: "2rem", textAlign: "center" }}>
|
|
21
|
+
<h1>Something went wrong</h1>
|
|
22
|
+
<button onClick={() => reset()}>Try again</button>
|
|
23
|
+
</div>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "@percepta/design/styles";
|
|
2
|
+
import "../styles/globals.css";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Providers } from "../components/Providers";
|
|
5
|
+
|
|
6
|
+
export default function RootLayout({
|
|
7
|
+
children,
|
|
8
|
+
}: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body className="antialiased" suppressHydrationWarning={true}>
|
|
14
|
+
<Providers>{children}</Providers>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FaroProvider as BaseFaroProvider } from "@percepta/next-utils/faro";
|
|
4
|
+
import { Alert, AlertTitle, AlertDescription, Button } from "@percepta/design";
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
// Import to trigger Faro initialization at module scope (earliest possible)
|
|
8
|
+
import "../services/observability/initFaro";
|
|
9
|
+
|
|
10
|
+
export function AppFaroProvider({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<BaseFaroProvider fallback={<ErrorFallback />}>
|
|
13
|
+
{children}
|
|
14
|
+
</BaseFaroProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ErrorFallback() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
21
|
+
<Alert variant="destructive" className="max-w-md">
|
|
22
|
+
<AlertTitle>Something went wrong</AlertTitle>
|
|
23
|
+
<AlertDescription>
|
|
24
|
+
An unexpected error occurred. Please refresh the page and try again.
|
|
25
|
+
</AlertDescription>
|
|
26
|
+
<Button
|
|
27
|
+
variant="outline"
|
|
28
|
+
size="sm"
|
|
29
|
+
className="mt-4"
|
|
30
|
+
onClick={() => window.location.reload()}
|
|
31
|
+
>
|
|
32
|
+
Refresh page
|
|
33
|
+
</Button>
|
|
34
|
+
</Alert>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|