Repository: iamtouha/next-lucia-auth Branch: main Commit: 6d0a4b31f322 Files: 125 Total size: 213.5 KB Directory structure: gitextract_w3daecuw/ ├── .devcontainer/ │ ├── Dockerfile │ ├── compose.dev.yml │ └── devcontainer.json ├── .eslintrc.cjs ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ └── check.yaml ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── drizzle.config.ts ├── next.config.js ├── package.json ├── playwright.config.ts ├── postcss.config.cjs ├── prettier.config.js ├── src/ │ ├── app/ │ │ ├── (auth)/ │ │ │ ├── layout.tsx │ │ │ ├── login/ │ │ │ │ ├── discord/ │ │ │ │ │ ├── callback/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── login.tsx │ │ │ │ └── page.tsx │ │ │ ├── reset-password/ │ │ │ │ ├── [token]/ │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── reset-password.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── send-reset-email.tsx │ │ │ ├── signup/ │ │ │ │ ├── page.tsx │ │ │ │ └── signup.tsx │ │ │ └── verify-email/ │ │ │ ├── page.tsx │ │ │ └── verify-code.tsx │ │ ├── (landing)/ │ │ │ ├── _components/ │ │ │ │ ├── copy-to-clipboard.tsx │ │ │ │ ├── feature-icons.tsx │ │ │ │ ├── footer.tsx │ │ │ │ ├── header.tsx │ │ │ │ └── hover-card.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── (main)/ │ │ │ ├── _components/ │ │ │ │ ├── footer.tsx │ │ │ │ ├── header.tsx │ │ │ │ └── user-dropdown.tsx │ │ │ ├── account/ │ │ │ │ └── page.tsx │ │ │ ├── dashboard/ │ │ │ │ ├── _components/ │ │ │ │ │ ├── dashboard-nav.tsx │ │ │ │ │ ├── new-post.tsx │ │ │ │ │ ├── post-card-skeleton.tsx │ │ │ │ │ ├── post-card.tsx │ │ │ │ │ ├── posts-skeleton.tsx │ │ │ │ │ ├── posts.tsx │ │ │ │ │ └── verificiation-warning.tsx │ │ │ │ ├── billing/ │ │ │ │ │ ├── _components/ │ │ │ │ │ │ ├── billing-skeleton.tsx │ │ │ │ │ │ ├── billing.tsx │ │ │ │ │ │ └── manage-subscription-form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── settings/ │ │ │ │ └── page.tsx │ │ │ ├── editor/ │ │ │ │ └── [postId]/ │ │ │ │ ├── _components/ │ │ │ │ │ ├── post-editor.tsx │ │ │ │ │ └── post-preview.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── api/ │ │ │ ├── trpc/ │ │ │ │ └── [trpc]/ │ │ │ │ └── route.ts │ │ │ └── webhooks/ │ │ │ └── stripe/ │ │ │ └── route.ts │ │ ├── icon.tsx │ │ ├── layout.tsx │ │ ├── robots.ts │ │ └── sitemap.ts │ ├── components/ │ │ ├── icons.tsx │ │ ├── loading-button.tsx │ │ ├── password-input.tsx │ │ ├── responsive-dialog.tsx │ │ ├── submit-button.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ └── ui/ │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── tabs.tsx │ │ └── textarea.tsx │ ├── config/ │ │ └── subscriptions.ts │ ├── env.js │ ├── lib/ │ │ ├── auth/ │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ └── validate-request.ts │ │ ├── constants.ts │ │ ├── email/ │ │ │ ├── index.tsx │ │ │ └── templates/ │ │ │ ├── email-verification.tsx │ │ │ └── reset-password.tsx │ │ ├── fonts.ts │ │ ├── hooks/ │ │ │ ├── use-debounce.ts │ │ │ └── use-media-query.ts │ │ ├── logger.ts │ │ ├── stripe.ts │ │ ├── utils.ts │ │ └── validators/ │ │ └── auth.ts │ ├── middleware.ts │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ ├── routers/ │ │ │ │ ├── post/ │ │ │ │ │ ├── post.input.ts │ │ │ │ │ ├── post.procedure.ts │ │ │ │ │ └── post.service.ts │ │ │ │ ├── stripe/ │ │ │ │ │ ├── stripe.input.ts │ │ │ │ │ ├── stripe.procedure.ts │ │ │ │ │ └── stripe.service.ts │ │ │ │ └── user/ │ │ │ │ └── user.procedure.ts │ │ │ └── trpc.ts │ │ └── db/ │ │ ├── index.ts │ │ └── schema.ts │ ├── styles/ │ │ └── globals.css │ └── trpc/ │ ├── react.tsx │ ├── server.ts │ └── shared.ts ├── tailwind.config.ts ├── tests/ │ └── e2e/ │ ├── auth-with-credential.spec.ts │ └── utils.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/devcontainers/javascript-node:20 ================================================ FILE: .devcontainer/compose.dev.yml ================================================ services: workspace: build: dockerfile: Dockerfile volumes: - ../:/workspace:cached command: /bin/sh -c "while sleep 1000; do :; done" depends_on: - database database: image: postgres:17.2-alpine environment: POSTGRES_DB: acme POSTGRES_USER: postgres POSTGRES_PASSWORD: root ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Next.js Auth Template", "dockerComposeFile": ["compose.dev.yml"], "service": "workspace", "workspaceFolder": "/workspace", "postCreateCommand": "pnpm config set store-dir $HOME/.pnpm-store", "postStartCommand": "pnpm install", "forwardPorts": [3000], "features": { "ghcr.io/devcontainers-extra/features/pnpm": "latest" }, "customizations": { "vscode": { "settings": { "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "explicit", "source.removeUnusedImports": "explicit" }, "editor.guides.bracketPairs": "active", "editor.rulers": [100], "typescript.tsdk": "node_modules/typescript/lib" }, "extensions": [ "dsznajder.es7-react-js-snippets", "eamodio.gitlens", "esbenp.prettier-vscode", "YoavBls.pretty-ts-errors", "bradlc.vscode-tailwindcss" ] } } } ================================================ FILE: .eslintrc.cjs ================================================ /** @type {import("eslint").Linter.Config} */ const config = { parser: "@typescript-eslint/parser", parserOptions: { project: true, }, plugins: ["@typescript-eslint"], extends: [ "plugin:@next/next/recommended", "plugin:@typescript-eslint/recommended-type-checked", "plugin:@typescript-eslint/stylistic-type-checked", ], rules: { // These opinionated rules are enabled in stylistic-type-checked above. // Feel free to reconfigure them to your own preference. "@typescript-eslint/array-type": "off", "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/consistent-type-imports": [ "warn", { prefer: "type-imports", fixStyle: "inline-type-imports", }, ], "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "@typescript-eslint/require-await": "off", "@typescript-eslint/no-misused-promises": [ "error", { checksVoidReturn: { attributes: false }, }, ], }, ignorePatterns: ["*.js"], }; module.exports = config; ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/pull_request_template.md ================================================ # Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] This change requires a documentation update - [ ] This change requires installing new dependencies ================================================ FILE: .github/workflows/check.yaml ================================================ name: Lint & Test on: pull_request: branches: [main] concurrency: group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: typecheck-and-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2 with: version: 9 - uses: actions/setup-node@v3 with: node-version: 20.x cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Type Check and Lint run: pnpm run typecheck && pnpm run lint env: SKIP_ENV_VALIDATION: true e2e-test: needs: typecheck-and-lint timeout-minutes: 60 runs-on: ubuntu-latest env: DATABASE_URL: ${{secrets.DATABASE_URL}} DISCORD_CLIENT_ID: ${{secrets.DISCORD_CLIENT_ID}} DISCORD_CLIENT_SECRET: ${{secrets.DISCORD_CLIENT_SECRET}} MOCK_SEND_EMAIL: "true" SMTP_HOST: host SMTP_PORT: 587 SMTP_USER: user SMTP_PASSWORD: password NEXT_PUBLIC_APP_URL: http://localhost:3000 STRIPE_API_KEY: stripe_api_key STRIPE_WEBHOOK_SECRET: stripe_webhook_secret STRIPE_PRO_MONTHLY_PLAN_ID: stripe_pro_monthly_plan_id steps: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2 with: version: 9 - uses: actions/setup-node@v3 with: node-version: 20.x cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build the app run: pnpm build - name: Install Playwright Browsers run: pnpm exec playwright install chromium --with-deps - name: Run Playwright tests run: pnpm exec playwright test - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # database /prisma/db.sqlite /prisma/db.sqlite-journal # next.js /.next/ /out/ next-env.d.ts # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* application.log # local env files # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables .env .env*.local # vercel .vercel # typescript *.tsbuildinfo /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ tests/e2e/output ================================================ FILE: LICENSE ================================================ # MIT License Copyright (c) [2023] [Touha Zohair] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Next.js Auth Starter Template ## Motivation Implementing authentication in Next.js, especially Email+Password authentication, can be challenging. NextAuth intentionally limits email password functionality to discourage the use of passwords due to security risks and added complexity. However, in certain projects, clients may require user password authentication. Lucia offers a flexible alternative to NextAuth.js, providing more customization options without compromising on security. This template serves as a starting point for building a Next.js app with Lucia authentication. ## Lucia vs. NextAuth.js Lucia is less opinionated than NextAuth, offering greater flexibility for customization. While Lucia involves more setup, it provides a higher degree of flexibility, making it a suitable choice for projects requiring unique authentication configurations. ## Key Features - **Authentication:** 💼 Support for Credential and OAuth authentication. - **Authorization:** 🔒 Easily manage public and protected routes within the `app directory`. - **Email Verification:** 📧 Verify user identities through email. - **Password Reset:** 🔑 Streamline password resets by sending email password reset links. - **Lucia + tRPC:** 🔄 Similar to NextAuth with tRPC, granting access to sessions and user information through tRPC procedures. - **E2E tests:** 🧪 Catch every issue before your users do with comprehensive E2E testing. - **Stripe Payment:** 💳 Setup user subscriptions seamlessly with stripe. - **Email template with react-email:** ✉️ Craft your email templates using React. - **PostgreSQL Database:** 🛢️ Utilize a PostgreSQL database set up using Drizzle for enhanced performance and type safety. - **Database Migration:** 🚀 Included migration script to extend the database schema according to your project needs. ## Tech Stack - [Next.js](https://nextjs.org) - [Lucia](https://lucia-auth.com/) - [tRPC](https://trpc.io) - [Drizzle ORM](https://orm.drizzle.team/) - [PostgreSQL](https://www.postgresql.org/) - [Stripe](https://stripe.com/) - [Tailwind CSS](https://tailwindcss.com) - [Shadcn UI](https://ui.shadcn.com/) - [React Hook Form](https://www.react-hook-form.com/) - [React Email](https://react.email/) - [Playwright](https://playwright.dev/) ## Get Started 1. Clone this repository to your local machine. 2. Copy `.env.example` to `.env` and fill in the required environment variables. 3. Run `pnpm install` to install dependencies. 4. `(for node v18 or lower):` Uncomment polyfills for `webCrypto` in `src/lib/auth/index.ts` 5. Update app title, database prefix, and other parameters in the `src/lib/constants.ts` file. 6. Run `pnpm db:push` to push your schema to the database. 7. Execute `pnpm dev` to start the development server and enjoy! ## Testing 1. Install [Playwright](https://playwright.dev/) (use this command if you want to install chromium only `pnpm exec playwright install chromium --with-deps`) 2. Build production files using `pnpm build` 3. Run `pnpm test:e2e` (add --debug flag to open tests in browser in debug mode) ## Using Github actions Add the following environment variables to your **github actions repository secrets** - `DATABASE_URL`, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET` ## Roadmap - [ ] Update Password - [x] Stripe Integration - [ ] Admin Dashboard (under consideration) - [ ] Role-Based Access Policy (under consideration) ## Contributing To contribute, fork the repository and create a feature branch. Test your changes, and if possible, open an issue for discussion before submitting a pull request. Follow project guidelines, and welcome feedback to ensure a smooth integration of your contributions. Your pull requests are warmly welcome. ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/styles/globals.css", "baseColor": "zinc", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ================================================ FILE: drizzle.config.ts ================================================ import { defineConfig } from "drizzle-kit"; import { DATABASE_PREFIX } from "@/lib/constants"; export default defineConfig({ schema: "./src/server/db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, }, tablesFilter: [`${DATABASE_PREFIX}_*`], }); ================================================ FILE: next.config.js ================================================ await import("./src/env.js"); /** @type {import("next").NextConfig} */ const config = {}; export default config; ================================================ FILE: package.json ================================================ { "name": "next-lucia-auth", "version": "0.1.0", "private": true, "type": "module", "scripts": { "build": "next build", "db:push": "dotenv drizzle-kit push", "db:generate": "dotenv drizzle-kit generate", "db:migrate": "dotenv drizzle-kit migrate", "db:studio": "dotenv drizzle-kit studio", "dev": "next dev", "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", "stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/stripe --latest", "test:e2e": "playwright test" }, "dependencies": { "@hookform/resolvers": "^3.9.0", "@lucia-auth/adapter-drizzle": "1.0.7", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@react-email/components": "^0.0.12", "@react-email/render": "^0.0.10", "@t3-oss/env-nextjs": "^0.7.3", "@tanstack/react-query": "^4.36.1", "@trpc/client": "^10.45.2", "@trpc/next": "^10.45.2", "@trpc/react-query": "^10.45.2", "@trpc/server": "^10.45.2", "arctic": "^1.9.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucia": "3.2.0", "next": "^14.2.5", "next-themes": "^0.2.1", "nodemailer": "^6.9.14", "oslo": "^1.2.1", "postgres": "^3.4.4", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.52.1", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.0", "server-only": "^0.0.1", "sonner": "^1.5.0", "stripe": "^14.25.0", "superjson": "^2.2.1", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "vaul": "^0.8.9", "zod": "^3.23.8" }, "devDependencies": { "@next/eslint-plugin-next": "^14.2.5", "@playwright/test": "^1.45.3", "@tailwindcss/typography": "^0.5.13", "@types/eslint": "^8.56.11", "@types/node": "^18.19.42", "@types/nodemailer": "^6.4.15", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.19", "dotenv": "^16.4.5", "dotenv-cli": "^7.4.2", "drizzle-kit": "^0.23.0", "drizzle-orm": "^0.32.1", "eslint": "^8.57.0", "pg": "^8.12.0", "postcss": "^8.4.40", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.5.14", "tailwindcss": "^3.4.7", "tsx": "^4.16.2", "typescript": "^5.5.4" }, "ct3aMetadata": { "initVersion": "7.24.2" } } ================================================ FILE: playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; import "dotenv/config"; const baseURL = `http://localhost:${process.env.PORT ?? 3000}`; export default defineConfig({ testDir: "./tests/e2e", outputDir: "./tests/e2e/output", timeout: 60 * 1000, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: "html", use: { trace: "on-first-retry", baseURL, }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, ], webServer: { command: "npx cross-env NODE_ENV=test npm run start", url: baseURL, stdout: "pipe", stderr: "pipe", reuseExistingServer: !process.env.CI, }, }); ================================================ FILE: postcss.config.cjs ================================================ const config = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; module.exports = config; ================================================ FILE: prettier.config.js ================================================ /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ const config = { plugins: ["prettier-plugin-tailwindcss"], tabWidth: 2, semi: true, singleQuote: false, printWidth: 100, }; export default config; ================================================ FILE: src/app/(auth)/layout.tsx ================================================ import type { ReactNode } from "react"; const AuthLayout = ({ children }: { children: ReactNode }) => { return (
{children}
); }; export default AuthLayout; ================================================ FILE: src/app/(auth)/login/discord/callback/route.ts ================================================ import { cookies } from "next/headers"; import { generateId } from "lucia"; import { OAuth2RequestError } from "arctic"; import { eq } from "drizzle-orm"; import { discord, lucia } from "@/lib/auth"; import { db } from "@/server/db"; import { Paths } from "@/lib/constants"; import { users } from "@/server/db/schema"; export async function GET(request: Request): Promise { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const storedState = cookies().get("discord_oauth_state")?.value ?? null; if (!code || !state || !storedState || state !== storedState) { return new Response(null, { status: 400, headers: { Location: Paths.Login }, }); } try { const tokens = await discord.validateAuthorizationCode(code); const discordUserRes = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${tokens.accessToken}`, }, }); const discordUser = (await discordUserRes.json()) as DiscordUser; if (!discordUser.email || !discordUser.verified) { return new Response( JSON.stringify({ error: "Your Discord account must have a verified email address.", }), { status: 400, headers: { Location: Paths.Login } }, ); } const existingUser = await db.query.users.findFirst({ where: (table, { eq, or }) => or(eq(table.discordId, discordUser.id), eq(table.email, discordUser.email!)), }); const avatar = discordUser.avatar ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp` : null; if (!existingUser) { const userId = generateId(21); await db.insert(users).values({ id: userId, email: discordUser.email, emailVerified: true, discordId: discordUser.id, avatar, }); const session = await lucia.createSession(userId, {}); const sessionCookie = lucia.createSessionCookie(session.id); cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); return new Response(null, { status: 302, headers: { Location: Paths.Dashboard }, }); } if (existingUser.discordId !== discordUser.id || existingUser.avatar !== avatar) { await db .update(users) .set({ discordId: discordUser.id, emailVerified: true, avatar, }) .where(eq(users.id, existingUser.id)); } const session = await lucia.createSession(existingUser.id, {}); const sessionCookie = lucia.createSessionCookie(session.id); cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); return new Response(null, { status: 302, headers: { Location: Paths.Dashboard }, }); } catch (e) { // the specific error message depends on the provider if (e instanceof OAuth2RequestError) { // invalid code return new Response(JSON.stringify({ message: "Invalid code" }), { status: 400, }); } console.error(e); return new Response(JSON.stringify({ message: "internal server error" }), { status: 500, }); } } interface DiscordUser { id: string; username: string; avatar: string | null; banner: string | null; global_name: string | null; banner_color: string | null; mfa_enabled: boolean; locale: string; email: string | null; verified: boolean; } ================================================ FILE: src/app/(auth)/login/discord/route.ts ================================================ import { cookies } from "next/headers"; import { generateState } from "arctic"; import { discord } from "@/lib/auth"; import { env } from "@/env"; export async function GET(): Promise { const state = generateState(); const url = await discord.createAuthorizationURL(state, { scopes: ["identify", "email"], }); cookies().set("discord_oauth_state", state, { path: "/", secure: env.NODE_ENV === "production", httpOnly: true, maxAge: 60 * 10, sameSite: "lax", }); return Response.redirect(url); } ================================================ FILE: src/app/(auth)/login/login.tsx ================================================ "use client"; import Link from "next/link"; import { useFormState } from "react-dom"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { PasswordInput } from "@/components/password-input"; import { DiscordLogoIcon } from "@/components/icons"; import { APP_TITLE } from "@/lib/constants"; import { login } from "@/lib/auth/actions"; import { Label } from "@/components/ui/label"; import { SubmitButton } from "@/components/submit-button"; export function Login() { const [state, formAction] = useFormState(login, null); return ( {APP_TITLE} Log In Log in to your account to access your dashboard
or
{state?.fieldError ? (
    {Object.values(state.fieldError).map((err) => (
  • {err}
  • ))}
) : state?.formError ? (

{state?.formError}

) : null} Log In
); } ================================================ FILE: src/app/(auth)/login/page.tsx ================================================ import { redirect } from "next/navigation"; import { validateRequest } from "@/lib/auth/validate-request"; import { Paths } from "@/lib/constants"; import { Login } from "./login"; export const metadata = { title: "Login", description: "Login Page", }; export default async function LoginPage() { const { user } = await validateRequest(); if (user) redirect(Paths.Dashboard); return ; } ================================================ FILE: src/app/(auth)/reset-password/[token]/page.tsx ================================================ import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { ResetPassword } from "./reset-password"; export const metadata = { title: "Reset Password", description: "Reset Password Page", }; export default function ResetPasswordPage({ params, }: { params: { token: string }; }) { return ( Reset password Enter new password. ); } ================================================ FILE: src/app/(auth)/reset-password/[token]/reset-password.tsx ================================================ "use client"; import { useEffect } from "react"; import { useFormState } from "react-dom"; import { toast } from "sonner"; import { ExclamationTriangleIcon } from "@/components/icons"; import { SubmitButton } from "@/components/submit-button"; import { PasswordInput } from "@/components/password-input"; import { Label } from "@/components/ui/label"; import { resetPassword } from "@/lib/auth/actions"; export function ResetPassword({ token }: { token: string }) { const [state, formAction] = useFormState(resetPassword, null); useEffect(() => { if (state?.error) { toast(state.error, { icon: , }); } }, [state?.error]); return (
Reset Password
); } ================================================ FILE: src/app/(auth)/reset-password/page.tsx ================================================ import { redirect } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { SendResetEmail } from "./send-reset-email"; import { validateRequest } from "@/lib/auth/validate-request"; import { Paths } from "@/lib/constants"; export const metadata = { title: "Forgot Password", description: "Forgot Password Page", }; export default async function ForgotPasswordPage() { const { user } = await validateRequest(); if (user) redirect(Paths.Dashboard); return ( Forgot password? Password reset link will be sent to your email. ); } ================================================ FILE: src/app/(auth)/reset-password/send-reset-email.tsx ================================================ "use client"; import { useEffect } from "react"; import { useFormState } from "react-dom"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { SubmitButton } from "@/components/submit-button"; import { sendPasswordResetLink } from "@/lib/auth/actions"; import { ExclamationTriangleIcon } from "@/components/icons"; import { Paths } from "@/lib/constants"; export function SendResetEmail() { const [state, formAction] = useFormState(sendPasswordResetLink, null); const router = useRouter(); useEffect(() => { if (state?.success) { toast("A password reset link has been sent to your email."); router.push(Paths.Login); } if (state?.error) { toast(state.error, { icon: , }); } }, [state?.error, state?.success]); return (
Reset Password
); } ================================================ FILE: src/app/(auth)/signup/page.tsx ================================================ import { redirect } from "next/navigation"; import { Signup } from "./signup"; import { validateRequest } from "@/lib/auth/validate-request"; import { Paths } from "@/lib/constants"; export const metadata = { title: "Sign Up", description: "Signup Page", }; export default async function SignupPage() { const { user } = await validateRequest(); if (user) redirect(Paths.Dashboard); return ; } ================================================ FILE: src/app/(auth)/signup/signup.tsx ================================================ "use client"; import { useFormState } from "react-dom"; import Link from "next/link"; import { PasswordInput } from "@/components/password-input"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { DiscordLogoIcon } from "@/components/icons"; import { APP_TITLE } from "@/lib/constants"; import { Label } from "@/components/ui/label"; import { signup } from "@/lib/auth/actions"; import { SubmitButton } from "@/components/submit-button"; export function Signup() { const [state, formAction] = useFormState(signup, null); return ( {APP_TITLE} Sign Up Sign up to start using the app
or
{state?.fieldError ? (
    {Object.values(state.fieldError).map((err) => (
  • {err}
  • ))}
) : state?.formError ? (

{state?.formError}

) : null}
Already signed up? Login instead.
Sign Up
); } ================================================ FILE: src/app/(auth)/verify-email/page.tsx ================================================ import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { redirect } from "next/navigation"; import { validateRequest } from "@/lib/auth/validate-request"; import { VerifyCode } from "./verify-code"; import { Paths } from "@/lib/constants"; export const metadata = { title: "Verify Email", description: "Verify Email Page", }; export default async function VerifyEmailPage() { const { user } = await validateRequest(); if (!user) redirect(Paths.Login); if (user.emailVerified) redirect(Paths.Dashboard); return ( Verify Email Verification code was sent to {user.email}. Check your spam folder if you can't find the email. ); } ================================================ FILE: src/app/(auth)/verify-email/verify-code.tsx ================================================ "use client"; import { Input } from "@/components/ui/input"; import { Label } from "@radix-ui/react-label"; import { useEffect, useRef } from "react"; import { useFormState } from "react-dom"; import { toast } from "sonner"; import { ExclamationTriangleIcon } from "@/components/icons"; import { logout, verifyEmail, resendVerificationEmail as resendEmail } from "@/lib/auth/actions"; import { SubmitButton } from "@/components/submit-button"; export const VerifyCode = () => { const [verifyEmailState, verifyEmailAction] = useFormState(verifyEmail, null); const [resendState, resendAction] = useFormState(resendEmail, null); const codeFormRef = useRef(null); useEffect(() => { if (resendState?.success) { toast("Email sent!"); } if (resendState?.error) { toast(resendState.error, { icon: , }); } }, [resendState?.error, resendState?.success]); useEffect(() => { if (verifyEmailState?.error) { toast(verifyEmailState.error, { icon: , }); } }, [verifyEmailState?.error]); return (
Verify
Resend Code
want to use another email? Log out now.
); }; ================================================ FILE: src/app/(landing)/_components/copy-to-clipboard.tsx ================================================ "use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import { toast } from "sonner"; export const CopyToClipboard = ({ text }: { text: string }) => { const [copied, setCopied] = useState(false); const copyToClipboard = async () => { setCopied(true); setTimeout(() => { setCopied(false); }, 2000); await navigator.clipboard.writeText(text); toast("Copied to clipboard", { icon: , }); }; return (
); }; ================================================ FILE: src/app/(landing)/_components/feature-icons.tsx ================================================ import { forwardRef, type SVGProps } from "react"; import { cn } from "@/lib/utils"; const NextjsLight = forwardRef>( ({ className, ...props }, ref) => ( ), ); NextjsLight.displayName = "NextjsLight"; const NextjsDark = forwardRef>( ({ className, ...props }, ref) => ( ), ); NextjsDark.displayName = "NextjsDark"; const ReactJs = forwardRef>( ({ className, ...props }, ref) => ( {" "} ), ); ReactJs.displayName = "ReactJs"; const TailwindCss = forwardRef>( ({ className, ...props }, ref) => ( ), ); TailwindCss.displayName = "TailwindCss"; const LuciaAuth = forwardRef>( ({ className, ...props }, ref) => ( ), ); LuciaAuth.displayName = "LuciaAuth"; const Drizzle = forwardRef>( ({ className, ...props }, ref) => ( ), ); Drizzle.displayName = "Drizzle"; const TRPC = forwardRef>(({ className, ...props }, ref) => ( )); TRPC.displayName = "TRPC"; const ShadcnUi = forwardRef>( ({ className, ...props }, ref) => ( ), ); ShadcnUi.displayName = "ShadcnUi"; const ReactEmail = forwardRef>( ({ className, ...props }, ref) => ( ), ); ReactEmail.displayName = "ReactEmail"; const StripeLogo = forwardRef>( ({ className, ...props }, ref) => ( ), ); StripeLogo.displayName = "StripeLogo"; export { Drizzle, LuciaAuth, NextjsDark, NextjsLight, ReactEmail, ReactJs, ShadcnUi, StripeLogo, TailwindCss, TRPC, }; ================================================ FILE: src/app/(landing)/_components/footer.tsx ================================================ import { ThemeToggle } from "@/components/theme-toggle"; import { CodeIcon } from "@radix-ui/react-icons"; const githubUrl = "https://github.com/iamtouha/next-lucia-auth"; const twitterUrl = "https://twitter.com/iamtouha"; export const Footer = () => { return ( ); }; ================================================ FILE: src/app/(landing)/_components/header.tsx ================================================ import Link from "next/link"; import { RocketIcon } from "@/components/icons"; import { APP_TITLE } from "@/lib/constants"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { HamburgerMenuIcon } from "@radix-ui/react-icons"; const routes = [ { name: "Home", href: "/" }, { name: "Features", href: "/#features" }, { name: "Documentation", href: "https://www.touha.dev/posts/simple-nextjs-t3-authentication-with-lucia", }, ] as const; export const Header = () => { return (
{routes.map(({ name, href }) => ( {name} ))}
{APP_TITLE}
); }; ================================================ FILE: src/app/(landing)/_components/hover-card.tsx ================================================ "use client"; import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import React, { useRef, useState } from "react"; type FeaturesProps = { name: string; description: string; logo: React.ReactNode; }; const CardSpotlight = (props: FeaturesProps) => { const divRef = useRef(null); const [isFocused, setIsFocused] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [opacity, setOpacity] = useState(0); const handleMouseMove = (e: React.MouseEvent) => { if (!divRef.current || isFocused) return; const div = divRef.current; const rect = div.getBoundingClientRect(); setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); }; const handleFocus = () => { setIsFocused(true); setOpacity(1); }; const handleBlur = () => { setIsFocused(false); setOpacity(0); }; const handleMouseEnter = () => { setOpacity(1); }; const handleMouseLeave = () => { setOpacity(0); }; return (
{props.logo}
{props.name} {props.description} ); }; export default CardSpotlight; ================================================ FILE: src/app/(landing)/layout.tsx ================================================ import { APP_TITLE } from "@/lib/constants"; import { type Metadata } from "next"; import { type ReactNode } from "react"; import { Footer } from "./_components/footer"; import { Header } from "./_components/header"; export const metadata: Metadata = { title: APP_TITLE, description: "A Next.js starter with T3 stack and Lucia auth.", }; function LandingPageLayout({ children }: { children: ReactNode }) { return ( <>
{children}