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 { 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[]) {

View File

@@ -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;
}

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 = [
[
path.join(projectDir, "packages/server/_env"),
@@ -112,26 +91,31 @@ export async function createProject(options: ProjectConfig): Promise<string> {
: "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";
}
}
}

View File

@@ -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;
}

View File

@@ -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")}`);
}

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -28,6 +28,33 @@ export default function UserMenu() {
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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -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=="],

View File

@@ -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"
},