mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add clerk auth support with convex (#548)
This commit is contained in:
@@ -13,7 +13,7 @@ export const DEFAULT_CONFIG_BASE = {
|
|||||||
frontend: ["tanstack-router"],
|
frontend: ["tanstack-router"],
|
||||||
database: "sqlite",
|
database: "sqlite",
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
auth: true,
|
auth: "better-auth",
|
||||||
addons: ["turborepo"],
|
addons: ["turborepo"],
|
||||||
examples: [],
|
examples: [],
|
||||||
git: true,
|
git: true,
|
||||||
@@ -43,6 +43,11 @@ export const dependencyVersionMap = {
|
|||||||
"better-auth": "^1.3.7",
|
"better-auth": "^1.3.7",
|
||||||
"@better-auth/expo": "^1.3.7",
|
"@better-auth/expo": "^1.3.7",
|
||||||
|
|
||||||
|
"@clerk/nextjs": "^6.31.5",
|
||||||
|
"@clerk/clerk-react": "^5.45.0",
|
||||||
|
"@clerk/tanstack-react-start": "^0.23.1",
|
||||||
|
"@clerk/clerk-expo": "^2.14.25",
|
||||||
|
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
"drizzle-kit": "^0.31.2",
|
"drizzle-kit": "^0.31.2",
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function addAddonsToProject(
|
|||||||
frontend: detectedConfig.frontend || [],
|
frontend: detectedConfig.frontend || [],
|
||||||
addons: input.addons,
|
addons: input.addons,
|
||||||
examples: detectedConfig.examples || [],
|
examples: detectedConfig.examples || [],
|
||||||
auth: detectedConfig.auth || false,
|
auth: detectedConfig.auth || "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager:
|
packageManager:
|
||||||
input.packageManager || detectedConfig.packageManager || "npm",
|
input.packageManager || detectedConfig.packageManager || "npm",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export async function addDeploymentToProject(
|
|||||||
frontend: detectedConfig.frontend || [],
|
frontend: detectedConfig.frontend || [],
|
||||||
addons: detectedConfig.addons || [],
|
addons: detectedConfig.addons || [],
|
||||||
examples: detectedConfig.examples || [],
|
examples: detectedConfig.examples || [],
|
||||||
auth: detectedConfig.auth || false,
|
auth: detectedConfig.auth || "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager:
|
packageManager:
|
||||||
input.packageManager || detectedConfig.packageManager || "npm",
|
input.packageManager || detectedConfig.packageManager || "npm",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { addPackageDependency } from "../../utils/add-package-deps";
|
|||||||
|
|
||||||
export async function setupAuth(config: ProjectConfig) {
|
export async function setupAuth(config: ProjectConfig) {
|
||||||
const { auth, frontend, backend, projectDir } = config;
|
const { auth, frontend, backend, projectDir } = config;
|
||||||
if (backend === "convex" || !auth) {
|
if (!auth || auth === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,48 @@ export async function setupAuth(config: ProjectConfig) {
|
|||||||
const serverDirExists = await fs.pathExists(serverDir);
|
const serverDirExists = await fs.pathExists(serverDir);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (serverDirExists) {
|
if (backend === "convex") {
|
||||||
|
if (auth === "clerk" && clientDirExists) {
|
||||||
|
const hasNextJs = frontend.includes("next");
|
||||||
|
const hasTanStackStart = frontend.includes("tanstack-start");
|
||||||
|
const hasViteReactOther = frontend.some((f) =>
|
||||||
|
["tanstack-router", "react-router"].includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasNextJs) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@clerk/nextjs"],
|
||||||
|
projectDir: clientDir,
|
||||||
|
});
|
||||||
|
} else if (hasTanStackStart) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@clerk/tanstack-react-start"],
|
||||||
|
projectDir: clientDir,
|
||||||
|
});
|
||||||
|
} else if (hasViteReactOther) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@clerk/clerk-react"],
|
||||||
|
projectDir: clientDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasNativeWind = frontend.includes("native-nativewind");
|
||||||
|
const hasUnistyles = frontend.includes("native-unistyles");
|
||||||
|
if (
|
||||||
|
auth === "clerk" &&
|
||||||
|
nativeDirExists &&
|
||||||
|
(hasNativeWind || hasUnistyles)
|
||||||
|
) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@clerk/clerk-expo"],
|
||||||
|
projectDir: nativeDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverDirExists && auth === "better-auth") {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["better-auth"],
|
dependencies: ["better-auth"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
@@ -40,10 +81,12 @@ export async function setupAuth(config: ProjectConfig) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hasWebFrontend && clientDirExists) {
|
if (hasWebFrontend && clientDirExists) {
|
||||||
await addPackageDependency({
|
if (auth === "better-auth") {
|
||||||
dependencies: ["better-auth"],
|
await addPackageDependency({
|
||||||
projectDir: clientDir,
|
dependencies: ["better-auth"],
|
||||||
});
|
projectDir: clientDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -51,15 +94,17 @@ export async function setupAuth(config: ProjectConfig) {
|
|||||||
frontend.includes("native-unistyles")) &&
|
frontend.includes("native-unistyles")) &&
|
||||||
nativeDirExists
|
nativeDirExists
|
||||||
) {
|
) {
|
||||||
await addPackageDependency({
|
if (auth === "better-auth") {
|
||||||
dependencies: ["better-auth", "@better-auth/expo"],
|
|
||||||
projectDir: nativeDir,
|
|
||||||
});
|
|
||||||
if (serverDirExists) {
|
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@better-auth/expo"],
|
dependencies: ["better-auth", "@better-auth/expo"],
|
||||||
projectDir: serverDir,
|
projectDir: nativeDir,
|
||||||
});
|
});
|
||||||
|
if (serverDirExists) {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@better-auth/expo"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,7 +103,7 @@ export async function createProjectHandler(
|
|||||||
frontend: [],
|
frontend: [],
|
||||||
addons: [],
|
addons: [],
|
||||||
examples: [],
|
examples: [],
|
||||||
auth: false,
|
auth: "none",
|
||||||
git: false,
|
git: false,
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
install: false,
|
install: false,
|
||||||
@@ -154,11 +154,11 @@ export async function createProjectHandler(
|
|||||||
|
|
||||||
if (config.backend === "convex") {
|
if (config.backend === "convex") {
|
||||||
log.info(
|
log.info(
|
||||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
`Due to '--backend convex' flag, the following options have been automatically set: database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo`,
|
||||||
);
|
);
|
||||||
} else if (config.backend === "none") {
|
} else if (config.backend === "none") {
|
||||||
log.info(
|
log.info(
|
||||||
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
"Due to '--backend none', the following options have been automatically set: --auth none, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { writeBtsConfig } from "../../utils/bts-config";
|
|||||||
import { exitWithError } from "../../utils/errors";
|
import { exitWithError } from "../../utils/errors";
|
||||||
import { formatProjectWithBiome } from "../../utils/format-with-biome";
|
import { formatProjectWithBiome } from "../../utils/format-with-biome";
|
||||||
import { setupAddons } from "../addons/addons-setup";
|
import { setupAddons } from "../addons/addons-setup";
|
||||||
import { setupAuth } from "../addons/auth-setup";
|
|
||||||
import { setupExamples } from "../addons/examples-setup";
|
import { setupExamples } from "../addons/examples-setup";
|
||||||
import { setupApi } from "../core/api-setup";
|
import { setupApi } from "../core/api-setup";
|
||||||
import { setupBackendDependencies } from "../core/backend-setup";
|
import { setupBackendDependencies } from "../core/backend-setup";
|
||||||
@@ -13,6 +12,7 @@ import { setupDatabase } from "../core/db-setup";
|
|||||||
import { setupRuntime } from "../core/runtime-setup";
|
import { setupRuntime } from "../core/runtime-setup";
|
||||||
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
||||||
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
||||||
|
import { setupAuth } from "./auth-setup";
|
||||||
import { runConvexCodegen } from "./convex-codegen";
|
import { runConvexCodegen } from "./convex-codegen";
|
||||||
import { createReadme } from "./create-readme";
|
import { createReadme } from "./create-readme";
|
||||||
import { setupEnvironmentVariables } from "./env-setup";
|
import { setupEnvironmentVariables } from "./env-setup";
|
||||||
@@ -46,8 +46,8 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
if (!isConvex) {
|
if (!isConvex) {
|
||||||
await setupDbOrmTemplates(projectDir, options);
|
await setupDbOrmTemplates(projectDir, options);
|
||||||
await setupDockerComposeTemplates(projectDir, options);
|
await setupDockerComposeTemplates(projectDir, options);
|
||||||
await setupAuthTemplate(projectDir, options);
|
|
||||||
}
|
}
|
||||||
|
await setupAuthTemplate(projectDir, options);
|
||||||
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
||||||
await setupExamplesTemplate(projectDir, options);
|
await setupExamplesTemplate(projectDir, options);
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
await setupAddons(options);
|
await setupAddons(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isConvex && options.auth) {
|
if (options.auth && options.auth !== "none") {
|
||||||
await setupAuth(options);
|
await setupAuth(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from "fs-extra";
|
|||||||
import type {
|
import type {
|
||||||
Addons,
|
Addons,
|
||||||
API,
|
API,
|
||||||
|
Auth,
|
||||||
Database,
|
Database,
|
||||||
DatabaseSetup,
|
DatabaseSetup,
|
||||||
Frontend,
|
Frontend,
|
||||||
@@ -98,7 +99,11 @@ This project uses Convex as a backend. You'll need to set up Convex before runni
|
|||||||
${packageManagerRunCmd} dev:setup
|
${packageManagerRunCmd} dev:setup
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Follow the prompts to create a new Convex project and connect it to your application.`
|
Follow the prompts to create a new Convex project and connect it to your application.${
|
||||||
|
auth === "clerk"
|
||||||
|
? " See [Convex + Clerk guide](https://docs.convex.dev/auth/clerk) for auth setup."
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
: generateDatabaseSetup(
|
: generateDatabaseSetup(
|
||||||
database,
|
database,
|
||||||
auth,
|
auth,
|
||||||
@@ -135,6 +140,7 @@ ${generateProjectStructure(
|
|||||||
addons,
|
addons,
|
||||||
isConvex,
|
isConvex,
|
||||||
api,
|
api,
|
||||||
|
auth,
|
||||||
)}
|
)}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
@@ -258,6 +264,7 @@ function generateProjectStructure(
|
|||||||
addons: Addons[],
|
addons: Addons[],
|
||||||
isConvex: boolean,
|
isConvex: boolean,
|
||||||
api: API,
|
api: API,
|
||||||
|
auth: Auth,
|
||||||
): string {
|
): string {
|
||||||
const structure: string[] = [`${projectName}/`, "├── apps/"];
|
const structure: string[] = [`${projectName}/`, "├── apps/"];
|
||||||
|
|
||||||
@@ -317,6 +324,12 @@ function generateProjectStructure(
|
|||||||
structure.push(
|
structure.push(
|
||||||
"│ └── backend/ # Convex backend functions and schema",
|
"│ └── backend/ # Convex backend functions and schema",
|
||||||
);
|
);
|
||||||
|
if (auth === "clerk") {
|
||||||
|
structure.push(
|
||||||
|
"│ ├── convex/ # Convex functions and schema",
|
||||||
|
"│ └── .env.local # Convex environment variables",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (!isBackendNone) {
|
} else if (!isBackendNone) {
|
||||||
const backendName = backend[0].toUpperCase() + backend.slice(1);
|
const backendName = backend[0].toUpperCase() + backend.slice(1);
|
||||||
const apiName = api !== "none" ? api.toUpperCase() : "";
|
const apiName = api !== "none" ? api.toUpperCase() : "";
|
||||||
@@ -329,7 +342,7 @@ function generateProjectStructure(
|
|||||||
|
|
||||||
function generateFeaturesList(
|
function generateFeaturesList(
|
||||||
database: Database,
|
database: Database,
|
||||||
auth: boolean,
|
auth: Auth,
|
||||||
addons: Addons[],
|
addons: Addons[],
|
||||||
orm: ORM,
|
orm: ORM,
|
||||||
runtime: Runtime,
|
runtime: Runtime,
|
||||||
@@ -449,10 +462,9 @@ function generateFeaturesList(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth && !isConvex) {
|
if (auth !== "none") {
|
||||||
addonsList.push(
|
const authLabel = auth === "clerk" ? "Clerk" : "Better-Auth";
|
||||||
"- **Authentication** - Email & password authentication with Better Auth",
|
addonsList.push(`- **Authentication** - ${authLabel}`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const addon of addons) {
|
for (const addon of addons) {
|
||||||
@@ -476,7 +488,7 @@ function generateFeaturesList(
|
|||||||
|
|
||||||
function generateDatabaseSetup(
|
function generateDatabaseSetup(
|
||||||
database: Database,
|
database: Database,
|
||||||
_auth: boolean,
|
_auth: Auth,
|
||||||
packageManagerRunCmd: string,
|
packageManagerRunCmd: string,
|
||||||
orm: ORM,
|
orm: ORM,
|
||||||
dbSetup: DatabaseSetup,
|
dbSetup: DatabaseSetup,
|
||||||
@@ -575,7 +587,7 @@ function generateScriptsList(
|
|||||||
packageManagerRunCmd: string,
|
packageManagerRunCmd: string,
|
||||||
database: Database,
|
database: Database,
|
||||||
orm: ORM,
|
orm: ORM,
|
||||||
_auth: boolean,
|
_auth: Auth,
|
||||||
hasNative: boolean,
|
hasNative: boolean,
|
||||||
addons: Addons[],
|
addons: Addons[],
|
||||||
backend: string,
|
backend: string,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { generateAuthSecret } from "../addons/auth-setup";
|
import { generateAuthSecret } from "./auth-setup";
|
||||||
|
|
||||||
export interface EnvVariable {
|
export interface EnvVariable {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -143,6 +143,42 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|||||||
condition: true,
|
condition: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (backend === "convex" && auth === "clerk") {
|
||||||
|
if (hasNextJs) {
|
||||||
|
clientVars.push(
|
||||||
|
{
|
||||||
|
key: "NEXT_PUBLIC_CLERK_FRONTEND_API_URL",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CLERK_SECRET_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
|
||||||
|
clientVars.push({
|
||||||
|
key: "VITE_CLERK_PUBLISHABLE_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
});
|
||||||
|
if (hasTanStackStart) {
|
||||||
|
clientVars.push({
|
||||||
|
key: "CLERK_SECRET_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +204,14 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|||||||
condition: true,
|
condition: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (backend === "convex" && auth === "clerk") {
|
||||||
|
nativeVars.push({
|
||||||
|
key: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
||||||
|
value: "",
|
||||||
|
condition: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export async function displayPostInstallInstructions(
|
|||||||
const starlightInstructions = addons?.includes("starlight")
|
const starlightInstructions = addons?.includes("starlight")
|
||||||
? getStarlightInstructions(runCmd)
|
? getStarlightInstructions(runCmd)
|
||||||
: "";
|
: "";
|
||||||
|
const clerkInstructions =
|
||||||
|
isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
|
||||||
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
||||||
runCmd,
|
runCmd,
|
||||||
webDeploy,
|
webDeploy,
|
||||||
@@ -119,6 +121,16 @@ export async function displayPostInstallInstructions(
|
|||||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(
|
||||||
" (this will guide you through Convex project setup)",
|
" (this will guide you through Convex project setup)",
|
||||||
)}\n`;
|
)}\n`;
|
||||||
|
|
||||||
|
if (config.auth === "clerk") {
|
||||||
|
output += `${pc.cyan(`${stepCounter++}.`)} ${pc.bold("Clerk Setup:")}\n${pc.dim(
|
||||||
|
" Follow the Convex + Clerk guide to configure authentication:",
|
||||||
|
)}\n${pc.cyan(" ")}${pc.underline("https://docs.convex.dev/auth/clerk")}\n\n`;
|
||||||
|
output += `${pc.cyan(`${stepCounter++}.`)} ${pc.bold("Required Environment Variables:")}\n`;
|
||||||
|
output += `${pc.dim(" •")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n`;
|
||||||
|
output += `${pc.dim(" •")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env\n`;
|
||||||
|
}
|
||||||
|
|
||||||
output += `${pc.cyan(
|
output += `${pc.cyan(
|
||||||
`${stepCounter++}.`,
|
`${stepCounter++}.`,
|
||||||
)} Copy environment variables from\n${pc.white(
|
)} Copy environment variables from\n${pc.white(
|
||||||
@@ -175,6 +187,7 @@ export async function displayPostInstallInstructions(
|
|||||||
if (alchemyDeployInstructions)
|
if (alchemyDeployInstructions)
|
||||||
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
||||||
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
||||||
|
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
|
||||||
|
|
||||||
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
||||||
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
||||||
@@ -403,6 +416,10 @@ function getWranglerDeployInstructions(
|
|||||||
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClerkInstructions(): string {
|
||||||
|
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("1.")} Sign up for Clerk at ${pc.underline("https://clerk.com/sign-up")}\n${pc.cyan("2.")} Create a new application in Clerk Dashboard\n${pc.cyan("3.")} Create a JWT template named ${pc.bold("'convex'")} (exact name required)\n${pc.cyan("4.")} Copy your Clerk Frontend API URL (Issuer URL)\n${pc.cyan("5.")} Set environment variables:\n${pc.dim(" •")} CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.dim(" •")} CLERK_PUBLISHABLE_KEY in apps/*/.env\n${pc.cyan("6.")} Follow the complete guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.yellow("NOTE:")} Use Convex's <Authenticated> components instead of Clerk's <SignedIn>`;
|
||||||
|
}
|
||||||
|
|
||||||
function getAlchemyDeployInstructions(
|
function getAlchemyDeployInstructions(
|
||||||
runCmd?: string,
|
runCmd?: string,
|
||||||
webDeploy?: string,
|
webDeploy?: string,
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export async function setupAuthTemplate(
|
|||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
) {
|
) {
|
||||||
if (context.backend === "convex" || !context.auth) return;
|
if (!context.auth || context.auth === "none") return;
|
||||||
|
|
||||||
const serverAppDir = path.join(projectDir, "apps/server");
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
@@ -382,8 +382,88 @@ export async function setupAuthTemplate(
|
|||||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||||
const hasNative = hasNativeWind || hasUnistyles;
|
const hasNative = hasNativeWind || hasUnistyles;
|
||||||
|
|
||||||
if (serverAppDirExists) {
|
const authProvider = context.auth;
|
||||||
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
|
|
||||||
|
if (context.backend === "convex" && authProvider === "clerk") {
|
||||||
|
const convexBackendDestDir = path.join(projectDir, "packages/backend");
|
||||||
|
const convexClerkBackendSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
"templates/auth/clerk/convex/backend",
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(convexClerkBackendSrc)) {
|
||||||
|
await fs.ensureDir(convexBackendDestDir);
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
convexClerkBackendSrc,
|
||||||
|
convexBackendDestDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webAppDirExists) {
|
||||||
|
const reactFramework = context.frontend.find((f) =>
|
||||||
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||||
|
f,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (reactFramework) {
|
||||||
|
const convexClerkWebSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/clerk/convex/web/react/${reactFramework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(convexClerkWebSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
convexClerkWebSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nativeAppDirExists) {
|
||||||
|
const convexClerkNativeBaseSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
"templates/auth/clerk/convex/native/base",
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(convexClerkNativeBaseSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
convexClerkNativeBaseSrc,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||||
|
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||||
|
let nativeFrameworkPath = "";
|
||||||
|
if (hasNativeWind) nativeFrameworkPath = "nativewind";
|
||||||
|
else if (hasUnistyles) nativeFrameworkPath = "unistyles";
|
||||||
|
if (nativeFrameworkPath) {
|
||||||
|
const convexClerkNativeFrameworkSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/clerk/convex/native/${nativeFrameworkPath}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(convexClerkNativeFrameworkSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
convexClerkNativeFrameworkSrc,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverAppDirExists && context.backend !== "convex") {
|
||||||
|
const authServerBaseSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/${authProvider}/server/base`,
|
||||||
|
);
|
||||||
if (await fs.pathExists(authServerBaseSrc)) {
|
if (await fs.pathExists(authServerBaseSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
"**/*",
|
"**/*",
|
||||||
@@ -391,13 +471,12 @@ export async function setupAuthTemplate(
|
|||||||
serverAppDir,
|
serverAppDir,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.backend === "next") {
|
if (context.backend === "next") {
|
||||||
const authServerNextSrc = path.join(
|
const authServerNextSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
"templates/auth/server/next",
|
`templates/auth/${authProvider}/server/next`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(authServerNextSrc)) {
|
if (await fs.pathExists(authServerNextSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
@@ -406,7 +485,6 @@ export async function setupAuthTemplate(
|
|||||||
serverAppDir,
|
serverAppDir,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,22 +495,21 @@ export async function setupAuthTemplate(
|
|||||||
if (orm === "drizzle") {
|
if (orm === "drizzle") {
|
||||||
authDbSrc = path.join(
|
authDbSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/auth/server/db/drizzle/${db}`,
|
`templates/auth/${authProvider}/server/db/drizzle/${db}`,
|
||||||
);
|
);
|
||||||
} else if (orm === "prisma") {
|
} else if (orm === "prisma") {
|
||||||
authDbSrc = path.join(
|
authDbSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/auth/server/db/prisma/${db}`,
|
`templates/auth/${authProvider}/server/db/prisma/${db}`,
|
||||||
);
|
);
|
||||||
} else if (orm === "mongoose") {
|
} else if (orm === "mongoose") {
|
||||||
authDbSrc = path.join(
|
authDbSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/auth/server/db/mongoose/${db}`,
|
`templates/auth/${authProvider}/server/db/mongoose/${db}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
|
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
|
||||||
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
||||||
} else if (authDbSrc) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,11 +521,10 @@ export async function setupAuthTemplate(
|
|||||||
if (hasReactWeb) {
|
if (hasReactWeb) {
|
||||||
const authWebBaseSrc = path.join(
|
const authWebBaseSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
"templates/auth/web/react/base",
|
`templates/auth/${authProvider}/web/react/base`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(authWebBaseSrc)) {
|
if (await fs.pathExists(authWebBaseSrc)) {
|
||||||
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reactFramework = context.frontend.find((f) =>
|
const reactFramework = context.frontend.find((f) =>
|
||||||
@@ -459,7 +535,7 @@ export async function setupAuthTemplate(
|
|||||||
if (reactFramework) {
|
if (reactFramework) {
|
||||||
const authWebFrameworkSrc = path.join(
|
const authWebFrameworkSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/auth/web/react/${reactFramework}`,
|
`templates/auth/${authProvider}/web/react/${reactFramework}`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(authWebFrameworkSrc)) {
|
if (await fs.pathExists(authWebFrameworkSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
@@ -468,26 +544,31 @@ export async function setupAuthTemplate(
|
|||||||
webAppDir,
|
webAppDir,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasNuxtWeb) {
|
} else if (hasNuxtWeb) {
|
||||||
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
|
const authWebNuxtSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/${authProvider}/web/nuxt`,
|
||||||
|
);
|
||||||
if (await fs.pathExists(authWebNuxtSrc)) {
|
if (await fs.pathExists(authWebNuxtSrc)) {
|
||||||
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} else if (hasSvelteWeb) {
|
} else if (hasSvelteWeb) {
|
||||||
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
|
const authWebSvelteSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/${authProvider}/web/svelte`,
|
||||||
|
);
|
||||||
if (await fs.pathExists(authWebSvelteSrc)) {
|
if (await fs.pathExists(authWebSvelteSrc)) {
|
||||||
await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
|
await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} else if (hasSolidWeb) {
|
} else if (hasSolidWeb) {
|
||||||
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
|
const authWebSolidSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/${authProvider}/web/solid`,
|
||||||
|
);
|
||||||
if (await fs.pathExists(authWebSolidSrc)) {
|
if (await fs.pathExists(authWebSolidSrc)) {
|
||||||
await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
|
await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +576,7 @@ export async function setupAuthTemplate(
|
|||||||
if (hasNative && nativeAppDirExists) {
|
if (hasNative && nativeAppDirExists) {
|
||||||
const authNativeBaseSrc = path.join(
|
const authNativeBaseSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
"templates/auth/native/native-base",
|
`templates/auth/${authProvider}/native/native-base`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(authNativeBaseSrc)) {
|
if (await fs.pathExists(authNativeBaseSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
@@ -516,7 +597,7 @@ export async function setupAuthTemplate(
|
|||||||
if (nativeFrameworkAuthPath) {
|
if (nativeFrameworkAuthPath) {
|
||||||
const authNativeFrameworkSrc = path.join(
|
const authNativeFrameworkSrc = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`templates/auth/native/${nativeFrameworkAuthPath}`,
|
`templates/auth/${authProvider}/native/${nativeFrameworkAuthPath}`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(authNativeFrameworkSrc)) {
|
if (await fs.pathExists(authNativeFrameworkSrc)) {
|
||||||
await processAndCopyFiles(
|
await processAndCopyFiles(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
AddonsSchema,
|
AddonsSchema,
|
||||||
type API,
|
type API,
|
||||||
APISchema,
|
APISchema,
|
||||||
|
AuthSchema,
|
||||||
type Backend,
|
type Backend,
|
||||||
BackendSchema,
|
BackendSchema,
|
||||||
type BetterTStackConfig,
|
type BetterTStackConfig,
|
||||||
@@ -78,7 +79,7 @@ export const router = t.router({
|
|||||||
.describe("Show detailed result information"),
|
.describe("Show detailed result information"),
|
||||||
database: DatabaseSchema.optional(),
|
database: DatabaseSchema.optional(),
|
||||||
orm: ORMSchema.optional(),
|
orm: ORMSchema.optional(),
|
||||||
auth: z.boolean().optional(),
|
auth: AuthSchema.optional(),
|
||||||
frontend: z.array(FrontendSchema).optional(),
|
frontend: z.array(FrontendSchema).optional(),
|
||||||
addons: z.array(AddonsSchema).optional(),
|
addons: z.array(AddonsSchema).optional(),
|
||||||
examples: z.array(ExamplesSchema).optional(),
|
examples: z.array(ExamplesSchema).optional(),
|
||||||
@@ -202,7 +203,7 @@ export function createBtsCli() {
|
|||||||
* backend: "hono",
|
* backend: "hono",
|
||||||
* database: "sqlite",
|
* database: "sqlite",
|
||||||
* orm: "drizzle",
|
* orm: "drizzle",
|
||||||
* auth: true,
|
* auth: "better-auth",
|
||||||
* addons: ["biome", "turborepo"],
|
* addons: ["biome", "turborepo"],
|
||||||
* packageManager: "bun",
|
* packageManager: "bun",
|
||||||
* install: false,
|
* install: false,
|
||||||
|
|||||||
@@ -1,27 +1,56 @@
|
|||||||
import { confirm, isCancel } from "@clack/prompts";
|
import { isCancel, select } from "@clack/prompts";
|
||||||
import { DEFAULT_CONFIG } from "../constants";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
import type { Backend } from "../types";
|
import type { Auth, Backend } from "../types";
|
||||||
import { exitCancelled } from "../utils/errors";
|
import { exitCancelled } from "../utils/errors";
|
||||||
|
|
||||||
export async function getAuthChoice(
|
export async function getAuthChoice(
|
||||||
auth: boolean | undefined,
|
auth: Auth | undefined,
|
||||||
hasDatabase: boolean,
|
hasDatabase: boolean,
|
||||||
backend?: Backend,
|
backend?: Backend,
|
||||||
|
frontend?: string[],
|
||||||
) {
|
) {
|
||||||
|
if (auth !== undefined) return auth;
|
||||||
if (backend === "convex") {
|
if (backend === "convex") {
|
||||||
return false;
|
const unsupportedFrontends = frontend?.filter((f) =>
|
||||||
|
["nuxt", "svelte", "solid"].includes(f),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unsupportedFrontends && unsupportedFrontends.length > 0) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await select({
|
||||||
|
message: "Select authentication provider",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "clerk",
|
||||||
|
label: "Clerk",
|
||||||
|
hint: "More than auth, Complete User Management",
|
||||||
|
},
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
],
|
||||||
|
initialValue: "clerk",
|
||||||
|
});
|
||||||
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
|
return response as Auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasDatabase) return false;
|
if (!hasDatabase) return "none";
|
||||||
|
|
||||||
if (auth !== undefined) return auth;
|
const response = await select({
|
||||||
|
message: "Select authentication provider",
|
||||||
const response = await confirm({
|
options: [
|
||||||
message: "Add authentication with Better-Auth?",
|
{
|
||||||
|
value: "better-auth",
|
||||||
|
label: "Better-Auth",
|
||||||
|
hint: "comprehensive auth framework for TypeScript",
|
||||||
|
},
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
],
|
||||||
initialValue: DEFAULT_CONFIG.auth,
|
initialValue: DEFAULT_CONFIG.auth,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
return response;
|
return response as Auth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { group } from "@clack/prompts";
|
|||||||
import type {
|
import type {
|
||||||
Addons,
|
Addons,
|
||||||
API,
|
API,
|
||||||
|
Auth,
|
||||||
Backend,
|
Backend,
|
||||||
Database,
|
Database,
|
||||||
DatabaseSetup,
|
DatabaseSetup,
|
||||||
@@ -38,7 +39,7 @@ type PromptGroupResults = {
|
|||||||
database: Database;
|
database: Database;
|
||||||
orm: ORM;
|
orm: ORM;
|
||||||
api: API;
|
api: API;
|
||||||
auth: boolean;
|
auth: Auth;
|
||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
dbSetup: DatabaseSetup;
|
dbSetup: DatabaseSetup;
|
||||||
@@ -57,7 +58,8 @@ export async function gatherConfig(
|
|||||||
): Promise<ProjectConfig> {
|
): Promise<ProjectConfig> {
|
||||||
const result = await group<PromptGroupResults>(
|
const result = await group<PromptGroupResults>(
|
||||||
{
|
{
|
||||||
frontend: () => getFrontendChoice(flags.frontend, flags.backend),
|
frontend: () =>
|
||||||
|
getFrontendChoice(flags.frontend, flags.backend, flags.auth),
|
||||||
backend: ({ results }) =>
|
backend: ({ results }) =>
|
||||||
getBackendFrameworkChoice(flags.backend, results.frontend),
|
getBackendFrameworkChoice(flags.backend, results.frontend),
|
||||||
runtime: ({ results }) =>
|
runtime: ({ results }) =>
|
||||||
@@ -75,7 +77,12 @@ export async function gatherConfig(
|
|||||||
api: ({ results }) =>
|
api: ({ results }) =>
|
||||||
getApiChoice(flags.api, results.frontend, results.backend),
|
getApiChoice(flags.api, results.frontend, results.backend),
|
||||||
auth: ({ results }) =>
|
auth: ({ results }) =>
|
||||||
getAuthChoice(flags.auth, results.database !== "none", results.backend),
|
getAuthChoice(
|
||||||
|
flags.auth as import("../types").Auth | undefined,
|
||||||
|
results.database !== "none",
|
||||||
|
results.backend,
|
||||||
|
results.frontend,
|
||||||
|
),
|
||||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||||
examples: ({ results }) =>
|
examples: ({ results }) =>
|
||||||
getExamplesChoice(
|
getExamplesChoice(
|
||||||
@@ -121,7 +128,6 @@ export async function gatherConfig(
|
|||||||
result.database = "none";
|
result.database = "none";
|
||||||
result.orm = "none";
|
result.orm = "none";
|
||||||
result.api = "none";
|
result.api = "none";
|
||||||
result.auth = false;
|
|
||||||
result.dbSetup = "none";
|
result.dbSetup = "none";
|
||||||
result.examples = ["todo"];
|
result.examples = ["todo"];
|
||||||
}
|
}
|
||||||
@@ -131,7 +137,7 @@ export async function gatherConfig(
|
|||||||
result.database = "none";
|
result.database = "none";
|
||||||
result.orm = "none";
|
result.orm = "none";
|
||||||
result.api = "none";
|
result.api = "none";
|
||||||
result.auth = false;
|
result.auth = "none";
|
||||||
result.dbSetup = "none";
|
result.dbSetup = "none";
|
||||||
result.examples = [];
|
result.examples = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { exitCancelled } from "../utils/errors";
|
|||||||
export async function getFrontendChoice(
|
export async function getFrontendChoice(
|
||||||
frontendOptions?: Frontend[],
|
frontendOptions?: Frontend[],
|
||||||
backend?: Backend,
|
backend?: Backend,
|
||||||
|
auth?: string,
|
||||||
): Promise<Frontend[]> {
|
): Promise<Frontend[]> {
|
||||||
if (frontendOptions !== undefined) return frontendOptions;
|
if (frontendOptions !== undefined) return frontendOptions;
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ export async function getFrontendChoice(
|
|||||||
];
|
];
|
||||||
|
|
||||||
const webOptions = allWebOptions.filter((option) =>
|
const webOptions = allWebOptions.filter((option) =>
|
||||||
isFrontendAllowedWithBackend(option.value, backend),
|
isFrontendAllowedWithBackend(option.value, backend, auth),
|
||||||
);
|
);
|
||||||
|
|
||||||
const webFramework = await select<Frontend>({
|
const webFramework = await select<Frontend>({
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ export type DatabaseSetup = z.infer<typeof DatabaseSetupSchema>;
|
|||||||
export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
|
export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
|
||||||
export type API = z.infer<typeof APISchema>;
|
export type API = z.infer<typeof APISchema>;
|
||||||
|
|
||||||
|
export const AuthSchema = z
|
||||||
|
.enum(["better-auth", "clerk", "none"])
|
||||||
|
.describe("Authentication provider");
|
||||||
|
export type Auth = z.infer<typeof AuthSchema>;
|
||||||
|
|
||||||
export const ProjectNameSchema = z
|
export const ProjectNameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Project name cannot be empty")
|
.min(1, "Project name cannot be empty")
|
||||||
@@ -125,7 +130,7 @@ export type CreateInput = {
|
|||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
database?: Database;
|
database?: Database;
|
||||||
orm?: ORM;
|
orm?: ORM;
|
||||||
auth?: boolean;
|
auth?: Auth;
|
||||||
frontend?: Frontend[];
|
frontend?: Frontend[];
|
||||||
addons?: Addons[];
|
addons?: Addons[];
|
||||||
examples?: Examples[];
|
examples?: Examples[];
|
||||||
@@ -167,7 +172,7 @@ export interface ProjectConfig {
|
|||||||
frontend: Frontend[];
|
frontend: Frontend[];
|
||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
auth: boolean;
|
auth: Auth;
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
install: boolean;
|
install: boolean;
|
||||||
@@ -187,7 +192,7 @@ export interface BetterTStackConfig {
|
|||||||
frontend: Frontend[];
|
frontend: Frontend[];
|
||||||
addons: Addons[];
|
addons: Addons[];
|
||||||
examples: Examples[];
|
examples: Examples[];
|
||||||
auth: boolean;
|
auth: Auth;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
dbSetup: DatabaseSetup;
|
dbSetup: DatabaseSetup;
|
||||||
api: API;
|
api: API;
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export function validateWorkersCompatibility(
|
|||||||
|
|
||||||
export function coerceBackendPresets(config: Partial<ProjectConfig>) {
|
export function coerceBackendPresets(config: Partial<ProjectConfig>) {
|
||||||
if (config.backend === "convex") {
|
if (config.backend === "convex") {
|
||||||
config.auth = false;
|
|
||||||
config.database = "none";
|
config.database = "none";
|
||||||
config.orm = "none";
|
config.orm = "none";
|
||||||
config.api = "none";
|
config.api = "none";
|
||||||
@@ -144,7 +143,7 @@ export function coerceBackendPresets(config: Partial<ProjectConfig>) {
|
|||||||
config.examples = ["todo"] as ProjectConfig["examples"];
|
config.examples = ["todo"] as ProjectConfig["examples"];
|
||||||
}
|
}
|
||||||
if (config.backend === "none") {
|
if (config.backend === "none") {
|
||||||
config.auth = false;
|
config.auth = "none" as ProjectConfig["auth"];
|
||||||
config.database = "none";
|
config.database = "none";
|
||||||
config.orm = "none";
|
config.orm = "none";
|
||||||
config.api = "none";
|
config.api = "none";
|
||||||
@@ -161,7 +160,13 @@ export function incompatibleFlagsForBackend(
|
|||||||
): string[] {
|
): string[] {
|
||||||
const list: string[] = [];
|
const list: string[] = [];
|
||||||
if (backend === "convex") {
|
if (backend === "convex") {
|
||||||
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
if (
|
||||||
|
providedFlags.has("auth") &&
|
||||||
|
options.auth &&
|
||||||
|
options.auth !== "none" &&
|
||||||
|
options.auth !== "clerk"
|
||||||
|
)
|
||||||
|
list.push(`--auth ${options.auth}`);
|
||||||
if (providedFlags.has("database") && options.database !== "none")
|
if (providedFlags.has("database") && options.database !== "none")
|
||||||
list.push(`--database ${options.database}`);
|
list.push(`--database ${options.database}`);
|
||||||
if (providedFlags.has("orm") && options.orm !== "none")
|
if (providedFlags.has("orm") && options.orm !== "none")
|
||||||
@@ -174,7 +179,8 @@ export function incompatibleFlagsForBackend(
|
|||||||
list.push(`--db-setup ${options.dbSetup}`);
|
list.push(`--db-setup ${options.dbSetup}`);
|
||||||
}
|
}
|
||||||
if (backend === "none") {
|
if (backend === "none") {
|
||||||
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
if (providedFlags.has("auth") && options.auth && options.auth !== "none")
|
||||||
|
list.push(`--auth ${options.auth}`);
|
||||||
if (providedFlags.has("database") && options.database !== "none")
|
if (providedFlags.has("database") && options.database !== "none")
|
||||||
list.push(`--database ${options.database}`);
|
list.push(`--database ${options.database}`);
|
||||||
if (providedFlags.has("orm") && options.orm !== "none")
|
if (providedFlags.has("orm") && options.orm !== "none")
|
||||||
@@ -210,8 +216,15 @@ export function validateApiFrontendCompatibility(
|
|||||||
export function isFrontendAllowedWithBackend(
|
export function isFrontendAllowedWithBackend(
|
||||||
frontend: Frontend,
|
frontend: Frontend,
|
||||||
backend?: ProjectConfig["backend"],
|
backend?: ProjectConfig["backend"],
|
||||||
|
auth?: string,
|
||||||
) {
|
) {
|
||||||
if (backend === "convex" && frontend === "solid") return false;
|
if (backend === "convex" && frontend === "solid") return false;
|
||||||
|
|
||||||
|
if (auth === "clerk" && backend === "convex") {
|
||||||
|
const incompatibleFrontends = ["nuxt", "svelte", "solid"];
|
||||||
|
if (incompatibleFrontends.includes(frontend)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type {
|
import type {
|
||||||
API,
|
API,
|
||||||
|
Auth,
|
||||||
Backend,
|
Backend,
|
||||||
CLIInput,
|
CLIInput,
|
||||||
Database,
|
Database,
|
||||||
@@ -57,7 +58,7 @@ export function processFlags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.auth !== undefined) {
|
if (options.auth !== undefined) {
|
||||||
config.auth = options.auth;
|
config.auth = options.auth as Auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.git !== undefined) {
|
if (options.git !== undefined) {
|
||||||
|
|||||||
@@ -66,15 +66,21 @@ export function validateDatabaseOrmAuth(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has("auth") && has("database") && cfg.auth && db === "none") {
|
if (
|
||||||
|
has("auth") &&
|
||||||
|
has("database") &&
|
||||||
|
cfg.auth !== "none" &&
|
||||||
|
db === "none" &&
|
||||||
|
cfg.backend !== "convex"
|
||||||
|
) {
|
||||||
exitWithError(
|
exitWithError(
|
||||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
"Authentication requires a database. Please choose a database or set '--auth none'.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.auth && db === "none") {
|
if (cfg.auth !== "none" && db === "none" && cfg.backend !== "convex") {
|
||||||
exitWithError(
|
exitWithError(
|
||||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
"Authentication requires a database. Please choose a database or set '--auth none'.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +184,35 @@ export function validateBackendConstraints(
|
|||||||
): void {
|
): void {
|
||||||
const { backend } = config;
|
const { backend } = config;
|
||||||
|
|
||||||
|
if (config.auth === "clerk" && backend !== "convex") {
|
||||||
|
exitWithError(
|
||||||
|
"Clerk authentication is only supported with the Convex backend. Please use '--backend convex' or choose a different auth provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backend === "convex" && config.auth === "clerk" && config.frontend) {
|
||||||
|
const incompatibleFrontends = config.frontend.filter((f) =>
|
||||||
|
["nuxt", "svelte", "solid"].includes(f),
|
||||||
|
);
|
||||||
|
if (incompatibleFrontends.length > 0) {
|
||||||
|
exitWithError(
|
||||||
|
`Clerk authentication is not compatible with the following frontends: ${incompatibleFrontends.join(
|
||||||
|
", ",
|
||||||
|
)}. Please choose a different frontend or auth provider.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
backend === "convex" &&
|
||||||
|
config.auth === "better-auth" &&
|
||||||
|
providedFlags.has("auth")
|
||||||
|
) {
|
||||||
|
exitWithError(
|
||||||
|
"Better-Auth is not compatible with the Convex backend. Please use '--auth clerk' or '--auth none'.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
providedFlags.has("backend") &&
|
providedFlags.has("backend") &&
|
||||||
backend &&
|
backend &&
|
||||||
|
|||||||
@@ -40,13 +40,7 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.auth !== undefined) {
|
if (config.auth !== undefined) {
|
||||||
const authText =
|
configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
||||||
typeof config.auth === "boolean"
|
|
||||||
? config.auth
|
|
||||||
? "Yes"
|
|
||||||
: "No"
|
|
||||||
: String(config.auth);
|
|
||||||
configDisplay.push(`${pc.blue("Authentication:")} ${authText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons !== undefined) {
|
if (config.addons !== undefined) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
|||||||
flags.push(`--database ${config.database}`);
|
flags.push(`--database ${config.database}`);
|
||||||
flags.push(`--orm ${config.orm}`);
|
flags.push(`--orm ${config.orm}`);
|
||||||
flags.push(`--api ${config.api}`);
|
flags.push(`--api ${config.api}`);
|
||||||
flags.push(config.auth ? "--auth" : "--no-auth");
|
flags.push(`--auth ${config.auth}`);
|
||||||
|
|
||||||
if (config.addons && config.addons.length > 0) {
|
if (config.addons && config.addons.length > 0) {
|
||||||
flags.push(`--addons ${config.addons.join(" ")}`);
|
flags.push(`--addons ${config.addons.join(" ")}`);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Database models are located in `apps/server/src/db/models/`
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Authentication is enabled in this project:
|
Authentication is enabled in this project:
|
||||||
@@ -129,4 +129,4 @@ This project includes a `bts.jsonc` configuration file that stores your Better-T
|
|||||||
- Turborepo handles build caching and parallel execution
|
- Turborepo handles build caching and parallel execution
|
||||||
{{/if}}
|
{{/if}}
|
||||||
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
|
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
|
||||||
create-better-t-stack add` to add more features later
|
create-better-t-stack add` to add more features later
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RPCLink } from "@orpc/client/fetch";
|
|||||||
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
import type { AppRouterClient } from "../../server/src/routers";
|
import type { AppRouterClient } from "../../server/src/routers";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export const link = new RPCLink({
|
export const link = new RPCLink({
|
||||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/rpc`,
|
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/rpc`,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
headers() {
|
headers() {
|
||||||
const headers = new Map<string, string>();
|
const headers = new Map<string, string>();
|
||||||
const cookies = authClient.getCookie();
|
const cookies = authClient.getCookie();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{{#if (eq backend 'next')}}
|
{{#if (eq backend 'next')}}
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext(req: NextRequest) {
|
export async function createContext(req: NextRequest) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@ export async function createContext(req: NextRequest) {
|
|||||||
|
|
||||||
{{else if (eq backend 'hono')}}
|
{{else if (eq backend 'hono')}}
|
||||||
import type { Context as HonoContext } from "hono";
|
import type { Context as HonoContext } from "hono";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export type CreateContextOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: context.req.raw.headers,
|
headers: context.req.raw.headers,
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export async function createContext({ context }: CreateContextOptions) {
|
|||||||
|
|
||||||
{{else if (eq backend 'elysia')}}
|
{{else if (eq backend 'elysia')}}
|
||||||
import type { Context as ElysiaContext } from "elysia";
|
import type { Context as ElysiaContext } from "elysia";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export type CreateContextOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: context.request.headers,
|
headers: context.request.headers,
|
||||||
});
|
});
|
||||||
@@ -70,13 +70,13 @@ export async function createContext({ context }: CreateContextOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{{else if (eq backend 'express')}}
|
{{else if (eq backend 'express')}}
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext(opts: any) {
|
export async function createContext(opts: any) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: fromNodeHeaders(opts.req.headers),
|
headers: fromNodeHeaders(opts.req.headers),
|
||||||
});
|
});
|
||||||
@@ -93,13 +93,13 @@ export async function createContext(opts: any) {
|
|||||||
|
|
||||||
{{else if (eq backend 'fastify')}}
|
{{else if (eq backend 'fastify')}}
|
||||||
import type { IncomingHttpHeaders } from "node:http";
|
import type { IncomingHttpHeaders } from "node:http";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext(req: IncomingHttpHeaders) {
|
export async function createContext(req: IncomingHttpHeaders) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: fromNodeHeaders(req),
|
headers: fromNodeHeaders(req),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const o = os.$context<Context>();
|
|||||||
|
|
||||||
export const publicProcedure = o;
|
export const publicProcedure = o;
|
||||||
|
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const requireAuth = o.middleware(async ({ context, next }) => {
|
const requireAuth = o.middleware(async ({ context, next }) => {
|
||||||
if (!context.session?.user) {
|
if (!context.session?.user) {
|
||||||
throw new ORPCError("UNAUTHORIZED");
|
throw new ORPCError("UNAUTHORIZED");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { createContext } from '@/lib/context'
|
import { createContext } from '@/lib/context'
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { appRouter } from '@/routers'
|
import { appRouter } from '@/routers'
|
||||||
@@ -10,7 +10,7 @@ const handler = new RPCHandler(appRouter)
|
|||||||
async function handleRequest(req: NextRequest) {
|
async function handleRequest(req: NextRequest) {
|
||||||
const { response } = await handler.handle(req, {
|
const { response } = await handler.handle(req, {
|
||||||
prefix: '/rpc',
|
prefix: '/rpc',
|
||||||
context: {{#if auth}}await createContext(req){{else}}{}{{/if}},
|
context: {{#if (eq auth "better-auth")}}await createContext(req){{else}}{}{{/if}},
|
||||||
})
|
})
|
||||||
|
|
||||||
return response ?? new Response('Not found', { status: 404 })
|
return response ?? new Response('Not found', { status: 404 })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default defineNuxtPlugin(() => {
|
|||||||
|
|
||||||
const rpcLink = new RPCLink({
|
const rpcLink = new RPCLink({
|
||||||
url: rpcUrl,
|
url: rpcUrl,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const link = new RPCLink({
|
|||||||
{{else}}
|
{{else}}
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export const link = new RPCLink({
|
export const link = new RPCLink({
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export const link = new RPCLink({
|
export const link = new RPCLink({
|
||||||
url: `${PUBLIC_SERVER_URL}/rpc`,
|
url: `${PUBLIC_SERVER_URL}/rpc`,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
@@ -12,7 +12,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
|||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
headers() {
|
headers() {
|
||||||
const headers = new Map<string, string>();
|
const headers = new Map<string, string>();
|
||||||
const cookies = authClient.getCookie();
|
const cookies = authClient.getCookie();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{{#if (eq backend 'next')}}
|
{{#if (eq backend 'next')}}
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext(req: NextRequest) {
|
export async function createContext(req: NextRequest) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export async function createContext(req: NextRequest) {
|
|||||||
|
|
||||||
{{else if (eq backend 'hono')}}
|
{{else if (eq backend 'hono')}}
|
||||||
import type { Context as HonoContext } from "hono";
|
import type { Context as HonoContext } from "hono";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export type CreateContextOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: context.req.raw.headers,
|
headers: context.req.raw.headers,
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,7 @@ export async function createContext({ context }: CreateContextOptions) {
|
|||||||
|
|
||||||
{{else if (eq backend 'elysia')}}
|
{{else if (eq backend 'elysia')}}
|
||||||
import type { Context as ElysiaContext } from "elysia";
|
import type { Context as ElysiaContext } from "elysia";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export type CreateContextOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: context.request.headers,
|
headers: context.request.headers,
|
||||||
});
|
});
|
||||||
@@ -74,13 +74,13 @@ export async function createContext({ context }: CreateContextOptions) {
|
|||||||
|
|
||||||
{{else if (eq backend 'express')}}
|
{{else if (eq backend 'express')}}
|
||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext(opts: CreateExpressContextOptions) {
|
export async function createContext(opts: CreateExpressContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: fromNodeHeaders(opts.req.headers),
|
headers: fromNodeHeaders(opts.req.headers),
|
||||||
});
|
});
|
||||||
@@ -97,13 +97,13 @@ export async function createContext(opts: CreateExpressContextOptions) {
|
|||||||
|
|
||||||
{{else if (eq backend 'fastify')}}
|
{{else if (eq backend 'fastify')}}
|
||||||
import type { CreateFastifyContextOptions } from "@trpc/server/adapters/fastify";
|
import type { CreateFastifyContextOptions } from "@trpc/server/adapters/fastify";
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
export async function createContext({ req, res }: CreateFastifyContextOptions) {
|
export async function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: fromNodeHeaders(req.headers),
|
headers: fromNodeHeaders(req.headers),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const router = t.router;
|
|||||||
|
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session) {
|
if (!ctx.session) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
|||||||
{{else}}
|
{{else}}
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
@@ -78,7 +78,7 @@ export const trpcClient = createTRPCClient<AppRouter>({
|
|||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||||
{{#if auth}}
|
{{#if (eq auth "better-auth")}}
|
||||||
fetch(url, options) {
|
fetch(url, options) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
// Replace with your own Clerk Issuer URL from your "convex" JWT template
|
||||||
|
// or with `process.env.CLERK_JWT_ISSUER_DOMAIN`
|
||||||
|
// and configure CLERK_JWT_ISSUER_DOMAIN on the Convex Dashboard
|
||||||
|
// See https://docs.convex.dev/auth/clerk#configuring-dev-and-prod-instances
|
||||||
|
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
|
||||||
|
applicationID: "convex",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { query } from "./_generated/server";
|
||||||
|
|
||||||
|
export const get = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
if (identity === null) {
|
||||||
|
return {
|
||||||
|
message: "Not authenticated",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: "This is private",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Redirect, Stack } from "expo-router";
|
||||||
|
import { useAuth } from "@clerk/clerk-expo";
|
||||||
|
|
||||||
|
export default function AuthRoutesLayout() {
|
||||||
|
const { isSignedIn } = useAuth();
|
||||||
|
|
||||||
|
if (isSignedIn) {
|
||||||
|
return <Redirect href={"/"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Stack />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useSignIn } from "@clerk/clerk-expo";
|
||||||
|
import { Link, useRouter } from "expo-router";
|
||||||
|
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { signIn, setActive, isLoaded } = useSignIn();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [emailAddress, setEmailAddress] = React.useState("");
|
||||||
|
const [password, setPassword] = React.useState("");
|
||||||
|
|
||||||
|
// Handle the submission of the sign-in form
|
||||||
|
const onSignInPress = async () => {
|
||||||
|
if (!isLoaded) return;
|
||||||
|
|
||||||
|
// Start the sign-in process using the email and password provided
|
||||||
|
try {
|
||||||
|
const signInAttempt = await signIn.create({
|
||||||
|
identifier: emailAddress,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If sign-in process is complete, set the created session as active
|
||||||
|
// and redirect the user
|
||||||
|
if (signInAttempt.status === "complete") {
|
||||||
|
await setActive({ session: signInAttempt.createdSessionId });
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
// If the status isn't complete, check why. User might need to
|
||||||
|
// complete further steps.
|
||||||
|
console.error(JSON.stringify(signInAttempt, null, 2));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// See https://clerk.com/docs/custom-flows/error-handling
|
||||||
|
// for more info on error handling
|
||||||
|
console.error(JSON.stringify(err, null, 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Sign in</Text>
|
||||||
|
<TextInput
|
||||||
|
autoCapitalize="none"
|
||||||
|
value={emailAddress}
|
||||||
|
placeholder="Enter email"
|
||||||
|
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={password}
|
||||||
|
placeholder="Enter password"
|
||||||
|
secureTextEntry={true}
|
||||||
|
onChangeText={(password) => setPassword(password)}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity onPress={onSignInPress}>
|
||||||
|
<Text>Continue</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style=\{{ display: "flex", flexDirection: "row", gap: 3 }}>
|
||||||
|
<Text>Don't have an account?</Text>
|
||||||
|
<Link href="/sign-up">
|
||||||
|
<Text>Sign up</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||||
|
import { useSignUp } from "@clerk/clerk-expo";
|
||||||
|
import { Link, useRouter } from "expo-router";
|
||||||
|
|
||||||
|
export default function SignUpScreen() {
|
||||||
|
const { isLoaded, signUp, setActive } = useSignUp();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [emailAddress, setEmailAddress] = React.useState("");
|
||||||
|
const [password, setPassword] = React.useState("");
|
||||||
|
const [pendingVerification, setPendingVerification] = React.useState(false);
|
||||||
|
const [code, setCode] = React.useState("");
|
||||||
|
|
||||||
|
// Handle submission of sign-up form
|
||||||
|
const onSignUpPress = async () => {
|
||||||
|
if (!isLoaded) return;
|
||||||
|
|
||||||
|
console.log(emailAddress, password);
|
||||||
|
|
||||||
|
// Start sign-up process using email and password provided
|
||||||
|
try {
|
||||||
|
await signUp.create({
|
||||||
|
emailAddress,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send user an email with verification code
|
||||||
|
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
|
||||||
|
|
||||||
|
// Set 'pendingVerification' to true to display second form
|
||||||
|
// and capture OTP code
|
||||||
|
setPendingVerification(true);
|
||||||
|
} catch (err) {
|
||||||
|
// See https://clerk.com/docs/custom-flows/error-handling
|
||||||
|
// for more info on error handling
|
||||||
|
console.error(JSON.stringify(err, null, 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle submission of verification form
|
||||||
|
const onVerifyPress = async () => {
|
||||||
|
if (!isLoaded) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the code the user provided to attempt verification
|
||||||
|
const signUpAttempt = await signUp.attemptEmailAddressVerification({
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If verification was completed, set the session to active
|
||||||
|
// and redirect the user
|
||||||
|
if (signUpAttempt.status === "complete") {
|
||||||
|
await setActive({ session: signUpAttempt.createdSessionId });
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
// If the status is not complete, check why. User may need to
|
||||||
|
// complete further steps.
|
||||||
|
console.error(JSON.stringify(signUpAttempt, null, 2));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// See https://clerk.com/docs/custom-flows/error-handling
|
||||||
|
// for more info on error handling
|
||||||
|
console.error(JSON.stringify(err, null, 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pendingVerification) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text>Verify your email</Text>
|
||||||
|
<TextInput
|
||||||
|
value={code}
|
||||||
|
placeholder="Enter your verification code"
|
||||||
|
onChangeText={(code) => setCode(code)}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity onPress={onVerifyPress}>
|
||||||
|
<Text>Verify</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Sign up</Text>
|
||||||
|
<TextInput
|
||||||
|
autoCapitalize="none"
|
||||||
|
value={emailAddress}
|
||||||
|
placeholder="Enter email"
|
||||||
|
onChangeText={(email) => setEmailAddress(email)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={password}
|
||||||
|
placeholder="Enter password"
|
||||||
|
secureTextEntry={true}
|
||||||
|
onChangeText={(password) => setPassword(password)}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity onPress={onSignUpPress}>
|
||||||
|
<Text>Continue</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style=\{{ display: "flex", flexDirection: "row", gap: 3 }}>
|
||||||
|
<Text>Already have an account?</Text>
|
||||||
|
<Link href="/sign-in">
|
||||||
|
<Text>Sign in</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useClerk } from "@clerk/clerk-expo";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { Text, TouchableOpacity } from "react-native";
|
||||||
|
|
||||||
|
export const SignOutButton = () => {
|
||||||
|
// Use `useClerk()` to access the `signOut()` function
|
||||||
|
const { signOut } = useClerk();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await signOut();
|
||||||
|
// Redirect to your desired page
|
||||||
|
router.replace("/");
|
||||||
|
} catch (err) {
|
||||||
|
// See https://clerk.com/docs/custom-flows/error-handling
|
||||||
|
// for more info on error handling
|
||||||
|
console.error(JSON.stringify(err, null, 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={handleSignOut}>
|
||||||
|
<Text>Sign out</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||||
|
import { SignInButton, UserButton, useUser } from "@clerk/nextjs";
|
||||||
|
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const user = useUser();
|
||||||
|
const privateData = useQuery(api.privateData.get);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Authenticated>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome {user.user?.fullName}</p>
|
||||||
|
<p>privateData: {privateData?.message}</p>
|
||||||
|
<UserButton />
|
||||||
|
</div>
|
||||||
|
</Authenticated>
|
||||||
|
<Unauthenticated>
|
||||||
|
<SignInButton />
|
||||||
|
</Unauthenticated>
|
||||||
|
<AuthLoading>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</AuthLoading>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { clerkMiddleware } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
|
export default clerkMiddleware();
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Skip Next.js internals and all static files, unless found in search params
|
||||||
|
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||||
|
// Always run for API routes
|
||||||
|
"/(api|trpc)(.*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { SignInButton, UserButton, useUser } from "@clerk/clerk-react";
|
||||||
|
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||||
|
import {
|
||||||
|
Authenticated,
|
||||||
|
AuthLoading,
|
||||||
|
Unauthenticated,
|
||||||
|
useQuery,
|
||||||
|
} from "convex/react";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const privateData = useQuery(api.privateData.get);
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Authenticated>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome {user.user?.fullName}</p>
|
||||||
|
<p>privateData: {privateData?.message}</p>
|
||||||
|
<UserButton />
|
||||||
|
</div>
|
||||||
|
</Authenticated>
|
||||||
|
<Unauthenticated>
|
||||||
|
<SignInButton />
|
||||||
|
</Unauthenticated>
|
||||||
|
<AuthLoading>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</AuthLoading>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user