diff --git a/.changeset/red-walls-invent.md b/.changeset/red-walls-invent.md new file mode 100644 index 0000000000..cd00e953c2 --- /dev/null +++ b/.changeset/red-walls-invent.md @@ -0,0 +1,5 @@ +--- +"create-t3-app": minor +--- + +feat: add lucia auth option diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index 22125f30a4..46b42dfa64 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -30,6 +30,8 @@ interface CliFlags { /** @internal Used in CI. */ nextAuth: boolean; /** @internal Used in CI. */ + lucia: boolean; + /** @internal Used in CI. */ appRouter: boolean; } @@ -52,6 +54,7 @@ const defaultOptions: CliResults = { prisma: false, drizzle: false, nextAuth: false, + lucia: false, importAlias: "~/", appRouter: false, }, @@ -100,6 +103,12 @@ export const runCli = async (): Promise => { "Experimental: Boolean value if we should install NextAuth.js. Must be used in conjunction with `--CI`.", (value) => !!value && value !== "false" ) + /** @experimental Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ + .option( + "--lucia [boolean]", + "Experimental: Boolean value if we should install Lucia Auth. Must be used in conjunction with `--CI`.", + (value) => !!value && value !== "false" + ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "--prisma [boolean]", @@ -167,6 +176,7 @@ export const runCli = async (): Promise => { if (cliResults.flags.prisma) cliResults.packages.push("prisma"); if (cliResults.flags.drizzle) cliResults.packages.push("drizzle"); if (cliResults.flags.nextAuth) cliResults.packages.push("nextAuth"); + if (cliResults.flags.lucia) cliResults.packages.push("lucia"); if (cliResults.flags.prisma && cliResults.flags.drizzle) { // We test a matrix of all possible combination of packages in CI. Checking for impossible @@ -176,6 +186,13 @@ export const runCli = async (): Promise => { process.exit(0); } + if (cliResults.flags.nextAuth && cliResults.flags.lucia) { + logger.warn( + "Incompatible combination NextAuth.js + Lucia Auth. Exiting." + ); + process.exit(0); + } + return cliResults; } @@ -237,6 +254,7 @@ export const runCli = async (): Promise => { options: [ { value: "none", label: "None" }, { value: "next-auth", label: "NextAuth.js" }, + { value: "lucia", label: "Lucia Auth" }, // Maybe later // { value: "clerk", label: "Clerk" }, ], @@ -301,6 +319,7 @@ export const runCli = async (): Promise => { if (project.styling) packages.push("tailwind"); if (project.trpc) packages.push("trpc"); if (project.authentication === "next-auth") packages.push("nextAuth"); + if (project.authentication === "lucia") packages.push("lucia"); if (project.database === "prisma") packages.push("prisma"); if (project.database === "drizzle") packages.push("drizzle"); diff --git a/cli/src/helpers/selectBoilerplate.ts b/cli/src/helpers/selectBoilerplate.ts index adee1b0bf9..05fca60df0 100644 --- a/cli/src/helpers/selectBoilerplate.ts +++ b/cli/src/helpers/selectBoilerplate.ts @@ -19,9 +19,9 @@ export const selectAppFile = ({ let appFile = "base.tsx"; if (usingNextAuth && usingTRPC) { - appFile = "with-auth-trpc.tsx"; + appFile = "with-nextauth-trpc.tsx"; } else if (usingNextAuth && !usingTRPC) { - appFile = "with-auth.tsx"; + appFile = "with-nextauth.tsx"; } else if (!usingNextAuth && usingTRPC) { appFile = "with-trpc.tsx"; } @@ -63,13 +63,18 @@ export const selectIndexFile = ({ const usingTRPC = packages.trpc.inUse; const usingTw = packages.tailwind.inUse; - const usingAuth = packages.nextAuth.inUse; + const usingNextAuth = packages.nextAuth.inUse; + const usingLucia = packages.lucia.inUse; let indexFile = "base.tsx"; - if (usingTRPC && usingTw && usingAuth) { - indexFile = "with-auth-trpc-tw.tsx"; - } else if (usingTRPC && !usingTw && usingAuth) { - indexFile = "with-auth-trpc.tsx"; + if (usingTRPC && usingTw && usingNextAuth) { + indexFile = "with-nextauth-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw && usingNextAuth) { + indexFile = "with-nextauth-trpc.tsx"; + } else if (usingTRPC && usingTw && usingLucia) { + indexFile = "with-lucia-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw && usingLucia) { + indexFile = "with-lucia-trpc.tsx"; } else if (usingTRPC && usingTw) { indexFile = "with-trpc-tw.tsx"; } else if (usingTRPC && !usingTw) { @@ -92,13 +97,22 @@ export const selectPageFile = ({ const usingTRPC = packages.trpc.inUse; const usingTw = packages.tailwind.inUse; - const usingAuth = packages.nextAuth.inUse; + const usingLucia = packages.lucia.inUse; + const usingNextAuth = packages.nextAuth.inUse; let indexFile = "base.tsx"; - if (usingTRPC && usingTw && usingAuth) { - indexFile = "with-auth-trpc-tw.tsx"; - } else if (usingTRPC && !usingTw && usingAuth) { - indexFile = "with-auth-trpc.tsx"; + if (usingTRPC && usingTw && usingNextAuth) { + indexFile = "with-nextauth-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw && usingNextAuth) { + indexFile = "with-nextauth-trpc.tsx"; + } else if (usingTRPC && usingTw && usingLucia) { + indexFile = "with-lucia-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw && usingLucia) { + indexFile = "with-lucia-trpc.tsx"; + } else if (!usingTRPC && usingTw && usingLucia) { + indexFile = "with-lucia-tw.tsx"; + } else if (!usingTRPC && !usingTw && usingLucia) { + indexFile = "with-lucia.tsx"; } else if (usingTRPC && usingTw) { indexFile = "with-trpc-tw.tsx"; } else if (usingTRPC && !usingTw) { diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index 7bfff4f9ce..327386e61c 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -8,6 +8,12 @@ export const dependencyVersionMap = { "@next-auth/prisma-adapter": "^1.0.7", "@auth/drizzle-adapter": "^0.3.2", + // Lucia Auth + lucia: "^2.7.1", + "@lucia-auth/adapter-mysql": "^2.1.0", + "@lucia-auth/adapter-prisma": "^3.0.2", + "@lucia-auth/oauth": "^3.3.1", + // Prisma prisma: "^5.1.1", "@prisma/client": "^5.1.1", diff --git a/cli/src/installers/drizzle.ts b/cli/src/installers/drizzle.ts index 1b88e16ff6..97ba20d7d8 100644 --- a/cli/src/installers/drizzle.ts +++ b/cli/src/installers/drizzle.ts @@ -27,13 +27,15 @@ export const drizzleInstaller: Installer = ({ const configFile = path.join(extrasDir, "config/drizzle.config.ts"); const configDest = path.join(projectDir, "drizzle.config.ts"); - const schemaSrc = path.join( - extrasDir, - "src/server/db", - packages?.nextAuth.inUse - ? "drizzle-schema-auth.ts" - : "drizzle-schema-base.ts" - ); + let schemaFile = "drizzle-schema-base.ts"; + if (packages?.nextAuth.inUse) { + schemaFile = "drizzle-schema-nextauth.ts"; + } + if (packages?.lucia.inUse) { + schemaFile = "drizzle-schema-auth.ts"; + } + + const schemaSrc = path.join(extrasDir, "src/server/db", schemaFile); const schemaDest = path.join(projectDir, "src/server/db/schema.ts"); // Replace placeholder table prefix with project name diff --git a/cli/src/installers/envVars.ts b/cli/src/installers/envVars.ts index 9b4e3a4233..1e976738ef 100644 --- a/cli/src/installers/envVars.ts +++ b/cli/src/installers/envVars.ts @@ -5,22 +5,26 @@ import { PKG_ROOT } from "~/consts.js"; import { type Installer } from "~/installers/index.js"; export const envVariablesInstaller: Installer = ({ projectDir, packages }) => { - const usingAuth = packages?.nextAuth.inUse; + const usingNextAuth = packages?.nextAuth.inUse; + const usingLucia = packages?.lucia.inUse; const usingPrisma = packages?.prisma.inUse; const usingDrizzle = packages?.drizzle.inUse; const usingDb = usingPrisma || usingDrizzle; - const envContent = getEnvContent(!!usingAuth, !!usingPrisma, !!usingDrizzle); + const envContent = getEnvContent({ + usingDrizzle: !!usingDrizzle, + usingLucia: !!usingLucia, + usingNextAuth: !!usingNextAuth, + usingPrisma: !!usingPrisma, + }); - const envFile = - usingAuth && usingDb - ? "with-auth-db.mjs" - : usingAuth - ? "with-auth.mjs" - : usingDb - ? "with-db.mjs" - : ""; + let envFile = ""; + if (usingDb) envFile = "with-db.mjs"; + if (usingNextAuth) envFile = "with-nextauth.mjs"; + if (usingNextAuth && usingDb) envFile = "with-nextauth-db.mjs"; + if (usingLucia) envFile = "with-lucia.mjs"; + if (usingLucia && usingDb) envFile = "with-lucia-db.mjs"; if (envFile !== "") { const envSchemaSrc = path.join( @@ -39,11 +43,17 @@ export const envVariablesInstaller: Installer = ({ projectDir, packages }) => { fs.writeFileSync(envExampleDest, exampleEnvContent + envContent, "utf-8"); }; -const getEnvContent = ( - usingAuth: boolean, - usingPrisma: boolean, - usingDrizzle: boolean -) => { +const getEnvContent = ({ + usingDrizzle, + usingLucia, + usingNextAuth, + usingPrisma, +}: { + usingNextAuth: boolean; + usingLucia: boolean; + usingPrisma: boolean; + usingDrizzle: boolean; +}) => { let content = ` # When adding additional environment variables, the schema in "/src/env.mjs" # should be updated accordingly. @@ -67,7 +77,7 @@ DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}' `; } - if (usingAuth) + if (usingNextAuth) content += ` # Next Auth # You can generate a new secret on the command line with: @@ -79,9 +89,18 @@ NEXTAUTH_URL="http://localhost:3000" # Next Auth Discord Provider DISCORD_CLIENT_ID="" DISCORD_CLIENT_SECRET="" +`; + if (usingLucia) + content += ` +# Lucia Auth +AUTH_URL="http://localhost:3000" + +# Lucia Auth Discord Provider +DISCORD_CLIENT_ID="" +DISCORD_CLIENT_SECRET="" `; - if (!usingAuth && !usingPrisma) + if (!usingNextAuth && !usingPrisma && !usingLucia && !usingDrizzle) content += ` # Example: # SERVERVAR="foo" diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index 8ab83c2c1b..3ff6e639cb 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -5,11 +5,13 @@ import { tailwindInstaller } from "~/installers/tailwind.js"; import { trpcInstaller } from "~/installers/trpc.js"; import { type PackageManager } from "~/utils/getUserPkgManager.js"; import { drizzleInstaller } from "./drizzle.js"; +import { luciaInstaller } from "./lucia.js"; // Turning this into a const allows the list to be iterated over for programatically creating prompt options // Should increase extensability in the future export const availablePackages = [ "nextAuth", + "lucia", "prisma", "drizzle", "tailwind", @@ -44,6 +46,10 @@ export const buildPkgInstallerMap = ( inUse: packages.includes("nextAuth"), installer: nextAuthInstaller, }, + lucia: { + inUse: packages.includes("lucia"), + installer: luciaInstaller, + }, prisma: { inUse: packages.includes("prisma"), installer: prismaInstaller, diff --git a/cli/src/installers/lucia.ts b/cli/src/installers/lucia.ts new file mode 100644 index 0000000000..1075f0e791 --- /dev/null +++ b/cli/src/installers/lucia.ts @@ -0,0 +1,91 @@ +import path from "path"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import { type AvailableDependencies } from "~/installers/dependencyVersionMap.js"; +import { type Installer } from "~/installers/index.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; + +export const luciaInstaller: Installer = ({ + projectDir, + packages, + appRouter, +}) => { + const usingPrisma = packages?.prisma.inUse; + const usingDrizzle = packages?.drizzle.inUse; + const usingTrpc = packages?.trpc.inUse; + + const deps: AvailableDependencies[] = ["lucia", "@lucia-auth/oauth"]; + if (usingPrisma) deps.push("@lucia-auth/adapter-prisma"); + if (usingDrizzle) deps.push("@lucia-auth/adapter-mysql"); + + addPackageDependency({ + projectDir, + dependencies: deps, + devMode: false, + }); + + const extrasDir = path.join(PKG_ROOT, "template/extras"); + + const tsDefinitionFile = "lucia-auth.d.ts"; + + const signInFile = appRouter + ? "src/app/api/auth/discord/signin/route.ts" + : "src/pages/api/auth/discord/signin.ts"; + const callbackFile = appRouter + ? "src/app/api/auth/discord/callback/route.ts" + : "src/app/api/auth/discord/callback.ts"; + const logOutFile = appRouter + ? "src/app/api/auth/logout/route.ts" + : "src/pages/api/auth/logout.ts"; + + const signInSrc = path.join(extrasDir, signInFile); + const signInDest = path.join(projectDir, signInFile); + const callbackSrc = path.join(extrasDir, callbackFile); + const callbackDest = path.join(projectDir, callbackFile); + + const tsDefinitionSrc = path.join(extrasDir, tsDefinitionFile); + const tsDefinitionDest = path.join(projectDir, tsDefinitionFile); + + const authConfigSrc = path.join( + extrasDir, + "src/server", + appRouter ? "lucia-app" : "lucia-pages", + usingPrisma + ? "with-prisma.ts" + : usingDrizzle + ? "with-drizzle.ts" + : "base.ts" + ); + const authConfigDest = path.join(projectDir, "src/server/auth.ts"); + + const copySrcDest: [string, string][] = [ + [signInSrc, signInDest], + [callbackSrc, callbackDest], + [tsDefinitionSrc, tsDefinitionDest], + [authConfigSrc, authConfigDest], + ]; + + if (appRouter) { + copySrcDest.push([ + path.join( + extrasDir, + usingTrpc + ? "src/app/_components/logout-button-trpc.tsx" + : "src/app/_components/logout-button.tsx" + ), + path.join(projectDir, "src/app/_components/logout-button.tsx"), + ]); + } + + if (!usingTrpc) { + const logOutSrc = path.join(extrasDir, logOutFile); + const logOutDest = path.join(projectDir, logOutFile); + + copySrcDest.push([logOutSrc, logOutDest]); + } + + copySrcDest.forEach(([src, dest]) => { + fs.copySync(src, dest); + }); +}; diff --git a/cli/src/installers/nextAuth.ts b/cli/src/installers/nextAuth.ts index 3ac489fa71..5bd1732b40 100644 --- a/cli/src/installers/nextAuth.ts +++ b/cli/src/installers/nextAuth.ts @@ -36,7 +36,7 @@ export const nextAuthInstaller: Installer = ({ const authConfigSrc = path.join( extrasDir, "src/server", - appRouter ? "auth-app" : "auth-pages", + appRouter ? "nextauth-app" : "nextauth-pages", usingPrisma ? "with-prisma.ts" : usingDrizzle diff --git a/cli/src/installers/prisma.ts b/cli/src/installers/prisma.ts index 6001b3067d..9c06633eb9 100644 --- a/cli/src/installers/prisma.ts +++ b/cli/src/installers/prisma.ts @@ -20,11 +20,15 @@ export const prismaInstaller: Installer = ({ projectDir, packages }) => { const extrasDir = path.join(PKG_ROOT, "template/extras"); - const schemaSrc = path.join( - extrasDir, - "prisma/schema", - packages?.nextAuth.inUse ? "with-auth.prisma" : "base.prisma" - ); + let schemaFile = "base.prisma"; + if (packages?.nextAuth.inUse) { + schemaFile = "with-nextauth.prisma"; + } + if (packages?.lucia.inUse) { + schemaFile = "with-lucia.prisma"; + } + + const schemaSrc = path.join(extrasDir, "prisma/schema", schemaFile); const schemaDest = path.join(projectDir, "prisma/schema.prisma"); const clientSrc = path.join(extrasDir, "src/server/db/db-prisma.ts"); diff --git a/cli/src/installers/trpc.ts b/cli/src/installers/trpc.ts index 1f15d16d92..ae1a5339fd 100644 --- a/cli/src/installers/trpc.ts +++ b/cli/src/installers/trpc.ts @@ -23,9 +23,11 @@ export const trpcInstaller: Installer = ({ devMode: false, }); - const usingAuth = packages?.nextAuth.inUse; + const usingNextAuth = packages?.nextAuth.inUse; + const usingLucia = packages?.lucia.inUse; const usingPrisma = packages?.prisma.inUse; const usingDrizzle = packages?.drizzle.inUse; + const usingTailwind = packages?.tailwind.inUse; const usingDb = usingPrisma || usingDrizzle; const extrasDir = path.join(PKG_ROOT, "template/extras"); @@ -37,14 +39,13 @@ export const trpcInstaller: Installer = ({ const apiHandlerSrc = path.join(extrasDir, srcToUse); const apiHandlerDest = path.join(projectDir, srcToUse); - const trpcFile = - usingAuth && usingDb - ? "with-auth-db.ts" - : usingAuth - ? "with-auth.ts" - : usingDb - ? "with-db.ts" - : "base.ts"; + let trpcFile = "base.ts"; + if (usingDb) trpcFile = "with-db.ts"; + if (usingNextAuth) trpcFile = "with-nextauth.ts"; + if (usingNextAuth && usingDb) trpcFile = "with-nextauth-db.ts"; + if (usingLucia) trpcFile = "with-lucia.ts"; + if (usingLucia && usingDb) trpcFile = "with-lucia-db.ts"; + const trpcSrc = path.join( extrasDir, "src/server/api", @@ -53,21 +54,24 @@ export const trpcInstaller: Installer = ({ ); const trpcDest = path.join(projectDir, "src/server/api/trpc.ts"); - const rootRouterSrc = path.join(extrasDir, "src/server/api/root.ts"); + const rootRouterSrc = path.join( + extrasDir, + usingLucia + ? "src/server/api/root/with-lucia.ts" + : "src/server/api/root/base.ts" + ); const rootRouterDest = path.join(projectDir, "src/server/api/root.ts"); - const exampleRouterFile = - usingAuth && usingPrisma - ? "with-auth-prisma.ts" - : usingAuth && usingDrizzle - ? "with-auth-drizzle.ts" - : usingAuth - ? "with-auth.ts" - : usingPrisma - ? "with-prisma.ts" - : usingDrizzle - ? "with-drizzle.ts" - : "base.ts"; + let exampleRouterFile = "base.ts"; + if (usingDrizzle) exampleRouterFile = "with-drizzle.ts"; + if (usingPrisma) exampleRouterFile = "with-prisma.ts"; + if (usingNextAuth || usingLucia) exampleRouterFile = "with-auth.ts"; + if (usingNextAuth && usingDrizzle) + exampleRouterFile = "with-nextauth-drizzle.ts"; + if (usingNextAuth && usingPrisma) + exampleRouterFile = "with-nextauth-prisma.ts"; + if (usingLucia && usingDrizzle) exampleRouterFile = "with-lucia-drizzle.ts"; + if (usingLucia && usingPrisma) exampleRouterFile = "with-lucia-prisma.ts"; const exampleRouterSrc = path.join( extrasDir, @@ -86,6 +90,18 @@ export const trpcInstaller: Installer = ({ [exampleRouterSrc, exampleRouterDest], ]; + if (usingLucia) { + const authRouterSrc = path.join( + extrasDir, + "src/server/api/routers/auth.ts" + ); + const authRouterDest = path.join( + projectDir, + "src/server/api/routers/auth.ts" + ); + copySrcDest.push([authRouterSrc, authRouterDest]); + } + if (appRouter) { const trpcDir = path.join(extrasDir, "src/trpc"); copySrcDest.push( @@ -105,7 +121,7 @@ export const trpcInstaller: Installer = ({ path.join( extrasDir, "src/app/_components", - packages?.tailwind.inUse ? "create-post-tw.tsx" : "create-post.tsx" + usingTailwind ? "create-post-tw.tsx" : "create-post.tsx" ), path.join(projectDir, "src/app/_components/create-post.tsx"), ] diff --git a/cli/template/extras/lucia-auth.d.ts b/cli/template/extras/lucia-auth.d.ts new file mode 100644 index 0000000000..dce7d6ff9a --- /dev/null +++ b/cli/template/extras/lucia-auth.d.ts @@ -0,0 +1,12 @@ +/// + +declare namespace Lucia { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + type Auth = import("~/server/auth").Auth; + interface DatabaseUserAttributes { + username: string; + discord_id: string; + } + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface DatabaseSessionAttributes {} +} diff --git a/cli/template/extras/prisma/schema/with-lucia.prisma b/cli/template/extras/prisma/schema/with-lucia.prisma new file mode 100644 index 0000000000..a4b2b020d6 --- /dev/null +++ b/cli/template/extras/prisma/schema/with-lucia.prisma @@ -0,0 +1,52 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + + @@index([name]) +} + +model User { + id String @id @unique + username String + discord_id String + + auth_session Session[] + key Key[] + Post Post[] +} + +model Session { + id String @id @unique + user_id String + active_expires BigInt + idle_expires BigInt + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) + + @@index([user_id]) +} + +model Key { + id String @id @unique + hashed_password String? + user_id String + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) + + @@index([user_id]) +} diff --git a/cli/template/extras/prisma/schema/with-auth.prisma b/cli/template/extras/prisma/schema/with-nextauth.prisma similarity index 100% rename from cli/template/extras/prisma/schema/with-auth.prisma rename to cli/template/extras/prisma/schema/with-nextauth.prisma diff --git a/cli/template/extras/src/app/_components/logout-button-trpc.tsx b/cli/template/extras/src/app/_components/logout-button-trpc.tsx new file mode 100644 index 0000000000..9dcb2a2f1c --- /dev/null +++ b/cli/template/extras/src/app/_components/logout-button-trpc.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { api } from "~/trpc/react"; + +export function LogOutButton({ className }: { className?: string }) { + const router = useRouter(); + const logOut = api.auth.logOut.useMutation({ + onSuccess: () => { + router.refresh(); + }, + }); + + return ( + + ); +} diff --git a/cli/template/extras/src/app/_components/logout-button.tsx b/cli/template/extras/src/app/_components/logout-button.tsx new file mode 100644 index 0000000000..c52e12b686 --- /dev/null +++ b/cli/template/extras/src/app/_components/logout-button.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +export function LogOutButton({ className }: { className?: string }) { + const router = useRouter(); + + const logOut = async () => { + const res = await fetch("/api/auth/logout", { method: "POST" }); + if (res.ok) return router.refresh(); + }; + + return ( + + ); +} diff --git a/cli/template/extras/src/app/api/auth/discord/callback/route.ts b/cli/template/extras/src/app/api/auth/discord/callback/route.ts new file mode 100644 index 0000000000..8f6a55e8ce --- /dev/null +++ b/cli/template/extras/src/app/api/auth/discord/callback/route.ts @@ -0,0 +1,72 @@ +import { OAuthRequestError } from "@lucia-auth/oauth"; +import { cookies, headers } from "next/headers"; +import { type NextRequest } from "next/server"; + +import { auth, discordAuth } from "~/server/auth"; + +export const GET = async (request: NextRequest) => { + const authRequest = auth.handleRequest(request.method, { + headers, + cookies, + }); + const session = await authRequest.validate(); + if (session) { + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } + const cookieStore = cookies(); + const storedState = cookieStore.get("discord_oauth_state")?.value; + const url = new URL(request.url); + const state = url.searchParams.get("state"); + const code = url.searchParams.get("code"); + // validate state + if (!storedState || !state || storedState !== state || !code) { + return new Response(null, { + status: 400, + }); + } + try { + const { getExistingUser, discordUser, createUser } = + await discordAuth.validateCallback(code); + + const getUser = async () => { + const existingUser = await getExistingUser(); + if (existingUser) return existingUser; + const user = await createUser({ + attributes: { + username: discordUser.username, + discord_id: discordUser.id, + }, + }); + return user; + }; + + const user = await getUser(); + const session = await auth.createSession({ + userId: user.userId, + attributes: {}, + }); + authRequest.setSession(session); + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } catch (e) { + if (e instanceof OAuthRequestError) { + // invalid code + return new Response(null, { + status: 400, + }); + } + console.error(e); + return new Response(null, { + status: 500, + }); + } +}; diff --git a/cli/template/extras/src/app/api/auth/discord/signin/route.ts b/cli/template/extras/src/app/api/auth/discord/signin/route.ts new file mode 100644 index 0000000000..6978f84503 --- /dev/null +++ b/cli/template/extras/src/app/api/auth/discord/signin/route.ts @@ -0,0 +1,34 @@ +import * as context from "next/headers"; +import { type NextRequest } from "next/server"; + +import { env } from "~/env.mjs"; +import { auth, discordAuth } from "~/server/auth"; + +// Redirect users to this page to sign in with Discord +export const GET = async (request: NextRequest) => { + const authRequest = auth.handleRequest(request.method, context); + const session = await authRequest.validate(); + if (session) { + // If already signed in, redirect to home page + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } + const [url, state] = await discordAuth.getAuthorizationUrl(); + const cookieStore = context.cookies(); + cookieStore.set("discord_oauth_state", state, { + httpOnly: true, + secure: env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60, + }); + return new Response(null, { + status: 302, + headers: { + Location: url.toString(), + }, + }); +}; diff --git a/cli/template/extras/src/app/api/auth/logout/route.ts b/cli/template/extras/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000000..3c1a6363c5 --- /dev/null +++ b/cli/template/extras/src/app/api/auth/logout/route.ts @@ -0,0 +1,22 @@ +import * as context from "next/headers"; +import { type NextRequest } from "next/server"; + +import { auth } from "~/server/auth"; + +export const POST = async (request: NextRequest) => { + const authRequest = auth.handleRequest(request.method, context); + // check if user is authenticated + const session = await authRequest.validate(); + if (!session) { + return new Response("Unauthorized", { + status: 401, + }); + } + // make sure to invalidate the current session! + await auth.invalidateSession(session.sessionId); + // delete session cookie + authRequest.setSession(null); + return new Response(null, { + status: 200, + }); +}; diff --git a/cli/template/extras/src/app/page/with-lucia-trpc-tw.tsx b/cli/template/extras/src/app/page/with-lucia-trpc-tw.tsx new file mode 100644 index 0000000000..171735e0b3 --- /dev/null +++ b/cli/template/extras/src/app/page/with-lucia-trpc-tw.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { LogOutButton } from "~/app/_components/logout-button"; +import { getPageSession } from "~/server/auth"; +import { api } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + const session = await getPageSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+ +
+

+ {session && Logged in as {session.user.username}} +

+ {session ? ( + + ) : ( + + Sign in + + )} +
+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const session = await getPageSession(); + if (!session) return null; + + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-lucia-trpc.tsx b/cli/template/extras/src/app/page/with-lucia-trpc.tsx new file mode 100644 index 0000000000..ac6176c64d --- /dev/null +++ b/cli/template/extras/src/app/page/with-lucia-trpc.tsx @@ -0,0 +1,90 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { LogOutButton } from "~/app/_components/logout-button"; +import { getPageSession } from "~/server/auth"; +import { api } from "~/trpc/server"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + const session = await getPageSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+ +
+

+ {session && Logged in as {session.user.username}} +

+ {session ? ( + + ) : ( + + Sign in + + )} +
+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const session = await getPageSession(); + if (!session) return null; + + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

+ Your most recent post: {latestPost.name} +

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-lucia-tw.tsx b/cli/template/extras/src/app/page/with-lucia-tw.tsx new file mode 100644 index 0000000000..590af0ab90 --- /dev/null +++ b/cli/template/extras/src/app/page/with-lucia-tw.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; + +import { LogOutButton } from "~/app/_components/logout-button"; +import { getPageSession } from "~/server/auth"; + +export default async function Home() { + const session = await getPageSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+

+ {session && Logged in as {session.user.username}} +

+ {session ? ( + + ) : ( + + Sign in + + )} +
+
+
+
+ ); +} diff --git a/cli/template/extras/src/app/page/with-lucia.tsx b/cli/template/extras/src/app/page/with-lucia.tsx new file mode 100644 index 0000000000..39642853bb --- /dev/null +++ b/cli/template/extras/src/app/page/with-lucia.tsx @@ -0,0 +1,60 @@ +import Link from "next/link"; + +import { LogOutButton } from "~/app/_components/logout-button"; +import { getPageSession } from "~/server/auth"; +import styles from "./index.module.css"; + +export default async function Home() { + const session = await getPageSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+

+ {session && Logged in as {session.user.username}} +

+ {session ? ( + + ) : ( + + Sign in + + )} +
+
+
+
+ ); +} diff --git a/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx b/cli/template/extras/src/app/page/with-nextauth-trpc-tw.tsx similarity index 100% rename from cli/template/extras/src/app/page/with-auth-trpc-tw.tsx rename to cli/template/extras/src/app/page/with-nextauth-trpc-tw.tsx diff --git a/cli/template/extras/src/app/page/with-auth-trpc.tsx b/cli/template/extras/src/app/page/with-nextauth-trpc.tsx similarity index 100% rename from cli/template/extras/src/app/page/with-auth-trpc.tsx rename to cli/template/extras/src/app/page/with-nextauth-trpc.tsx diff --git a/cli/template/extras/src/env/with-lucia-db.mjs b/cli/template/extras/src/env/with-lucia-db.mjs new file mode 100644 index 0000000000..0328a7d5a1 --- /dev/null +++ b/cli/template/extras/src/env/with-lucia-db.mjs @@ -0,0 +1,58 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + /** + * Specify your server-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. + */ + server: { + DATABASE_URL: z + .string() + .url() + .refine( + (str) => !str.includes("YOUR_MYSQL_URL_HERE"), + "You forgot to change the default URL" + ), + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + DISCORD_CLIENT_ID: z.string(), + DISCORD_CLIENT_SECRET: z.string(), + AUTH_URL: z.string(), + }, + + /** + * Specify your client-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. To expose them to the client, prefix them with + * `NEXT_PUBLIC_`. + */ + client: { + // NEXT_PUBLIC_CLIENTVAR: z.string(), + }, + + /** + * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. + * middlewares) or client-side so we need to destruct manually. + */ + runtimeEnv: { + DATABASE_URL: process.env.DATABASE_URL, + NODE_ENV: process.env.NODE_ENV, + DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, + DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, + AUTH_URL: process.env.VERCEL_URL // Use Vercel URL, if deployed on Vercel + ? `https://${process.env.VERCEL_URL}` + : process.env.AUTH_URL, + // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + }, + /** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially + * useful for Docker builds. + */ + skipValidation: !!process.env.SKIP_ENV_VALIDATION, + /** + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. + */ + emptyStringAsUndefined: true, +}); diff --git a/cli/template/extras/src/env/with-lucia.mjs b/cli/template/extras/src/env/with-lucia.mjs new file mode 100644 index 0000000000..fcada8c7cb --- /dev/null +++ b/cli/template/extras/src/env/with-lucia.mjs @@ -0,0 +1,50 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + /** + * Specify your server-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. + */ + server: { + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + DISCORD_CLIENT_ID: z.string(), + DISCORD_CLIENT_SECRET: z.string(), + AUTH_URL: z.string(), + }, + + /** + * Specify your client-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. To expose them to the client, prefix them with + * `NEXT_PUBLIC_`. + */ + client: { + // NEXT_PUBLIC_CLIENTVAR: z.string(), + }, + + /** + * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. + * middlewares) or client-side so we need to destruct manually. + */ + runtimeEnv: { + NODE_ENV: process.env.NODE_ENV, + DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, + DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, + AUTH_URL: process.env.VERCEL_URL // Use Vercel URL, if deployed on Vercel + ? `https://${process.env.VERCEL_URL}` + : process.env.AUTH_URL, + // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + }, + /** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially + * useful for Docker builds. + */ + skipValidation: !!process.env.SKIP_ENV_VALIDATION, + /** + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. + */ + emptyStringAsUndefined: true, +}); diff --git a/cli/template/extras/src/env/with-auth-db.mjs b/cli/template/extras/src/env/with-nextauth-db.mjs similarity index 94% rename from cli/template/extras/src/env/with-auth-db.mjs rename to cli/template/extras/src/env/with-nextauth-db.mjs index afde07b1ea..69b9b78410 100644 --- a/cli/template/extras/src/env/with-auth-db.mjs +++ b/cli/template/extras/src/env/with-nextauth-db.mjs @@ -60,8 +60,8 @@ export const env = createEnv({ */ skipValidation: !!process.env.SKIP_ENV_VALIDATION, /** - * Makes it so that empty strings are treated as undefined. - * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. */ emptyStringAsUndefined: true, }); diff --git a/cli/template/extras/src/env/with-auth.mjs b/cli/template/extras/src/env/with-nextauth.mjs similarity index 91% rename from cli/template/extras/src/env/with-auth.mjs rename to cli/template/extras/src/env/with-nextauth.mjs index d9e9b7cdcf..fc3ede798f 100644 --- a/cli/template/extras/src/env/with-auth.mjs +++ b/cli/template/extras/src/env/with-nextauth.mjs @@ -48,13 +48,13 @@ export const env = createEnv({ // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. - * This is especially useful for Docker builds. + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially + * useful for Docker builds. */ skipValidation: !!process.env.SKIP_ENV_VALIDATION, /** - * Makes it so that empty strings are treated as undefined. - * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. */ emptyStringAsUndefined: true, }); diff --git a/cli/template/extras/src/pages/_app/with-auth-trpc.tsx b/cli/template/extras/src/pages/_app/with-nextauth-trpc.tsx similarity index 100% rename from cli/template/extras/src/pages/_app/with-auth-trpc.tsx rename to cli/template/extras/src/pages/_app/with-nextauth-trpc.tsx diff --git a/cli/template/extras/src/pages/_app/with-auth.tsx b/cli/template/extras/src/pages/_app/with-nextauth.tsx similarity index 100% rename from cli/template/extras/src/pages/_app/with-auth.tsx rename to cli/template/extras/src/pages/_app/with-nextauth.tsx diff --git a/cli/template/extras/src/pages/api/auth/discord/callback.ts b/cli/template/extras/src/pages/api/auth/discord/callback.ts new file mode 100644 index 0000000000..1d5c2cd501 --- /dev/null +++ b/cli/template/extras/src/pages/api/auth/discord/callback.ts @@ -0,0 +1,60 @@ +import { OAuthRequestError } from "@lucia-auth/oauth"; +import { parseCookie } from "lucia/utils"; +import { type NextApiRequest, type NextApiResponse } from "next"; + +import { auth, discordAuth } from "~/server/auth"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== "GET") return res.status(405); + const authRequest = auth.handleRequest({ req, res }); + const session = await authRequest.validate(); + if (session) { + return res.status(302).setHeader("Location", "/").end(); + } + const cookies = parseCookie(req.headers.cookie ?? ""); + const storedState = cookies.discord_oauth_state; + const state = req.query.state; + const code = req.query.code; + // validate state + if ( + !storedState || + !state || + storedState !== state || + typeof code !== "string" + ) { + return res.status(400).end(); + } + try { + const { getExistingUser, discordUser, createUser } = + await discordAuth.validateCallback(code); + + const getUser = async () => { + const existingUser = await getExistingUser(); + if (existingUser) return existingUser; + const user = await createUser({ + attributes: { + discord_id: discordUser.id, + username: discordUser.username, + }, + }); + return user; + }; + + const user = await getUser(); + const session = await auth.createSession({ + userId: user.userId, + attributes: {}, + }); + authRequest.setSession(session); + return res.status(302).setHeader("Location", "/").end(); + } catch (e) { + if (e instanceof OAuthRequestError) { + // invalid code + return res.status(400).end(); + } + console.error(e); + return res.status(500).end(); + } +}; + +export default handler; diff --git a/cli/template/extras/src/pages/api/auth/discord/signin.ts b/cli/template/extras/src/pages/api/auth/discord/signin.ts new file mode 100644 index 0000000000..d267190aa8 --- /dev/null +++ b/cli/template/extras/src/pages/api/auth/discord/signin.ts @@ -0,0 +1,29 @@ +import { serializeCookie } from "lucia/utils"; +import { type NextApiRequest, type NextApiResponse } from "next"; + +import { auth, discordAuth } from "~/server/auth"; + +// Redirect users to this page to sign in with Discord +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== "GET") return res.status(405); + const authRequest = auth.handleRequest({ req, res }); + const session = await authRequest.validate(); + if (session) { + // If already signed in, redirect to home page + return res.status(302).setHeader("Location", "/").end(); + } + const [url, state] = await discordAuth.getAuthorizationUrl(); + const stateCookie = serializeCookie("discord_oauth_state", state, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60, + }); + return res + .status(302) + .setHeader("Set-Cookie", stateCookie) + .setHeader("Location", url.toString()) + .end(); +}; + +export default handler; diff --git a/cli/template/extras/src/pages/api/auth/logout.ts b/cli/template/extras/src/pages/api/auth/logout.ts new file mode 100644 index 0000000000..95dd865195 --- /dev/null +++ b/cli/template/extras/src/pages/api/auth/logout.ts @@ -0,0 +1,20 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; + +import { auth } from "~/auth/lucia"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== "POST") return res.status(405); + const authRequest = auth.handleRequest({ req, res }); + // check if user is authenticated + const session = await authRequest.validate(); + if (!session) { + return res.status(401).send("Unauthorized"); + } + // make sure to invalidate the current session! + await auth.invalidateSession(session.sessionId); + // delete session cookie + authRequest.setSession(null); + return res.end(); +}; + +export default handler; diff --git a/cli/template/extras/src/pages/index/with-lucia-trpc-tw.tsx b/cli/template/extras/src/pages/index/with-lucia-trpc-tw.tsx new file mode 100644 index 0000000000..9c6b0d659b --- /dev/null +++ b/cli/template/extras/src/pages/index/with-lucia-trpc-tw.tsx @@ -0,0 +1,94 @@ +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+ +
+
+
+ + ); +} + +function AuthShowcase() { + const { data: user } = api.auth.getCurrentUser.useQuery(); + const utils = api.useContext(); + const logOut = api.auth.logOut.useMutation({ + onSuccess: () => { + utils.auth.getCurrentUser.invalidate(); + }, + }); + + const { data: secretMessage } = api.post.getSecretMessage.useQuery( + undefined, // no input + { enabled: !!user } + ); + + return ( +
+

+ {user && Logged in as {user.username}} + {secretMessage && - {secretMessage}} +

+ {user ? ( + + ) : ( + + Sign in + + )} +
+ ); +} diff --git a/cli/template/extras/src/pages/index/with-lucia-trpc.tsx b/cli/template/extras/src/pages/index/with-lucia-trpc.tsx new file mode 100644 index 0000000000..932e7a272e --- /dev/null +++ b/cli/template/extras/src/pages/index/with-lucia-trpc.tsx @@ -0,0 +1,89 @@ +import Head from "next/head"; +import Link from "next/link"; + +import { api } from "~/utils/api"; +import styles from "./index.module.css"; + +export default function Home() { + const hello = api.post.hello.useQuery({ text: "from tRPC" }); + + return ( + <> + + Create T3 App + + + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello.data ? hello.data.greeting : "Loading tRPC query..."} +

+ +
+
+
+ + ); +} + +function AuthShowcase() { + const { data: user } = api.auth.getCurrentUser.useQuery(); + const utils = api.useContext(); + const logOut = api.auth.logOut.useMutation({ + onSuccess: () => { + utils.auth.getCurrentUser.invalidate(); + }, + }); + + const { data: secretMessage } = api.post.getSecretMessage.useQuery( + undefined, // no input + { enabled: !!user } + ); + + return ( +
+

+ {user && Logged in as {user.username}} + {secretMessage && - {secretMessage}} +

+ {user ? ( + + ) : ( + + Sign in + + )} +
+ ); +} diff --git a/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx b/cli/template/extras/src/pages/index/with-nextauth-trpc-tw.tsx similarity index 100% rename from cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx rename to cli/template/extras/src/pages/index/with-nextauth-trpc-tw.tsx diff --git a/cli/template/extras/src/pages/index/with-auth-trpc.tsx b/cli/template/extras/src/pages/index/with-nextauth-trpc.tsx similarity index 100% rename from cli/template/extras/src/pages/index/with-auth-trpc.tsx rename to cli/template/extras/src/pages/index/with-nextauth-trpc.tsx diff --git a/cli/template/extras/src/server/api/root.ts b/cli/template/extras/src/server/api/root/base.ts similarity index 100% rename from cli/template/extras/src/server/api/root.ts rename to cli/template/extras/src/server/api/root/base.ts diff --git a/cli/template/extras/src/server/api/root/with-lucia.ts b/cli/template/extras/src/server/api/root/with-lucia.ts new file mode 100644 index 0000000000..db02041bcf --- /dev/null +++ b/cli/template/extras/src/server/api/root/with-lucia.ts @@ -0,0 +1,16 @@ +import { authRouter } from "~/server/api/routers/auth"; +import { postRouter } from "~/server/api/routers/post"; +import { createTRPCRouter } from "~/server/api/trpc"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const appRouter = createTRPCRouter({ + post: postRouter, + auth: authRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; diff --git a/cli/template/extras/src/server/api/routers/auth.ts b/cli/template/extras/src/server/api/routers/auth.ts new file mode 100644 index 0000000000..504444ff4b --- /dev/null +++ b/cli/template/extras/src/server/api/routers/auth.ts @@ -0,0 +1,15 @@ +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { auth } from "~/server/auth"; + +export const authRouter = createTRPCRouter({ + getCurrentUser: publicProcedure.query(({ ctx }) => ctx.session?.user ?? null), + + logOut: protectedProcedure.mutation(async ({ ctx }) => { + await auth.invalidateSession(ctx.session?.sessionId); + ctx.authRequest.setSession(null); + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-lucia-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-lucia-drizzle.ts new file mode 100644 index 0000000000..87932239a6 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-lucia-drizzle.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await ctx.db.insert(posts).values({ + name: input.name, + createdById: ctx.session.user.userId, + }); + }), + + getLatest: publicProcedure.query(({ ctx }) => { + return ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-lucia-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-lucia-prisma.ts new file mode 100644 index 0000000000..8d00cffd57 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-lucia-prisma.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return ctx.db.post.create({ + data: { + name: input.name, + createdBy: { connect: { id: ctx.session.user.userId } }, + }, + }); + }), + + getLatest: protectedProcedure.query(({ ctx }) => { + return ctx.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + where: { createdBy: { id: ctx.session.user.userId } }, + }); + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-nextauth-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts rename to cli/template/extras/src/server/api/routers/post/with-nextauth-drizzle.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-nextauth-prisma.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts rename to cli/template/extras/src/server/api/routers/post/with-nextauth-prisma.ts diff --git a/cli/template/extras/src/server/api/trpc-app/with-lucia-db.ts b/cli/template/extras/src/server/api/trpc-app/with-lucia-db.ts new file mode 100644 index 0000000000..06944e0fb4 --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/with-lucia-db.ts @@ -0,0 +1,138 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC, TRPCError } from "@trpc/server"; +import { type Session } from "lucia"; +import * as context from "next/headers"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; + session: Session | null; + authRequest: ReturnType; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + session: opts.session, + authRequest: opts.authRequest, + headers: opts.headers, + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + const authRequest = auth.handleRequest(opts.req.method, context); + const session = await authRequest.validate(); + + return createInnerTRPCContext({ + authRequest, + session, + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-app/with-lucia.ts b/cli/template/extras/src/server/api/trpc-app/with-lucia.ts new file mode 100644 index 0000000000..e08bef5dfe --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/with-lucia.ts @@ -0,0 +1,136 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC, TRPCError } from "@trpc/server"; +import { type Session } from "lucia"; +import * as context from "next/headers"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { auth } from "~/server/auth"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; + session: Session | null; + authRequest: ReturnType; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + session: opts.session, + authRequest: opts.authRequest, + headers: opts.headers, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + const authRequest = auth.handleRequest(opts.req.method, context); + const session = await authRequest.validate(); + + return createInnerTRPCContext({ + authRequest, + session, + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-app/with-nextauth-db.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc-app/with-auth-db.ts rename to cli/template/extras/src/server/api/trpc-app/with-nextauth-db.ts diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth.ts b/cli/template/extras/src/server/api/trpc-app/with-nextauth.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc-app/with-auth.ts rename to cli/template/extras/src/server/api/trpc-app/with-nextauth.ts diff --git a/cli/template/extras/src/server/api/trpc-pages/with-lucia-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-lucia-db.ts new file mode 100644 index 0000000000..a23f04241e --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-pages/with-lucia-db.ts @@ -0,0 +1,134 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import { type Session } from "lucia"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + session: Session | null; + authRequest: ReturnType; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + session: opts.session, + authRequest: opts.authRequest, + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: CreateNextContextOptions) => { + const { req, res } = opts; + + // Get server session + const authRequest = auth.handleRequest({ req, res }); + const session = await authRequest.validate(); + + return createInnerTRPCContext({ + session, + authRequest, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session?.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-pages/with-lucia.ts b/cli/template/extras/src/server/api/trpc-pages/with-lucia.ts new file mode 100644 index 0000000000..165bc745f4 --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-pages/with-lucia.ts @@ -0,0 +1,132 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; +import { type Session } from "lucia"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { auth } from "~/server/auth"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + session: Session | null; + authRequest: ReturnType; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + session: opts.session, + authRequest: opts.authRequest, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: CreateNextContextOptions) => { + const { req, res } = opts; + + // Get server session + const authRequest = auth.handleRequest({ req, res }); + const session = await authRequest.validate(); + + return createInnerTRPCContext({ + session, + authRequest, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session?.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-nextauth-db.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts rename to cli/template/extras/src/server/api/trpc-pages/with-nextauth-db.ts diff --git a/cli/template/extras/src/server/api/trpc-pages/with-auth.ts b/cli/template/extras/src/server/api/trpc-pages/with-nextauth.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc-pages/with-auth.ts rename to cli/template/extras/src/server/api/trpc-pages/with-nextauth.ts diff --git a/cli/template/extras/src/server/db/drizzle-schema-lucia.ts b/cli/template/extras/src/server/db/drizzle-schema-lucia.ts new file mode 100644 index 0000000000..46db2543b5 --- /dev/null +++ b/cli/template/extras/src/server/db/drizzle-schema-lucia.ts @@ -0,0 +1,80 @@ +import { relations, sql } from "drizzle-orm"; +import { + bigint, + index, + mysqlTableCreator, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const mysqlTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = mysqlTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdById: varchar("createdById", { length: 255 }).notNull(), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updatedAt").onUpdateNow(), + }, + (example) => ({ + createdByIdIdx: index("createdById_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = mysqlTable("user", { + id: varchar("id", { length: 15 }).notNull().primaryKey(), + username: varchar("username", { length: 255 }), + discordId: varchar("discord_id", { length: 18 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(keys), +})); + +export const keys = mysqlTable("key", { + id: varchar("id", { + length: 255, + }).primaryKey(), + userId: varchar("user_id", { + length: 15, + }).notNull(), + + // Required for Lucia Auth even if you don't use username/password auth + hashedPassword: varchar("hashed_password", { + length: 255, + }), +}); + +export const keysRelations = relations(keys, ({ one }) => ({ + user: one(users, { fields: [keys.userId], references: [users.id] }), +})); + +export const sessions = mysqlTable("session", { + id: varchar("id", { + length: 128, + }).primaryKey(), + userId: varchar("user_id", { + length: 15, + }).notNull(), + activeExpires: bigint("active_expires", { + mode: "number", + }).notNull(), + idleExpires: bigint("idle_expires", { + mode: "number", + }).notNull(), +}); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); diff --git a/cli/template/extras/src/server/db/drizzle-schema-auth.ts b/cli/template/extras/src/server/db/drizzle-schema-nextauth.ts similarity index 100% rename from cli/template/extras/src/server/db/drizzle-schema-auth.ts rename to cli/template/extras/src/server/db/drizzle-schema-nextauth.ts diff --git a/cli/template/extras/src/server/db/index-drizzle.ts b/cli/template/extras/src/server/db/index-drizzle.ts index 2a0c8e8f4d..d921d8a124 100644 --- a/cli/template/extras/src/server/db/index-drizzle.ts +++ b/cli/template/extras/src/server/db/index-drizzle.ts @@ -4,9 +4,8 @@ import { drizzle } from "drizzle-orm/planetscale-serverless"; import { env } from "~/env.mjs"; import * as schema from "./schema"; -export const db = drizzle( - new Client({ - url: env.DATABASE_URL, - }).connection(), - { schema } -); +export const dbConnection = new Client({ + url: env.DATABASE_URL, +}).connection(); + +export const db = drizzle(dbConnection, { schema }); diff --git a/cli/template/extras/src/server/lucia-app/base.ts b/cli/template/extras/src/server/lucia-app/base.ts new file mode 100644 index 0000000000..0249c31341 --- /dev/null +++ b/cli/template/extras/src/server/lucia-app/base.ts @@ -0,0 +1,36 @@ +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; +import * as context from "next/headers"; +import { cache } from "react"; + +import { env } from "~/env.mjs"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + // Lucia needs a database adapter. Please set up one for your database: https://lucia-auth.com/getting-started/#setup-your-database + // adapter: yourDBAdapter(), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +/** Get auth session from a server component. */ +export const getPageSession = cache(() => { + const authRequest = auth.handleRequest("GET", context); + return authRequest.validate(); +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/lucia-app/with-drizzle.ts b/cli/template/extras/src/server/lucia-app/with-drizzle.ts new file mode 100644 index 0000000000..3234d20c4d --- /dev/null +++ b/cli/template/extras/src/server/lucia-app/with-drizzle.ts @@ -0,0 +1,42 @@ +import { planetscale } from "@lucia-auth/adapter-mysql"; +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; +import * as context from "next/headers"; +import { cache } from "react"; + +import { env } from "~/env.mjs"; +import { dbConnection } from "./db"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + adapter: planetscale(dbConnection, { + // MySQL table names + user: "project1_user", + key: "project1_key", + session: "project1_session", + }), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +/** Get auth session from a server component. */ +export const getPageSession = cache(() => { + const authRequest = auth.handleRequest("GET", context); + return authRequest.validate(); +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/lucia-app/with-prisma.ts b/cli/template/extras/src/server/lucia-app/with-prisma.ts new file mode 100644 index 0000000000..d0ea8e0772 --- /dev/null +++ b/cli/template/extras/src/server/lucia-app/with-prisma.ts @@ -0,0 +1,42 @@ +import { prisma } from "@lucia-auth/adapter-prisma"; +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; +import * as context from "next/headers"; +import { cache } from "react"; + +import { env } from "~/env.mjs"; +import { db } from "./db"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + adapter: prisma(db, { + // SQL Table Names + user: "user", // model User {} + key: "key", // model Key {} + session: "session", // model Session {} + }), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +/** Get auth session from a server component. */ +export const getPageSession = cache(() => { + const authRequest = auth.handleRequest("GET", context); + return authRequest.validate(); +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/lucia-pages/base.ts b/cli/template/extras/src/server/lucia-pages/base.ts new file mode 100644 index 0000000000..f8bfdae285 --- /dev/null +++ b/cli/template/extras/src/server/lucia-pages/base.ts @@ -0,0 +1,28 @@ +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; + +import { env } from "~/env.mjs"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + // Lucia needs a database adapter. Please set up one for your database: https://lucia-auth.com/getting-started/#setup-your-database + // adapter: yourDBAdapter(), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/lucia-pages/with-drizzle.ts b/cli/template/extras/src/server/lucia-pages/with-drizzle.ts new file mode 100644 index 0000000000..7714524489 --- /dev/null +++ b/cli/template/extras/src/server/lucia-pages/with-drizzle.ts @@ -0,0 +1,34 @@ +import { mysql2 } from "@lucia-auth/adapter-mysql"; +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; + +import { env } from "~/env.mjs"; +import { dbConnection } from "./db"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + adapter: mysql2(dbConnection, { + // MySQL table names + user: "project1_user", + key: "project1_key", + session: "project1_session", + }), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/lucia-pages/with-prisma.ts b/cli/template/extras/src/server/lucia-pages/with-prisma.ts new file mode 100644 index 0000000000..9a0af419fa --- /dev/null +++ b/cli/template/extras/src/server/lucia-pages/with-prisma.ts @@ -0,0 +1,34 @@ +import { prisma } from "@lucia-auth/adapter-prisma"; +import { discord } from "@lucia-auth/oauth/providers"; +import { lucia } from "lucia"; +import { nextjs_future } from "lucia/middleware"; + +import { env } from "~/env.mjs"; +import { db } from "./db"; + +/** @see https://lucia-auth.com/basics/configuration/ */ +export const auth = lucia({ + adapter: prisma(db, { + // SQL Table Names + user: "user", // model User {} + key: "key", // model Key {} + session: "session", // model Session {} + }), + env: env.NODE_ENV === "development" ? "DEV" : "PROD", + middleware: nextjs_future(), + sessionCookie: { + expires: false, + }, + getUserAttributes: (databaseUser) => ({ + username: databaseUser.username, + discordId: databaseUser.discord_id, + }), +}); + +export const discordAuth = discord(auth, { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectUri: env.AUTH_URL + "/api/auth/discord/callback", +}); + +export type Auth = typeof auth; diff --git a/cli/template/extras/src/server/auth-app/base.ts b/cli/template/extras/src/server/nextauth-app/base.ts similarity index 100% rename from cli/template/extras/src/server/auth-app/base.ts rename to cli/template/extras/src/server/nextauth-app/base.ts diff --git a/cli/template/extras/src/server/auth-app/with-drizzle.ts b/cli/template/extras/src/server/nextauth-app/with-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/auth-app/with-drizzle.ts rename to cli/template/extras/src/server/nextauth-app/with-drizzle.ts diff --git a/cli/template/extras/src/server/auth-app/with-prisma.ts b/cli/template/extras/src/server/nextauth-app/with-prisma.ts similarity index 100% rename from cli/template/extras/src/server/auth-app/with-prisma.ts rename to cli/template/extras/src/server/nextauth-app/with-prisma.ts diff --git a/cli/template/extras/src/server/auth-pages/base.ts b/cli/template/extras/src/server/nextauth-pages/base.ts similarity index 100% rename from cli/template/extras/src/server/auth-pages/base.ts rename to cli/template/extras/src/server/nextauth-pages/base.ts diff --git a/cli/template/extras/src/server/auth-pages/with-drizzle.ts b/cli/template/extras/src/server/nextauth-pages/with-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/auth-pages/with-drizzle.ts rename to cli/template/extras/src/server/nextauth-pages/with-drizzle.ts diff --git a/cli/template/extras/src/server/auth-pages/with-prisma.ts b/cli/template/extras/src/server/nextauth-pages/with-prisma.ts similarity index 100% rename from cli/template/extras/src/server/auth-pages/with-prisma.ts rename to cli/template/extras/src/server/nextauth-pages/with-prisma.ts