diff --git a/.changeset/eight-candies-design.md b/.changeset/eight-candies-design.md new file mode 100644 index 0000000..71fcf35 --- /dev/null +++ b/.changeset/eight-candies-design.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +Enhance authentication setup and improve documentation diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 300c39c..fec954f 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -1,7 +1,5 @@ import path from "node:path"; -import { log } from "@clack/prompts"; import fs from "fs-extra"; -import pc from "picocolors"; import type { ProjectAddons } from "../types"; export async function setupAddons(projectDir: string, addons: ProjectAddons[]) { diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index 12c454b..4b00437 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -50,11 +50,58 @@ export async function configureAuth( ); } else { 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)) { - await fs.copy(envExamplePath, envPath); - await fs.remove(envExamplePath); + if (!(await fs.pathExists(envPath))) { + if (await fs.pathExists(templateEnvPath)) { + 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") { @@ -71,6 +118,15 @@ export async function configureAuth( await fs.ensureDir(path.dirname(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") { const drizzleAuthPath = path.join(serverDir, "src/lib/auth.ts"); const defaultDrizzleAuthPath = path.join( @@ -95,3 +151,14 @@ export async function configureAuth( 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; +} diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index f270ef0..9a08569 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -37,27 +37,6 @@ export async function createProject(options: ProjectConfig): Promise { } } - 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 = [ [ path.join(projectDir, "packages/server/_env"), @@ -112,26 +91,31 @@ export async function createProject(options: ProjectConfig): Promise { : "bun@1.2.4"; } - if (options.auth && options.database !== "none") { - packageJson.scripts["auth:generate"] = - "cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts"; + if (options.database !== "none") { + if (options.database === "sqlite") { + packageJson.scripts["db:local"] = + "cd packages/server && turso dev --db-file local.db"; + } - if (options.orm === "prisma") { - packageJson.scripts["prisma:generate"] = - "cd packages/server && npx prisma generate"; - packageJson.scripts["prisma:push"] = - "cd packages/server && npx prisma db push"; - packageJson.scripts["prisma:studio"] = - "cd packages/server && npx prisma studio"; + if (options.auth) { + packageJson.scripts["auth:generate"] = + "cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts"; - 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"; + if (options.orm === "prisma") { + packageJson.scripts["prisma:generate"] = + "cd packages/server && npx prisma generate"; + 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"] = + "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"; + } } } diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index 857de25..3a67a74 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -28,7 +28,7 @@ function generateReadmeContent(options: ProjectConfig): string { 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 @@ -42,6 +42,8 @@ First, install the dependencies: ${packageManager} install \`\`\` +${generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)} + Then, run the development server: \`\`\`bash @@ -51,10 +53,6 @@ ${packageManagerRunCmd} dev 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). -## Database Setup - -${generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)} - ## Project Structure \`\`\` @@ -64,9 +62,9 @@ ${projectName}/ │ └── server/ # Backend API (Hono, tRPC) \`\`\` -## Scripts +## Available Scripts -${generateScriptsList(packageManagerRunCmd)} +${generateScriptsList(packageManagerRunCmd, database, orm, auth)} `; } @@ -77,31 +75,34 @@ function generateFeaturesList( orm: string, ): string { const featuresList = [ - "TypeScript - For type safety", - "TanStack Router - File-based routing", - `${orm === "drizzle" ? "Drizzle" : "Prisma"} - ORM`, - "TailwindCSS - Utility-first CSS", - "shadcn/ui - Reusable components", - "Hono - Lightweight, performant server", + "- **TypeScript** - For type safety and improved developer experience", + "- **TanStack Router** - File-based routing with full type safety", + "- **TailwindCSS** - Utility-first CSS for rapid UI development", + "- **shadcn/ui** - Reusable UI components", + "- **Hono** - Lightweight, performant server framework", + "- **tRPC** - End-to-end type-safe APIs", ]; if (database !== "none") { 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) { - featuresList.push("Authentication - Email & password auth"); + featuresList.push( + "- **Authentication** - Email & password authentication with Better Auth", + ); } for (const feature of features) { if (feature === "docker") { - featuresList.push("Docker - Containerized deployment"); + featuresList.push("- **Docker** - Containerized deployment"); } else if (feature === "github-actions") { - featuresList.push("GitHub Actions - CI/CD"); + featuresList.push("- **GitHub Actions** - CI/CD workflows"); } 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, ): string { if (database === "none") { - return "This project does not include a database."; + return ""; } - if (database === "sqlite") { - return `This project uses SQLite/Turso for the database. + let setup = "## Database Setup\n\n"; -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 ${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"}. -${ - auth - ? `3. If using authentication, generate the auth schema: +1. Make sure you have a PostgreSQL database set up. +2. Update your \`packages/server/.env\` file with your PostgreSQL connection details. +`; + } + + if (auth) { + setup += ` +3. Generate the authentication schema: \`\`\`bash ${packageManagerRunCmd} auth:generate \`\`\` -4. Apply the schema to your database: +4. ${ + orm === "prisma" + ? `Generate the Prisma client and push the schema: \`\`\`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 `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 ""; + return setup; } -function generateScriptsList(packageManagerRunCmd: string): string { - return `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode +function generateScriptsList( + 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} dev:client\`: Start only the client -- \`${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`; +- \`${packageManagerRunCmd} dev:server\`: Start only the server`; + + 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; } diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 86fb144..1b03ed8 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -10,35 +10,57 @@ export function displayPostInstallInstructions( orm?: string, ) { 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: -${ - hasAuth && database !== "none" - ? `${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`)}` -} +${cdCmd} +${steps.join("\n")} -` - : "" -}${ - 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`); +The client application will be available at ${pc.cyan("http://localhost:3001")} +The API server will be running at ${pc.cyan("http://localhost:3000")}`); } diff --git a/apps/cli/src/prompts/database.ts b/apps/cli/src/prompts/database.ts index e45e668..8e5760d 100644 --- a/apps/cli/src/prompts/database.ts +++ b/apps/cli/src/prompts/database.ts @@ -18,7 +18,7 @@ export async function getDatabaseChoice( { value: "sqlite", label: "SQLite", - hint: "by Turso (recommended)", + hint: "by Turso", }, { value: "postgres", diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index 43ec6b4..83211b5 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -15,7 +15,7 @@ export async function getORMChoice( { value: "drizzle", label: "Drizzle", - hint: "Type-safe, lightweight ORM (recommended)", + hint: "Type-safe, lightweight ORM", }, { value: "prisma", diff --git a/apps/cli/src/prompts/package-manager.ts b/apps/cli/src/prompts/package-manager.ts index 38d8fac..f0556fc 100644 --- a/apps/cli/src/prompts/package-manager.ts +++ b/apps/cli/src/prompts/package-manager.ts @@ -37,7 +37,7 @@ export async function getPackageManagerChoice( { value: "bun", label: "bun", - hint: "All-in-one JavaScript runtime & toolkit (recommended)", + hint: "All-in-one JavaScript runtime & toolkit", }, ], initialValue: "bun", diff --git a/apps/cli/template/base/packages/client/.env.example b/apps/cli/template/base/packages/client/_env similarity index 100% rename from apps/cli/template/base/packages/client/.env.example rename to apps/cli/template/base/packages/client/_env diff --git a/apps/cli/template/base/packages/client/src/components/user-menu.tsx b/apps/cli/template/base/packages/client/src/components/user-menu.tsx index 1a611a2..554c966 100644 --- a/apps/cli/template/base/packages/client/src/components/user-menu.tsx +++ b/apps/cli/template/base/packages/client/src/components/user-menu.tsx @@ -28,6 +28,33 @@ export default function UserMenu() { return ; } + if (!session) { + return ( + + + + + + My Account + + + + + + + ); + } + return ( diff --git a/bun.lock b/bun.lock index fa10d88..2210340 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "0.10.1", + "version": "0.11.0", "bin": { "create-better-t-stack": "dist/index.js", }, @@ -1085,7 +1085,7 @@ "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=="], diff --git a/package.json b/package.json index 0e418fe..7b416ed 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@biomejs/biome": "1.9.4", "@changesets/cli": "^2.28.1", "husky": "^9.1.7", - "lint-staged": "^15.4.3", + "lint-staged": "^15.5.0", "turbo": "^2.4.4", "typescript": "5.7.3" },