simplify auth setup

This commit is contained in:
Aman Varshney
2025-03-18 09:52:50 +05:30
parent 8678ec614a
commit ac2f22073b
26 changed files with 303 additions and 216 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
simplify auth setup

View File

@@ -5,7 +5,7 @@ import pc from "picocolors";
import { PKG_ROOT } from "../constants"; import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function configureAuth( export async function setupAuth(
projectDir: string, projectDir: string,
enableAuth: boolean, enableAuth: boolean,
hasDatabase: boolean, hasDatabase: boolean,
@@ -16,177 +16,98 @@ export async function configureAuth(
try { try {
if (!enableAuth) { if (!enableAuth) {
await fs.remove(path.join(clientDir, "src/components/sign-up-form.tsx")); return;
await fs.remove(path.join(clientDir, "src/components/sign-in-form.tsx")); }
await fs.remove(path.join(clientDir, "src/components/auth-forms.tsx"));
await fs.remove(path.join(clientDir, "src/components/user-menu.tsx"));
await fs.remove(path.join(clientDir, "src/lib/auth-client.ts"));
const indexRoutePath = path.join(clientDir, "src/routes/index.tsx"); if (!hasDatabase) {
const indexRouteContent = await fs.readFile(indexRoutePath, "utf8");
const updatedIndexRouteContent = indexRouteContent
.replace(/import AuthForms from "@\/components\/auth-forms";\n/, "")
.replace(/<AuthForms \/>/, "");
await fs.writeFile(indexRoutePath, updatedIndexRouteContent, "utf8");
await fs.remove(path.join(serverDir, "src/lib/auth.ts"));
const indexFilePath = path.join(serverDir, "src/index.ts");
const indexContent = await fs.readFile(indexFilePath, "utf8");
const updatedIndexContent = indexContent
.replace(/import { auth } from "\.\/lib\/auth";\n/, "")
.replace(
/app\.on\(\["POST", "GET"\], "\/api\/auth\/\*\*", \(c\) => auth\.handler\(c\.req\.raw\)\);\n\n/,
"",
);
await fs.writeFile(indexFilePath, updatedIndexContent, "utf8");
const contextFilePath = path.join(serverDir, "src/lib/context.ts");
const contextContent = await fs.readFile(contextFilePath, "utf8");
const updatedContextContent = contextContent
.replace(/import { auth } from "\.\/auth";\n/, "")
.replace(
/const session = await auth\.api\.getSession\({\n\s+headers: hono\.req\.raw\.headers,\n\s+}\);/,
"const session = null;",
);
await fs.writeFile(contextFilePath, updatedContextContent, "utf8");
} else if (!hasDatabase) {
log.warn( log.warn(
pc.yellow( pc.yellow(
"Authentication enabled but no database selected. Auth will not function properly.", "Authentication enabled but no database selected. Auth will not function properly.",
), ),
); );
} else { return;
const envPath = path.join(serverDir, ".env"); }
const templateEnvPath = path.join(
PKG_ROOT,
getOrmTemplatePath(
options.orm,
options.database,
"packages/server/_env",
),
);
if (!(await fs.pathExists(envPath))) { const envPath = path.join(serverDir, ".env");
if (await fs.pathExists(templateEnvPath)) { const templateEnvPath = path.join(
await fs.copy(templateEnvPath, envPath); PKG_ROOT,
} else { getOrmTemplatePath(options.orm, options.database, "packages/server/_env"),
const defaultEnv = `BETTER_AUTH_SECRET=${generateAuthSecret()} );
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 BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:3001 CORS_ORIGIN=http://localhost:3001
${options.database === "sqlite" ? "TURSO_CONNECTION_URL=http://127.0.0.1:8080" : ""} ${options.database === "sqlite" ? "TURSO_CONNECTION_URL=http://127.0.0.1:8080" : ""}
${options.orm === "prisma" ? 'DATABASE_URL="file:./dev.db"' : ""} ${options.orm === "prisma" ? 'DATABASE_URL="file:./dev.db"' : ""}
`; `;
await fs.writeFile(envPath, defaultEnv); await fs.writeFile(envPath, defaultEnv);
} }
} else { } else {
let envContent = await fs.readFile(envPath, "utf8"); let envContent = await fs.readFile(envPath, "utf8");
if (!envContent.includes("BETTER_AUTH_SECRET")) { if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`; 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 (!envContent.includes("BETTER_AUTH_URL")) {
if (!(await fs.pathExists(clientEnvPath))) { envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
const clientEnvContent = "VITE_SERVER_URL=http://localhost:3000\n";
await fs.writeFile(clientEnvPath, clientEnvContent);
} }
if (options.orm === "prisma") { if (!envContent.includes("CORS_ORIGIN")) {
const prismaAuthPath = path.join(serverDir, "src/lib/auth.ts"); envContent += "\nCORS_ORIGIN=http://localhost:3001";
const defaultPrismaAuthPath = path.join( }
PKG_ROOT,
getOrmTemplatePath(
options.orm,
options.database,
"packages/server/src/lib/auth.ts",
),
);
if ( if (
(await fs.pathExists(defaultPrismaAuthPath)) && options.database === "sqlite" &&
!(await fs.pathExists(prismaAuthPath)) !envContent.includes("TURSO_CONNECTION_URL")
) { ) {
await fs.ensureDir(path.dirname(prismaAuthPath)); envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080";
await fs.copy(defaultPrismaAuthPath, prismaAuthPath); }
}
let authContent = await fs.readFile(prismaAuthPath, "utf8"); if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
if (!authContent.includes("trustedOrigins")) { envContent += '\nDATABASE_URL="file:./dev.db"';
authContent = authContent.replace( }
"export const auth = betterAuth({",
"export const auth = betterAuth({\n trustedOrigins: [process.env.CORS_ORIGIN!],",
);
await fs.writeFile(prismaAuthPath, authContent);
}
const packageJsonPath = path.join(projectDir, "package.json"); await fs.writeFile(envPath, envContent);
if (await fs.pathExists(packageJsonPath)) { }
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts["prisma:generate"] = const clientEnvPath = path.join(clientDir, ".env");
"cd packages/server && npx prisma generate"; if (!(await fs.pathExists(clientEnvPath))) {
packageJson.scripts["prisma:push"] = const clientEnvContent = "VITE_SERVER_URL=http://localhost:3000\n";
"cd packages/server && npx prisma db push"; await fs.writeFile(clientEnvPath, clientEnvContent);
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";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); if (options.orm === "prisma") {
} const packageJsonPath = path.join(projectDir, "package.json");
} else if (options.orm === "drizzle") { if (await fs.pathExists(packageJsonPath)) {
const drizzleAuthPath = path.join(serverDir, "src/lib/auth.ts"); const packageJson = await fs.readJson(packageJsonPath);
const defaultDrizzleAuthPath = path.join(
PKG_ROOT,
getOrmTemplatePath(
options.orm,
options.database,
"packages/server/src/lib/auth.ts",
),
);
if ( packageJson.scripts["prisma:generate"] =
(await fs.pathExists(defaultDrizzleAuthPath)) && "cd packages/server && npx prisma generate";
!(await fs.pathExists(drizzleAuthPath)) packageJson.scripts["prisma:push"] =
) { "cd packages/server && npx prisma db push";
await fs.ensureDir(path.dirname(drizzleAuthPath)); packageJson.scripts["prisma:studio"] =
await fs.copy(defaultDrizzleAuthPath, drizzleAuthPath); "cd packages/server && npx prisma studio";
} packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run prisma:generate && npm run prisma:push";
const packageJsonPath = path.join(projectDir, "package.json"); await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
if (await fs.pathExists(packageJsonPath)) { }
const packageJson = await fs.readJson(packageJsonPath); } else if (options.orm === "drizzle") {
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts["db:push"] = packageJson.scripts["db:push"] =
"cd packages/server && npx @better-auth/cli migrate"; "cd packages/server && npx @better-auth/cli migrate";
packageJson.scripts["db:setup"] = packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run db:push"; "npm run auth:generate && npm run db:push";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
} }
} }
} catch (error) { } catch (error) {

View File

@@ -6,7 +6,7 @@ import pc from "picocolors";
import { PKG_ROOT } from "../constants"; import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
import { setupAddons } from "./addons-setup"; import { setupAddons } from "./addons-setup";
import { configureAuth } from "./auth-setup"; import { setupAuth } from "./auth-setup";
import { createReadme } from "./create-readme"; import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup"; import { setupDatabase } from "./db-setup";
import { displayPostInstallInstructions } from "./post-installation"; import { displayPostInstallInstructions } from "./post-installation";
@@ -24,6 +24,13 @@ export async function createProject(options: ProjectConfig): Promise<string> {
} }
await fs.copy(templateDir, projectDir); await fs.copy(templateDir, projectDir);
if (options.auth) {
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
}
}
if (options.orm !== "none" && options.database !== "none") { if (options.orm !== "none" && options.database !== "none") {
const ormTemplateDir = path.join( const ormTemplateDir = path.join(
PKG_ROOT, PKG_ROOT,
@@ -40,6 +47,10 @@ export async function createProject(options: ProjectConfig): Promise<string> {
path.join(projectDir, "packages/server/_env"), path.join(projectDir, "packages/server/_env"),
path.join(projectDir, "packages/server/.env"), path.join(projectDir, "packages/server/.env"),
], ],
[
path.join(projectDir, "packages/client/_env"),
path.join(projectDir, "packages/client/.env"),
],
]; ];
for (const [source, target] of envFiles) { for (const [source, target] of envFiles) {
@@ -58,7 +69,8 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.orm, options.orm,
options.turso ?? options.database === "sqlite", options.turso ?? options.database === "sqlite",
); );
await configureAuth(
await setupAuth(
projectDir, projectDir,
options.auth, options.auth,
options.database !== "none", options.database !== "none",

View File

@@ -1,6 +1,5 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { ModeToggle } from "./mode-toggle"; import { ModeToggle } from "./mode-toggle";
import UserMenu from "./user-menu";
export default function Header() { export default function Header() {
return ( return (
@@ -16,27 +15,9 @@ export default function Header() {
> >
Home Home
</Link> </Link>
<Link
to="/dashboard"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Dashboard
</Link>
<Link
to="/about"
activeProps={{
className: "font-bold",
}}
>
About
</Link>
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<ModeToggle /> <ModeToggle />
<UserMenu />
</div> </div>
</div> </div>
<hr /> <hr />

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({
component: AboutComponent,
});
function AboutComponent() {
return (
<div className="p-2">
<h3>About</h3>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import AuthForms from "@/components/auth-forms";
import { trpc } from "@/utils/trpc"; import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
@@ -13,7 +12,6 @@ function HomeComponent() {
<h3>Welcome Home!</h3> <h3>Welcome Home!</h3>
<Link to="/dashboard">Go to Dashboard</Link> <Link to="/dashboard">Go to Dashboard</Link>
<p>healthCheck: {healthCheck.data}</p> <p>healthCheck: {healthCheck.data}</p>
<AuthForms />
</div> </div>
); );
} }

View File

@@ -4,7 +4,6 @@ import "dotenv/config";
import { Hono } from "hono"; import { Hono } from "hono";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { auth } from "./lib/auth";
import { createContext } from "./lib/context"; import { createContext } from "./lib/context";
import { appRouter } from "./routers/index"; import { appRouter } from "./routers/index";
@@ -17,13 +16,9 @@ app.use(
cors({ cors({
origin: process.env.CORS_ORIGIN!, origin: process.env.CORS_ORIGIN!,
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}), }),
); );
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use( app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({

View File

@@ -1,17 +1,12 @@
import type { Context as HonoContext } from "hono"; import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = { export type CreateContextOptions = {
hono: HonoContext; hono: HonoContext;
}; };
export async function createContext({ hono }: CreateContextOptions) { export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return { return {
session, session: null,
}; };
} }

View File

@@ -6,19 +6,3 @@ export const t = initTRPC.context<Context>().create();
export const router = t.router; export const router = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -1,15 +1,9 @@
import { router, publicProcedure, protectedProcedure } from "../lib/trpc"; import { router, publicProcedure } from "../lib/trpc";
export const appRouter = router({ export const appRouter = router({
healthCheck: publicProcedure.query(() => { healthCheck: publicProcedure.query(() => {
return "OK"; return "OK";
}), }),
privateData: protectedProcedure.query(({ ctx }) => {
return {
message: "This is private",
user: ctx.session.user,
};
}),
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,37 @@
import { Link } from "@tanstack/react-router";
import { ModeToggle } from "./mode-toggle";
import UserMenu from "./user-menu";
export default function Header() {
return (
<div>
<div className="flex flex-row items-center justify-between px-2 py-1">
<div className="flex gap-4 text-lg">
<Link
to="/"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Home
</Link>
<Link
to="/dashboard"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Dashboard
</Link>
</div>
<div className="flex flex-row items-center gap-2">
<ModeToggle />
<UserMenu />
</div>
</div>
<hr />
</div>
);
}

View File

@@ -0,0 +1,19 @@
import AuthForms from "@/components/auth-forms";
import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomeComponent,
});
function HomeComponent() {
const healthCheck = trpc.healthCheck.useQuery();
return (
<div className="p-2">
<h3>Welcome Home!</h3>
<Link to="/dashboard">Go to Dashboard</Link>
<p>healthCheck: {healthCheck.data}</p>
<AuthForms />
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { serve } from "@hono/node-server";
import { trpcServer } from "@hono/trpc-server";
import "dotenv/config";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { auth } from "./lib/auth";
import { createContext } from "./lib/context";
import { appRouter } from "./routers/index";
const app = new Hono();
app.use(logger());
app.use(
"/*",
cors({
origin: process.env.CORS_ORIGIN!,
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use(
"/trpc/*",
trpcServer({
router: appRouter,
createContext: (_opts, hono) => {
return createContext({ hono });
},
}),
);
app.get("/healthCheck", (c) => {
return c.text("OK");
});
const port = 3000;
console.log(`Server is running on http://localhost:${port}`);
serve({
fetch: app.fetch,
port,
});

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,16 @@
import { router, publicProcedure, protectedProcedure } from "../lib/trpc";
export const appRouter = router({
healthCheck: publicProcedure.query(() => {
return "OK";
}),
privateData: protectedProcedure.query(({ ctx }) => {
return {
message: "This is private",
user: ctx.session.user,
};
}),
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,18 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -0,0 +1,18 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -0,0 +1,18 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -0,0 +1,18 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
hono: HonoContext;
};
export async function createContext({ hono }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: hono.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;