Implement comprehensive auth template system

This commit is contained in:
Aman Varshney
2025-03-21 17:44:48 +05:30
parent fbdf12fcca
commit 1ca76b23f5
22 changed files with 394 additions and 198 deletions

View File

@@ -18,13 +18,14 @@ export async function setupAuth(
projectDir: string,
enableAuth: boolean,
): Promise<void> {
if (!enableAuth) {
return;
}
const serverDir = path.join(projectDir, "packages/server");
const clientDir = path.join(projectDir, "packages/client");
try {
if (!enableAuth) {
return;
}
addPackageDependency({
dependencies: ["better-auth"],
projectDir: serverDir,

View File

@@ -40,6 +40,65 @@ export async function createProject(options: ProjectConfig): Promise<string> {
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const serverSrcPath = path.join(projectDir, "packages/server/src");
const baseLibPath = path.join(serverSrcPath, "lib");
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
if (options.auth) {
await fs.remove(baseLibPath);
await fs.move(withAuthLibPath, baseLibPath);
if (options.orm === "prisma") {
const schemaPath = path.join(
projectDir,
"packages/server/prisma/schema.prisma",
);
const withAuthSchemaPath = path.join(
projectDir,
"packages/server/prisma/with-auth-schema.prisma",
);
if (await fs.pathExists(withAuthSchemaPath)) {
await fs.remove(schemaPath);
await fs.move(withAuthSchemaPath, schemaPath);
}
} else if (options.orm === "drizzle") {
const schemaPath = path.join(
projectDir,
"packages/server/src/db/schema.ts",
);
const withAuthSchemaPath = path.join(
projectDir,
"packages/server/src/db/with-auth-schema.ts",
);
if (await fs.pathExists(withAuthSchemaPath)) {
await fs.remove(schemaPath);
await fs.move(withAuthSchemaPath, schemaPath);
}
}
} else {
await fs.remove(withAuthLibPath);
if (options.orm === "prisma") {
const withAuthSchema = path.join(
projectDir,
"packages/server/prisma/with-auth-schema.prisma",
);
if (await fs.pathExists(withAuthSchema)) {
await fs.remove(withAuthSchema);
}
} else if (options.orm === "drizzle") {
const withAuthSchema = path.join(
projectDir,
"packages/server/src/db/with-auth-schema.ts",
);
if (await fs.pathExists(withAuthSchema)) {
await fs.remove(withAuthSchema);
}
}
}
}
}
@@ -51,7 +110,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
);
await setupAuth(projectDir, options.auth);
await setupEnvironmentVariables(projectDir, options);
if (options.git) {
@@ -63,7 +121,6 @@ export async function createProject(options: ProjectConfig): Promise<string> {
}
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
displayPostInstallInstructions(

View File

@@ -17,6 +17,10 @@ export async function setupEnvironmentVariables(
envContent = await fs.readFile(envPath, "utf8");
}
if (!envContent.includes("CORS_ORIGIN")) {
envContent += "\nCORS_ORIGIN=http://localhost:3001";
}
if (options.auth) {
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
@@ -25,16 +29,6 @@ export async function setupEnvironmentVariables(
if (!envContent.includes("BETTER_AUTH_URL")) {
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
}
if (!envContent.includes("CORS_ORIGIN")) {
envContent += "\nCORS_ORIGIN=http://localhost:3001";
}
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.database !== "none") {
@@ -54,4 +48,17 @@ export async function setupEnvironmentVariables(
}
await fs.writeFile(envPath, envContent.trim());
const clientEnvPath = path.join(clientDir, ".env");
let clientEnvContent = "";
if (await fs.pathExists(clientEnvPath)) {
clientEnvContent = await fs.readFile(clientEnvPath, "utf8");
}
if (!clientEnvContent.includes("VITE_SERVER_URL")) {
clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n";
}
await fs.writeFile(clientEnvPath, clientEnvContent.trim());
}

View File

@@ -33,32 +33,30 @@ function getDatabaseInstructions(
const instructions = [];
if (orm === "prisma") {
instructions.push(
`${pc.cyan("•")} Apply schema: ${pc.dim(`${runCmd} db:push`)}`,
);
instructions.push(
`${pc.cyan("•")} Database UI: ${pc.dim(`${runCmd} db:studio`)}`,
);
if (database === "sqlite") {
instructions.push(
`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`,
`${pc.dim("Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso")}`,
);
}
} else if (orm === "drizzle") {
instructions.push(
`${pc.cyan("•")} Apply schema: ${pc.dim(`${runCmd} db:push`)}`,
);
instructions.push(
`${pc.cyan("•")} Database UI: ${pc.dim(`${runCmd} db:studio`)}`,
);
} else if (orm === "drizzle") {
if (database === "sqlite") {
instructions.push(
`${pc.cyan("•")} Start local DB: ${pc.dim(`cd packages/server && ${runCmd} db:local`)}`,
);
}
instructions.push(
`${pc.cyan("•")} Apply schema: ${pc.dim(`${runCmd} db:push`)}`,
);
instructions.push(
`${pc.cyan("•")} Database UI: ${pc.dim(`${runCmd} db:studio`)}`,
);
}
return instructions.length

View File

@@ -31,12 +31,6 @@ const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
}),
],
});

View File

@@ -0,0 +1,77 @@
import {
QueryCache,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { httpBatchLink } from "@trpc/client";
import { createTRPCQueryUtils } from "@trpc/react-query";
import ReactDOM from "react-dom/client";
import { toast } from "sonner";
import Loader from "./components/loader";
import { routeTree } from "./routeTree.gen";
import { trpc } from "./utils/trpc";
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
toast.error(error.message, {
action: {
label: "retry",
onClick: () => {
queryClient.invalidateQueries();
},
},
});
},
}),
});
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
}),
],
});
export const trpcQueryUtils = createTRPCQueryUtils({
queryClient,
client: trpcClient,
});
const router = createRouter({
routeTree,
defaultPreload: "intent",
context: { trpcQueryUtils },
defaultPendingComponent: () => <Loader />,
Wrap: function WrapComponent({ children }) {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
},
});
// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootElement = document.getElementById("app")!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />);
}

