Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions packages/cli/lib/saas-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,19 @@ export async function writeSaasFiles(appDir, opts = {}) {
"import { db } from '#db/connection.server.ts';",
"import { users } from '#db/schema.server.ts';",
"import { hash } from '#lib/password.server.ts';",
"",
"import { signIn } from '#lib/auth.server.ts';",
"",
"// Creates the account, then signs the new user in and lands on the",
"// dashboard. signIn returns a 302 Response carrying the session cookie; the",
"// signup page action returns that Response as-is (a page action may return a",
"// Response). signIn lives in the server-only auth module, imported here",
"// server-to-server, so it never reaches the browser (the signup page only",
"// imports this action's RPC stub).",
"export async function signup(input: { name: string; email: string; password: string }) {",
" const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
" if (exists) return { success: false as const, error: 'Email already registered', status: 409 };",
" const [user] = await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) }).returning();",
" return { success: true as const, data: { id: user.id, name: user.name, email: user.email } };",
" await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) });",
" return signIn('credentials', { email: input.email, password: input.password }, { redirectTo: '/dashboard' });",
"}",
"",
].join('\n'));
Expand Down Expand Up @@ -261,10 +268,11 @@ export async function writeSaasFiles(appDir, opts = {}) {
" headers: { 'content-type': 'application/x-www-form-urlencoded' },",
" body: new URLSearchParams({ name: 'Harness', email, password }).toString(),",
" });",
" // Success is a 303 PRG to /login; a 422 means validation failed (still a",
" // real response, just not the happy path). Either way the action ran.",
" assert.ok([303, 422].includes(signupRes.status), 'signup action ran');",
" if (signupRes.status !== 303) canSignup = false;",
" // Success auto-logs-in and 302s to /dashboard (carrying the session",
" // cookie); a 422 means validation failed. Either way the action ran.",
" assert.ok([302, 422].includes(signupRes.status), 'signup action ran');",
" if (signupRes.status === 302) assert.equal(signupRes.headers.get('location'), '/dashboard', 'signup lands on the dashboard');",
" if (signupRes.status !== 302) canSignup = false;",
" } catch {",
" // No migrated DB table -> the action throws. Skip the DB-backed assertions.",
" canSignup = false;",
Expand All @@ -279,6 +287,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
" assert.equal(dash.status, 200, 'the session cookie unlocks the dashboard');",
" const body = await dash.text();",
" assert.match(body, /Dashboard/, 'the dashboard content rendered');",
" // The greeting interpolates the real user, so the name renders and the",
" // literal template source never leaks (a counterfactual for the escaping bug).",
" assert.match(body, /Harness/, 'the dashboard greets the signed-in user by name');",
" assert.ok(!body.includes('${user'), 'the greeting interpolation is not a literal string');",
"});",
"",
].join('\n');
Expand Down Expand Up @@ -317,6 +329,8 @@ export async function writeSaasFiles(appDir, opts = {}) {
" </div>",
" <div class=${cardContentClass()}>",
" <form method=\"POST\" action=\"/api/auth/signin/credentials\" class=\"flex flex-col gap-4\">",
" <!-- createAuth reads redirectTo from the posted form and 302s there after a successful signin. -->",
" <input type=\"hidden\" name=\"redirectTo\" value=\"/dashboard\">",
" <div class=\"flex flex-col gap-1.5\">",
" <label class=${labelClass()} for=\"email\">Email</label>",
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" required>",
Expand Down Expand Up @@ -366,9 +380,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
" if (password.length < 8) fieldErrors.password = 'At least 8 characters';",
" if (Object.keys(fieldErrors).length) return { success: false, fieldErrors, values, status: 422 };",
" const result = await signup({ name, email, password });",
" if (!result.success) return { success: false, fieldErrors: { email: result.error }, values, status: result.status };",
" // Account created. Redirect to login via PRG so a reload will not resubmit.",
" return { success: true, redirect: '/login' };",
" // On success signup returns signIn's 302 Response (auto-login -> /dashboard);",
" // a page action may return a Response, so pass it straight through.",
" if (result instanceof Response) return result;",
" return { success: false, fieldErrors: { email: result.error }, values, status: result.status };",
"}",
"",
"export default function SignupPage({ actionData }: { actionData?: { fieldErrors?: Record<string, string>; values?: Record<string, string> } }) {",
Expand Down Expand Up @@ -445,7 +460,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
" </div>",
" <div class=${cardClass()}>",
" <div class=${cardHeaderClass()}>",
" <h3 class=${cardTitleClass()}>Welcome, ${`\\$\\{user?.name || user?.email\\}`}!</h3>",
" <h3 class=${cardTitleClass()}>Welcome, ${user?.name || user?.email}!</h3>",
" <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
" </div>",
" <div class=${cardContentClass()}>",
Expand Down Expand Up @@ -477,9 +492,9 @@ export async function writeSaasFiles(appDir, opts = {}) {
" <div class=${cardContentClass()}>",
" <dl class=\"grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm\">",
" <dt class=\"text-muted-foreground\">Email</dt>",
" <dd>${`\\$\\{user?.email\\}`}</dd>",
" <dd>${user?.email}</dd>",
" <dt class=\"text-muted-foreground\">Name</dt>",
" <dd>${`\\$\\{user?.name || 'Not set'\\}`}</dd>",
" <dd>${user?.name || 'Not set'}</dd>",
" </dl>",
" </div>",
" </div>",
Expand Down
Loading