Enhance authentication setup and improve documentation

Adds automatic auth secret generation, improves environment file handling,
creates client env files, adds trusted origins configuration, enhances
README generation with better structure and instructions, and updates
post-installation guidance with clearer steps.
This commit is contained in:
Aman Varshney
2025-03-16 01:48:45 +05:30
parent 811c849279
commit 036c62cf0b
13 changed files with 266 additions and 135 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
Enhance authentication setup and improve documentation

View File

@@ -1,7 +1,5 @@
import path from "node:path"; import path from "node:path";
import { log } from "@clack/prompts";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectAddons } from "../types"; import type { ProjectAddons } from "../types";
export async function setupAddons(projectDir: string, addons: ProjectAddons[]) { export async function setupAddons(projectDir: string, addons: ProjectAddons[]) {

View File

@@ -50,11 +50,58 @@ export async function configureAuth(
); );
} else { } else {
const envPath = path.join(serverDir, ".env"); const envPath = path.join(serverDir, ".env");
const envExamplePath = path.join(serverDir, "_env"); const templateEnvPath = path.join(
PKG_ROOT,
options.orm === "drizzle"
? "template/with-drizzle/packages/server/_env"
: "template/base/packages/server/_env",
);
if (await fs.pathExists(envExamplePath)) { if (!(await fs.pathExists(envPath))) {
await fs.copy(envExamplePath, envPath); if (await fs.pathExists(templateEnvPath)) {
await fs.remove(envExamplePath); await fs.copy(templateEnvPath, envPath);
} else {
const defaultEnv = `BETTER_AUTH_SECRET=${generateAuthSecret()}
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:3001
${options.database === "sqlite" ? "TURSO_CONNECTION_URL=http://127.0.0.1:8080" : ""}
${options.orm === "prisma" ? 'DATABASE_URL="file:./dev.db"' : ""}
`;
await fs.writeFile(envPath, defaultEnv);
}
} else {
let envContent = await fs.readFile(envPath, "utf8");
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
}
if (!envContent.includes("BETTER_AUTH_URL")) {
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
}
if (!envContent.includes("CORS_ORIGIN")) {
envContent += "\nCORS_ORIGIN=http://localhost:3001";
}
if (
options.database === "sqlite" &&
!envContent.includes("TURSO_CONNECTION_URL")
) {
envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080";
}
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
envContent += '\nDATABASE_URL="file:./dev.db"';
}
await fs.writeFile(envPath, envContent);
}
const clientEnvPath = path.join(clientDir, ".env");
if (!(await fs.pathExists(clientEnvPath))) {
const clientEnvContent = "VITE_SERVER_URL=http://localhost:3000\n";
await fs.writeFile(clientEnvPath, clientEnvContent);
} }
if (options.orm === "prisma") { if (options.orm === "prisma") {
@@ -71,6 +118,15 @@ export async function configureAuth(
await fs.ensureDir(path.dirname(prismaAuthPath)); await fs.ensureDir(path.dirname(prismaAuthPath));
await fs.copy(defaultPrismaAuthPath, prismaAuthPath); await fs.copy(defaultPrismaAuthPath, prismaAuthPath);
} }
let authContent = await fs.readFile(prismaAuthPath, "utf8");
if (!authContent.includes("trustedOrigins")) {
authContent = authContent.replace(
"export const auth = betterAuth({",
"export const auth = betterAuth({\n trustedOrigins: [process.env.CORS_ORIGIN!],",
);
await fs.writeFile(prismaAuthPath, authContent);
}
} else if (options.orm === "drizzle") { } else if (options.orm === "drizzle") {
const drizzleAuthPath = path.join(serverDir, "src/lib/auth.ts"); const drizzleAuthPath = path.join(serverDir, "src/lib/auth.ts");
const defaultDrizzleAuthPath = path.join( const defaultDrizzleAuthPath = path.join(
@@ -95,3 +151,14 @@ export async function configureAuth(
throw error; throw error;
} }
} }
function generateAuthSecret(length = 32): string {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View File

@@ -37,27 +37,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
} }
} }
const gitignoreFiles = [
[
path.join(projectDir, "_gitignore"),
path.join(projectDir, ".gitignore"),
],
[
path.join(projectDir, "packages/client/_gitignore"),
path.join(projectDir, "packages/client/.gitignore"),
],
[
path.join(projectDir, "packages/server/_gitignore"),
path.join(projectDir, "packages/server/.gitignore"),
],
];
for (const [source, target] of gitignoreFiles) {
if (await fs.pathExists(source)) {
await fs.move(source, target);
}
}
const envFiles = [ const envFiles = [
[ [
path.join(projectDir, "packages/server/_env"), path.join(projectDir, "packages/server/_env"),
@@ -112,26 +91,31 @@ export async function createProject(options: ProjectConfig): Promise<string> {
: "bun@1.2.4"; : "bun@1.2.4";
} }
if (options.auth && options.database !== "none") { if (options.database !== "none") {
packageJson.scripts["auth:generate"] = if (options.database === "sqlite") {
"cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts"; packageJson.scripts["db:local"] =
"cd packages/server && turso dev --db-file local.db";
}
if (options.orm === "prisma") { if (options.auth) {
packageJson.scripts["prisma:generate"] = packageJson.scripts["auth:generate"] =
"cd packages/server && npx prisma generate"; "cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts";
packageJson.scripts["prisma:push"] =
"cd packages/server && npx prisma db push";
packageJson.scripts["prisma:studio"] =
"cd packages/server && npx prisma studio";
packageJson.scripts["db:setup"] = if (options.orm === "prisma") {
"npm run auth:generate && npm run prisma:generate && npm run prisma:push"; packageJson.scripts["prisma:generate"] =
} else if (options.orm === "drizzle") { "cd packages/server && npx prisma generate";
packageJson.scripts["drizzle:migrate"] = packageJson.scripts["prisma:push"] =
"cd packages/server && npx @better-auth/cli migrate"; "cd packages/server && npx prisma db push";
packageJson.scripts["prisma:studio"] =
packageJson.scripts["db:setup"] = "cd packages/server && npx prisma studio";
"npm run auth:generate && npm run drizzle:migrate"; packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run prisma:generate && npm run prisma:push";
} else if (options.orm === "drizzle") {
packageJson.scripts["drizzle:migrate"] =
"cd packages/server && npx @better-auth/cli migrate";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run drizzle:migrate";
}
} }
} }

View File

@@ -28,7 +28,7 @@ function generateReadmeContent(options: ProjectConfig): string {
return `# ${projectName} return `# ${projectName}
This project was created with [Better-T-Stack](https://github.com/better-t-stack/Better-T-Stack). This project was created with [Better-T-Stack](https://github.com/better-t-stack/Better-T-Stack), a modern TypeScript stack that combines React, TanStack Router, Hono, tRPC, and more.
## Features ## Features
@@ -42,6 +42,8 @@ First, install the dependencies:
${packageManager} install ${packageManager} install
\`\`\` \`\`\`
${generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)}
Then, run the development server: Then, run the development server:
\`\`\`bash \`\`\`bash
@@ -51,10 +53,6 @@ ${packageManagerRunCmd} dev
Open [http://localhost:3001](http://localhost:3001) in your browser to see the client application. Open [http://localhost:3001](http://localhost:3001) in your browser to see the client application.
The API is running at [http://localhost:3000](http://localhost:3000). The API is running at [http://localhost:3000](http://localhost:3000).
## Database Setup
${generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)}
## Project Structure ## Project Structure
\`\`\` \`\`\`
@@ -64,9 +62,9 @@ ${projectName}/
│ └── server/ # Backend API (Hono, tRPC) │ └── server/ # Backend API (Hono, tRPC)
\`\`\` \`\`\`
## Scripts ## Available Scripts
${generateScriptsList(packageManagerRunCmd)} ${generateScriptsList(packageManagerRunCmd, database, orm, auth)}
`; `;
} }
@@ -77,31 +75,34 @@ function generateFeaturesList(
orm: string, orm: string,
): string { ): string {
const featuresList = [ const featuresList = [
"TypeScript - For type safety", "- **TypeScript** - For type safety and improved developer experience",
"TanStack Router - File-based routing", "- **TanStack Router** - File-based routing with full type safety",
`${orm === "drizzle" ? "Drizzle" : "Prisma"} - ORM`, "- **TailwindCSS** - Utility-first CSS for rapid UI development",
"TailwindCSS - Utility-first CSS", "- **shadcn/ui** - Reusable UI components",
"shadcn/ui - Reusable components", "- **Hono** - Lightweight, performant server framework",
"Hono - Lightweight, performant server", "- **tRPC** - End-to-end type-safe APIs",
]; ];
if (database !== "none") { if (database !== "none") {
featuresList.push( featuresList.push(
`${database === "sqlite" ? "SQLite/Turso DB" : "PostgreSQL"} - Database`, `- **${orm === "drizzle" ? "Drizzle" : "Prisma"}** - TypeScript-first ORM`,
`- **${database === "sqlite" ? "SQLite/Turso" : "PostgreSQL"}** - Database engine`,
); );
} }
if (auth) { if (auth) {
featuresList.push("Authentication - Email & password auth"); featuresList.push(
"- **Authentication** - Email & password authentication with Better Auth",
);
} }
for (const feature of features) { for (const feature of features) {
if (feature === "docker") { if (feature === "docker") {
featuresList.push("Docker - Containerized deployment"); featuresList.push("- **Docker** - Containerized deployment");
} else if (feature === "github-actions") { } else if (feature === "github-actions") {
featuresList.push("GitHub Actions - CI/CD"); featuresList.push("- **GitHub Actions** - CI/CD workflows");
} else if (feature === "SEO") { } else if (feature === "SEO") {
featuresList.push("SEO - Search engine optimization"); featuresList.push("- **SEO** - Search engine optimization tools");
} }
} }
@@ -115,63 +116,90 @@ function generateDatabaseSetup(
orm: string, orm: string,
): string { ): string {
if (database === "none") { if (database === "none") {
return "This project does not include a database."; return "";
} }
if (database === "sqlite") { let setup = "## Database Setup\n\n";
return `This project uses SQLite/Turso for the database.
1. Start the local database: if (database === "sqlite") {
setup += `This project uses SQLite${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
1. Start the local SQLite database:
\`\`\`bash \`\`\`bash
${packageManagerRunCmd} db:local ${packageManagerRunCmd} db:local
\`\`\` \`\`\`
2. Update your \`.env\` file with the connection details. 2. Update your \`.env\` file with the appropriate connection details if needed.
`;
} else if (database === "postgres") {
setup += `This project uses PostgreSQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
${ 1. Make sure you have a PostgreSQL database set up.
auth 2. Update your \`packages/server/.env\` file with your PostgreSQL connection details.
? `3. If using authentication, generate the auth schema: `;
}
if (auth) {
setup += `
3. Generate the authentication schema:
\`\`\`bash \`\`\`bash
${packageManagerRunCmd} auth:generate ${packageManagerRunCmd} auth:generate
\`\`\` \`\`\`
4. Apply the schema to your database: 4. ${
orm === "prisma"
? `Generate the Prisma client and push the schema:
\`\`\`bash \`\`\`bash
${packageManagerRunCmd} ${orm === "drizzle" ? "drizzle:migrate" : "prisma:push"} ${packageManagerRunCmd} prisma:generate
${packageManagerRunCmd} prisma:push
\`\`\`` \`\`\``
: "" : `Apply the Drizzle migrations:
}`; \`\`\`bash
${packageManagerRunCmd} drizzle:migrate
\`\`\``
}
`;
} }
if (database === "postgres") { return setup;
return `This project uses PostgreSQL for the database.
1. Set up your PostgreSQL database.
2. Update your \`.env\` file with the connection details.
${
auth
? `3. If using authentication, generate the auth schema:
\`\`\`bash
${packageManagerRunCmd} auth:generate
\`\`\`
4. Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} ${orm === "drizzle" ? "drizzle:migrate" : "prisma:push"}
\`\`\``
: ""
}`;
}
return "";
} }
function generateScriptsList(packageManagerRunCmd: string): string { function generateScriptsList(
return `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode packageManagerRunCmd: string,
database: string,
orm: string,
auth: boolean,
): string {
let scripts = `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode
- \`${packageManagerRunCmd} build\`: Build both client and server - \`${packageManagerRunCmd} build\`: Build both client and server
- \`${packageManagerRunCmd} dev:client\`: Start only the client - \`${packageManagerRunCmd} dev:client\`: Start only the client
- \`${packageManagerRunCmd} dev:server\`: Start only the server - \`${packageManagerRunCmd} dev:server\`: Start only the server`;
- \`${packageManagerRunCmd} db:local\`: Start the local SQLite database (if applicable)
- \`${packageManagerRunCmd} db:push\`: Push schema changes to the database`; if (database !== "none") {
if (database === "sqlite") {
scripts += `\n- \`${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
}
if (orm === "prisma") {
scripts += `
- \`${packageManagerRunCmd} prisma:generate\`: Generate Prisma client
- \`${packageManagerRunCmd} prisma:push\`: Push schema changes to database
- \`${packageManagerRunCmd} prisma:studio\`: Open Prisma Studio`;
} else if (orm === "drizzle") {
scripts += `
- \`${packageManagerRunCmd} db:generate\`: Generate database schema
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
- \`${packageManagerRunCmd} db:studio\`: Open Drizzle Studio`;
}
}
if (auth) {
scripts += `\n- \`${packageManagerRunCmd} auth:generate\`: Generate authentication schema`;
}
if (auth && database !== "none") {
scripts += `\n- \`${packageManagerRunCmd} db:setup\`: Complete database setup for auth`;
}
return scripts;
} }

View File

@@ -10,35 +10,57 @@ export function displayPostInstallInstructions(
orm?: string, orm?: string,
) { ) {
const runCmd = packageManager === "npm" ? "npm run" : packageManager; const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`;
const steps = [];
if (!depsInstalled) {
steps.push(`${pc.cyan(packageManager)} install`);
}
if (hasAuth && database !== "none") {
steps.push(`${pc.yellow("Authentication Setup:")}`);
steps.push(
`${pc.cyan("1.")} Generate auth schema: ${pc.green(`${runCmd} auth:generate`)}`,
);
if (orm === "prisma") {
steps.push(
`${pc.cyan("2.")} Generate Prisma client: ${pc.green(`${runCmd} prisma:generate`)}`,
);
steps.push(
`${pc.cyan("3.")} Push schema to database: ${pc.green(`${runCmd} prisma:push`)}`,
);
} else if (orm === "drizzle") {
steps.push(
`${pc.cyan("2.")} Apply migrations: ${pc.green(`${runCmd} drizzle:migrate`)}`,
);
}
}
if (database === "postgres") {
steps.push(`${pc.yellow("PostgreSQL Configuration:")}`);
steps.push(
`Make sure to update ${pc.cyan("packages/server/.env")} with your PostgreSQL connection string.`,
);
} else if (database === "sqlite") {
steps.push(`${pc.yellow("Database Configuration:")}`);
steps.push(
`${pc.cyan("packages/server/.env")} contains your SQLite connection details. Update if needed.`,
);
steps.push(
`Start the local SQLite database with: ${pc.green(`${runCmd} db:local`)}`,
);
}
steps.push(`${pc.yellow("Start Development:")}`);
steps.push(`${pc.green(`${runCmd} dev`)}`);
log.info(`${pc.cyan("Installation completed!")} Here are some next steps: log.info(`${pc.cyan("Installation completed!")} Here are some next steps:
${ ${cdCmd}
hasAuth && database !== "none" ${steps.join("\n")}
? `${pc.yellow("Authentication Setup:")}
${pc.cyan("1.")} Generate auth schema: ${pc.green(`cd ${projectName} && ${packageManager} run auth:generate`)}
${
orm === "prisma"
? `${pc.cyan("2.")} Generate Prisma client: ${pc.green(`${packageManager} run prisma:generate`)}
${pc.cyan("3.")} Push schema to database: ${pc.green(`${packageManager} run prisma:push`)}`
: `${pc.cyan("2.")} Apply migrations: ${pc.green(`${packageManager} run drizzle:migrate`)}`
}
` The client application will be available at ${pc.cyan("http://localhost:3001")}
: "" The API server will be running at ${pc.cyan("http://localhost:3000")}`);
}${
database === "postgres"
? `${pc.yellow("PostgreSQL Configuration:")}
Make sure to update ${pc.cyan("packages/server/.env")} with your PostgreSQL connection string.
`
: database === "sqlite"
? `${pc.yellow("Database Configuration:")}
${pc.cyan("packages/server/.env")} contains your SQLite connection details. Update if needed.`
: ""
}
${pc.yellow("Start Development:")}
${pc.cyan("cd")} ${projectName}${!depsInstalled ? `\n${pc.cyan(packageManager)} install` : ""}
${pc.cyan(runCmd)} dev`);
} }

View File

@@ -18,7 +18,7 @@ export async function getDatabaseChoice(
{ {
value: "sqlite", value: "sqlite",
label: "SQLite", label: "SQLite",
hint: "by Turso (recommended)", hint: "by Turso",
}, },
{ {
value: "postgres", value: "postgres",

View File

@@ -15,7 +15,7 @@ export async function getORMChoice(
{ {
value: "drizzle", value: "drizzle",
label: "Drizzle", label: "Drizzle",
hint: "Type-safe, lightweight ORM (recommended)", hint: "Type-safe, lightweight ORM",
}, },
{ {
value: "prisma", value: "prisma",

View File

@@ -37,7 +37,7 @@ export async function getPackageManagerChoice(
{ {
value: "bun", value: "bun",
label: "bun", label: "bun",
hint: "All-in-one JavaScript runtime & toolkit (recommended)", hint: "All-in-one JavaScript runtime & toolkit",
}, },
], ],
initialValue: "bun", initialValue: "bun",

View File

@@ -28,6 +28,33 @@ export default function UserMenu() {
return <Skeleton className="h-9 w-24" />; return <Skeleton className="h-9 w-24" />;
} }
if (!session) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Sign In</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
variant="outline"
className="w-full"
onClick={() => {
navigate({
to: "/sign-in",
});
}}
>
Sign In
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@@ -14,7 +14,7 @@
}, },
"apps/cli": { "apps/cli": {
"name": "create-better-t-stack", "name": "create-better-t-stack",
"version": "0.10.1", "version": "0.11.0",
"bin": { "bin": {
"create-better-t-stack": "dist/index.js", "create-better-t-stack": "dist/index.js",
}, },
@@ -1085,7 +1085,7 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lint-staged": ["lint-staged@15.4.3", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g=="], "lint-staged": ["lint-staged@15.5.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg=="],
"listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="],

View File

@@ -15,7 +15,7 @@
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@changesets/cli": "^2.28.1", "@changesets/cli": "^2.28.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^15.4.3", "lint-staged": "^15.5.0",
"turbo": "^2.4.4", "turbo": "^2.4.4",
"typescript": "5.7.3" "typescript": "5.7.3"
}, },