diff --git a/packages/cli/lib/saas-template.js b/packages/cli/lib/saas-template.js index aec13bba9..f70156bb1 100644 --- a/packages/cli/lib/saas-template.js +++ b/packages/cli/lib/saas-template.js @@ -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')); @@ -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;", @@ -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'); @@ -317,6 +329,8 @@ export async function writeSaasFiles(appDir, opts = {}) { " ", "
", "
", + " ", + " ", "
", " ", " ", @@ -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; values?: Record } }) {", @@ -445,7 +460,7 @@ export async function writeSaasFiles(appDir, opts = {}) { "
", "
", "
", - "

Welcome, ${`\\$\\{user?.name || user?.email\\}`}!

", + "

Welcome, ${user?.name || user?.email}!

", "

You're authenticated. Replace this scaffold with your real app.

", "
", "
", @@ -477,9 +492,9 @@ export async function writeSaasFiles(appDir, opts = {}) { "
", "
", "
Email
", - "
${`\\$\\{user?.email\\}`}
", + "
${user?.email}
", "
Name
", - "
${`\\$\\{user?.name || 'Not set'\\}`}
", + "
${user?.name || 'Not set'}
", "
", "
", "
",