View File

@@ -1,47 +1,7 @@
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});
export const todo = pgTable("todo", {
id: serial("id").primaryKey(),
text: text("text").notNull(),
completed: boolean("completed").default(false).notNull()
});

View File

@@ -0,0 +1,53 @@
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const todo = pgTable("todo", {
id: serial("id").primaryKey(),
text: text("text").notNull(),
completed: boolean("completed").default(false).notNull()
});
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});

View File

@@ -5,7 +5,7 @@ import * as schema from "../db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
provider: "pg",
schema: schema,
}),
trustedOrigins: [process.env.CORS_ORIGIN!],

View File

@@ -1,55 +1,7 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const session = sqliteTable("session", {
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id),
});
export const account = sqliteTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
export const todo = sqliteTable("todo", {
id: integer("id").primaryKey({ autoIncrement: true }),
text: text("text").notNull(),
completed: integer("completed", { mode: "boolean" }).default(false).notNull()
});

View File

@@ -0,0 +1,61 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const todo = sqliteTable("todo", {
id: integer("id").primaryKey({ autoIncrement: true }),
text: text("text").notNull(),
completed: integer("completed", { mode: "boolean" }).default(false).notNull()
});
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const session = sqliteTable("session", {
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id),
});
export const account = sqliteTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
});

View File

@@ -1,9 +1,3 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
@@ -13,6 +7,14 @@ datasource db {
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}
model User {
id String @id @map("_id")
name String

View File

@@ -0,0 +1,76 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}
model User {
id String @id @map("_id")
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id @map("_id")
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id @map("_id")
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id @map("_id")
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}

View File

@@ -1,74 +1,16 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
url = "file:./local.db"
}
model User {
id String @id @map("_id")
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@unique([email])
@@map("user")
}
model Session {
id String @id @map("_id")
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id @map("_id")
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id @map("_id")
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
@@map("todo")
}

View File

@@ -0,0 +1,16 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./local.db"
}
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}