diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 00000000..e57084cc --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,23 @@ +--- +name: code-reviewer +description: Expert code reviewer for quality, performance, and maintainability. Use PROACTIVELY after writing or modifying code to ensure standards are met before committing. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +Senior code reviewer for programming.in.th (Next.js 15, React 19, TypeScript, Prisma). + +**Process**: `git diff --name-only` → `git diff` → read files → review + +**Review for**: +- **Performance**: Server Components (avoid unnecessary `'use client'`), selective Prisma fields, no N+1, pagination, caching +- **Types**: No `any`, Zod validation for APIs, proper error handling +- **Patterns**: Follows codebase conventions, focused functions, clear naming + +**Key patterns**: +- Prisma: Always `select`, import from `@/lib/prisma` +- Auth: `getServerUser()` from `@/lib/session`, check `user.admin` +- APIs: Zod schemas, consistent errors (400/401/403/404/500) +- Components: Tailwind, `dark:` variants, accessibility + +**Output**: Issues by severity (Critical/Warning/Suggestion) with `file:line` and fixes. Verdict: **APPROVED** / **CHANGES REQUESTED** diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md new file mode 100644 index 00000000..4ef12f5f --- /dev/null +++ b/.claude/agents/security-reviewer.md @@ -0,0 +1,33 @@ +--- +name: security-reviewer +description: Security specialist for identifying vulnerabilities, auth issues, and data exposure risks. Use PROACTIVELY when reviewing API routes, auth logic, file handling, or user input processing. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +Security specialist for programming.in.th (auth, code submissions, file storage). + +**Process**: `git diff` for changes OR grep for security patterns → analyze → remediate + +**Check for**: +- **Auth**: `getServerUser()` on protected routes, `user.admin` for admin routes +- **Validation**: Zod `safeParse()` for all input, no internal details in errors +- **Injection**: Prisma parameterized queries, no user input in commands/paths +- **Data exposure**: Selective fields only, no secrets in responses/logs +- **Files**: Presigned S3 URLs, validate types/sizes, sanitize paths + +**Search for secrets**: +```bash +grep -rE "(password|secret|key|token)\s*[:=]" --include="*.ts" +``` + +**Required patterns**: +```typescript +const user = await getServerUser() +if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }) + +const result = Schema.safeParse(input) +if (!result.success) return Response.json({ error: 'Invalid' }, { status: 400 }) +``` + +**Output**: Findings by severity (Critical/High/Medium/Low) with risk, evidence, fix. Verdict: **SECURE** / **ISSUES FOUND** / **CRITICAL** diff --git a/.claude/commands/perf.md b/.claude/commands/perf.md new file mode 100644 index 00000000..d10a42f7 --- /dev/null +++ b/.claude/commands/perf.md @@ -0,0 +1,12 @@ +--- +description: Analyze a component or page for performance issues +allowed-tools: Read, Glob, Grep, Bash +argument-hint: "" +--- + +Analyze `$1` for: +- **React**: Unnecessary `'use client'`? Missing memoization? Re-render issues? +- **Data**: Selective fields? N+1 queries? Pagination? Caching (ISR/SWR)? +- **Bundle**: Optimized imports? Next.js ``? + +Report issues by severity (Critical/Warning) with specific fixes. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 00000000..972c4e9c --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,13 @@ +--- +description: Review code changes for quality, performance, and maintainability +allowed-tools: Bash, Read, Glob, Grep +argument-hint: "[file-path or --staged]" +--- + +1. Get changes: `git diff` (or `git diff --cached` for staged, or read specific file) +2. Review for: + - **Performance**: Server Components, selective queries, no N+1, caching + - **Types**: No `any`, proper Zod validation + - **Security**: Auth checks, input validation, no secrets + - **Patterns**: Follows codebase conventions +3. Report issues and verdict: **APPROVED** / **CHANGES REQUESTED** diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 00000000..d7a8a7e9 --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,11 @@ +--- +description: Run all verification checks (types, lint, tests) - the "No Regression" check +allowed-tools: Bash, Read, Glob, Grep +--- + +Run in sequence, stop on failure: +1. `pnpm check-types` +2. `pnpm lint` +3. `pnpm test` + +Report **PASS/FAIL** with error details if any. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..f3bea4a7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(pnpm:*)", + "Bash(npm:*)", + "Bash(npx:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git branch:*)", + "Bash(git show:*)", + "Read", + "Glob", + "Grep", + "Skill" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash -c 'FILE=\"$CLAUDE_TOOL_ARG_file_path\"; if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx ]]; then echo \"[Hook] TypeScript file modified: $FILE\"; fi'" + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel)\" && if git diff --name-only HEAD 2>/dev/null | grep -qE \"\\.(ts|tsx)$\"; then echo \"[Reminder] TypeScript files changed. Consider running: pnpm check-types && pnpm lint && pnpm test\"; fi'" + } + ] + } + ] + } +} diff --git a/.claude/skills/api-development/SKILL.md b/.claude/skills/api-development/SKILL.md new file mode 100644 index 00000000..c6f721c3 --- /dev/null +++ b/.claude/skills/api-development/SKILL.md @@ -0,0 +1,50 @@ +--- +name: api-development +description: Use when creating or modifying Elysia API routes. Ensures proper validation with t schema, auth guards, error handling, and performance patterns. +allowed-tools: Read, Edit, Write, Glob, Grep, Bash +--- + +API routes use [Elysia](https://elysiajs.com) with TypeBox validation: + +```typescript +import { Elysia, t } from 'elysia' +import { prisma } from '@/lib/prisma' + +const app = new Elysia() + .get('/tasks/:id', async ({ params, query, status }) => { + const limit = query.limit ?? 10 + const task = await prisma.task.findUnique({ + where: { id: params.id }, + select: { id: true, title: true } + }) + if (!task) return status(404, { error: 'Not found' }) + return task + }, { + params: t.Object({ id: t.String() }), + query: t.Object({ limit: t.Optional(t.Numeric()) }) + }) + .post('/tasks', async ({ body, status }) => { + const task = await prisma.task.create({ data: body }) + return status(201, task) + }, { + body: t.Object({ + title: t.String({ minLength: 1 }), + fullScore: t.Number({ minimum: 0 }) + }) + }) +``` + +**Auth guard pattern**: +```typescript +.derive(async ({ headers, status }) => { + const user = await getUser(headers.authorization) + if (!user) return status(401, { error: 'Unauthorized' }) + return { user } +}) +.get('/admin', ({ user, status }) => { + if (!user.admin) return status(403, { error: 'Forbidden' }) + return 'admin only' +}) +``` + +**Checklist**: `t.Object` validation, auth derive/guard, selective Prisma fields, pagination, `status()` for errors. diff --git a/.claude/skills/component-development/SKILL.md b/.claude/skills/component-development/SKILL.md new file mode 100644 index 00000000..42005b7a --- /dev/null +++ b/.claude/skills/component-development/SKILL.md @@ -0,0 +1,30 @@ +--- +name: component-development +description: Use when creating or modifying React components. Ensures proper Server/Client component patterns, performance optimization, and accessibility. +allowed-tools: Read, Edit, Write, Glob, Grep +--- + +Components in `src/components/`. Default to Server Components. + +```tsx +// Server Component (default) - no directive needed +export function TaskCard({ task }: { task: Task }) { + return
{task.title}
+} + +// Client Component - only for interactivity +'use client' +import { useState } from 'react' +export function Toggle() { + const [on, setOn] = useState(false) + return +} +``` + +**When to use `'use client'`**: onClick/onSubmit, useState/useEffect, browser APIs. + +**Performance**: Push `'use client'` to smallest component, use `memo()` for expensive renders, Next.js ``. + +**Accessibility**: Labels for inputs, ARIA attributes, keyboard nav for custom elements. + +**Styling**: Tailwind only, `dark:` variants, custom colors: `prog-primary-500`. diff --git a/.claude/skills/database-changes/SKILL.md b/.claude/skills/database-changes/SKILL.md new file mode 100644 index 00000000..7d845025 --- /dev/null +++ b/.claude/skills/database-changes/SKILL.md @@ -0,0 +1,35 @@ +--- +name: database-changes +description: Use when modifying Prisma schema or database queries. Ensures proper migrations, type safety, and query performance. +allowed-tools: Read, Edit, Write, Glob, Grep, Bash +--- + +Schema at `prisma/schema.prisma`. Always import from `@/lib/prisma`. + +**Schema changes**: +```bash +# Edit schema, then: +pnpm prisma migrate dev --name descriptive_name +pnpm check-types +``` + +**Query patterns**: +```typescript +// Always select specific fields +const tasks = await prisma.task.findMany({ + where: { private: false }, + select: { id: true, title: true }, + take: 10, skip: 0 // Always paginate +}) + +// Avoid N+1 - use include or batch queries +const tasks = await prisma.task.findMany({ include: { tags: true } }) +// OR +const submissions = await prisma.submission.findMany({ + where: { taskId: { in: taskIds } } +}) +``` + +**Indexes**: Add `@@index([field])` for WHERE/ORDER BY columns. + +**Models**: User, Task, Submission, Assessment, Category, Tag, Bookmark. diff --git a/.env.example b/.env.example index 3ba11ce4..39e5f077 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,12 @@ GITHUB_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= +# Auth.js v5 Configuration +# Generate AUTH_SECRET with: openssl rand -base64 32 +AUTH_SECRET= +# Base URL for authentication (replaces NEXTAUTH_URL in v5) +AUTH_URL=http://localhost:3000 + # Bucket for files such as statement and attachments BUCKET_NAME= BUCKET_KEY_ID= @@ -15,8 +21,5 @@ BUCKET_REGION=us-east-1 DATABASE_URL= # DIRECT_URL= # SHADOW_DATABASE_URL= - -# Public URL -NEXTAUTH_URL=http://localhost:3000 NEXT_PUBLIC_REALTIME_URL=https://rtss.crackncode.org NEXT_PUBLIC_AWS_URL=https://prginth01.sgp1.cdn.digitaloceanspaces.com diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2eeab582 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,45 @@ +# programming.in.th + +Next.js 15 + React 19 + TypeScript + Prisma competitive programming platform. + +## Core Principles + +1. **Performance**: Server Components by default, selective Prisma fields, ISR/SWR caching +2. **No Regression**: Run `pnpm check-types && pnpm lint && pnpm test` before commits +3. **Maintainability**: Follow existing patterns, strict TypeScript, Zod validation + +## Key Patterns + +```tsx +// Server Component (default) - direct Prisma +const tasks = await prisma.task.findMany({ + where: { private: false }, + select: { id: true, title: true } // Always select specific fields +}) + +// Client Component - only when needed +'use client' // forms, useState, useEffect, browser APIs + +// API Routes - always validate with Zod +const result = Schema.safeParse(input) +if (!result.success) return Response.json({ error: 'Invalid' }, { status: 400 }) + +// Auth - use getServerUser() from @/lib/session +// Prisma - import from @/lib/prisma (singleton) +``` + +## Commands + +```bash +pnpm dev # Dev server (Turbopack) +pnpm check-types # TypeScript check +pnpm lint # ESLint +pnpm test # Vitest +``` + +## Gotchas + +- Prisma: Always `@/lib/prisma`, always use `select` +- Auth: `getServerUser()` for server-side, check `user.admin` for admin routes +- Files: Presigned S3 URLs only, sanitize paths +- Dark mode: `dark:` Tailwind variants diff --git a/MIGRATION_V5.md b/MIGRATION_V5.md new file mode 100644 index 00000000..7695e9ef --- /dev/null +++ b/MIGRATION_V5.md @@ -0,0 +1,229 @@ +# Auth.js v5 Migration Guide + +## Overview + +This document describes the migration from NextAuth.js v4 to Auth.js v5 and the breaking changes that affect all users. + +## 🚨 Critical Breaking Changes + +### 1. All Users Will Be Logged Out + +**Impact:** When this update is deployed, **ALL users will be forcibly logged out** and will need to sign in again. + +**Reason:** Auth.js v5 uses different session cookie names: + +- **Old (v4):** `next-auth.session-token` and `__Secure-next-auth.session-token` +- **New (v5):** `authjs.session-token` and `__Secure-authjs.session-token` + +**Mitigation:** + +- Users will need to log in again after deployment +- Consider announcing this change in advance +- Plan deployment during low-traffic periods if possible + +### 2. New Environment Variables Required + +Auth.js v5 requires new environment variables. Update your `.env` file: + +```bash +# Required: Generate with: openssl rand -base64 32 +AUTH_SECRET=your-generated-secret-here + +# Required: Base URL for authentication (replaces NEXTAUTH_URL) +AUTH_URL=http://localhost:3000 + +# Keep existing OAuth credentials +GITHUB_ID=... +GITHUB_SECRET=... +GOOGLE_CLIENT_ID=... +GOOGLE_CLIENT_SECRET=... +``` + +**Action Required:** + +1. Generate `AUTH_SECRET`: `openssl rand -base64 32` +2. Add it to your production environment variables +3. Update `AUTH_URL` to match your production URL + +## Technical Changes + +### Package Updates + +```json +{ + "next-auth": "4.24.11" → "5.0.0-beta.30", + "@next-auth/prisma-adapter": "1.0.7" → "@auth/prisma-adapter": "2.11.1" +} +``` + +**Note:** We are using Auth.js v5 beta version. While this has been tested, be aware that beta versions may have stability considerations. + +### API Changes + +#### Authentication Configuration + +**Before (v4):** + +```typescript +import NextAuth, { NextAuthOptions } from 'next-auth' + +export const authOptions: NextAuthOptions = { ... } +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST } +``` + +**After (v5):** + +```typescript +import NextAuth from 'next-auth' + +export const { handlers, auth, signIn, signOut } = NextAuth({ ... }) +export const { GET, POST } = handlers +``` + +#### Session Retrieval + +**Before (v4):** + +```typescript +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' + +const session = await getServerSession(authOptions) +``` + +**After (v5):** + +```typescript +import { auth } from '@/lib/auth' + +const session = await auth() +``` + +#### Middleware + +**Before (v4):** + +```typescript +export default function middleware(req: NextRequest) { + if (req.cookies.get('next-auth.session-token')) { + return NextResponse.redirect(new URL('/', req.url)) + } +} +``` + +**After (v5):** + +```typescript +import { auth } from '@/lib/auth' + +export default auth(req => { + if (req.auth) { + return NextResponse.redirect(new URL('/', req.url)) + } +}) +``` + +#### Auth Hooks (useRequireAuth, useRequireAdmin) + +**Before (v4):** + +```typescript +import { useRouter } from 'next/router' + +const { data: session } = useSession() +useEffect(() => { + if (!session && typeof session != 'undefined') { + router.push('/login') + } +}, [session, router]) +``` + +**After (v5):** + +```typescript +import { useRouter } from 'next/navigation' + +const { data: session, status } = useSession() +useEffect(() => { + if (status === 'loading') return + if (status === 'unauthenticated') { + router.push('/login') + } +}, [status, router]) + +return status === 'loading' ? null : session +``` + +**Improvements:** + +- Uses `next/navigation` instead of `next/router` (Next.js 15 App Router) +- Checks `status` instead of session value for better loading state handling +- Prevents redirect flashing by waiting for loading to complete +- Returns `null` during loading to prevent flash of wrong content + +## Deployment Checklist + +Before deploying this update: + +- [ ] Generate `AUTH_SECRET` using `openssl rand -base64 32` +- [ ] Add `AUTH_SECRET` to production environment variables +- [ ] Add `AUTH_URL` to production environment variables +- [ ] Verify `GITHUB_ID`, `GITHUB_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` are set +- [ ] Announce to users that they will need to log in again +- [ ] Plan deployment timing (consider low-traffic periods) +- [ ] Test authentication flow in staging environment: + - [ ] GitHub OAuth login + - [ ] Google OAuth login + - [ ] Session persistence across page reloads + - [ ] Protected routes redirect correctly + - [ ] Admin-only routes check permissions correctly +- [ ] Verify type checking passes: `pnpm check-types` +- [ ] Verify linting passes: `pnpm lint` +- [ ] Verify tests pass: `pnpm test` + +## Testing Authentication + +After deployment, verify: + +1. **OAuth Login:** + - Test GitHub login + - Test Google login + - Verify user profile data is correctly populated + +2. **Session Management:** + - Verify sessions persist across page reloads + - Verify logout works correctly + - Verify session data includes custom fields (`username`, `admin`) + +3. **Protected Routes:** + - Verify unauthenticated users are redirected to `/login` + - Verify authenticated users can access protected pages + - Verify admin-only pages check the `admin` flag correctly + +4. **No Redirect Flashing:** + - Verify no flash of content during authentication checks + - Verify loading states are handled properly + +## Rollback Plan + +If issues arise after deployment: + +1. Revert to the previous commit +2. Redeploy +3. Users will need to log in again (another forced logout) + +## Support + +If you encounter issues after this migration: + +- Check environment variables are correctly set +- Verify `AUTH_SECRET` is at least 32 characters +- Check browser console for auth errors +- Review server logs for authentication failures + +## References + +- [Auth.js v5 Migration Guide](https://authjs.dev/getting-started/migrating-to-v5) +- [Auth.js Documentation](https://authjs.dev/) +- [PR #988: Migrate to Auth.js v5](../pull/988) diff --git a/package.json b/package.json index 3c9c5d88..a098d2ca 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:e2e": "playwright test" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.1", "@aws-sdk/client-s3": "3.896.0", "@aws-sdk/s3-request-presigner": "3.896.0", "@codemirror/lang-cpp": "6.0.3", @@ -47,7 +48,6 @@ "@dnd-kit/utilities": "3.2.2", "@headlessui/react": "2.2.9", "@heroicons/react": "2.2.0", - "@next-auth/prisma-adapter": "1.0.7", "@next/bundle-analyzer": "15.5.5", "@prisma/client": "6.17.1", "@smithy/node-http-handler": "^4.4.5", @@ -66,7 +66,7 @@ "katex": "0.16.25", "motion": "12.23.24", "next": "15.5.9", - "next-auth": "4.24.11", + "next-auth": "5.0.0-beta.30", "next-mdx-remote": "5.0.0", "next-themes": "0.4.6", "prism-react-renderer": "1.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a837104f..e4e08ea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@auth/prisma-adapter': + specifier: ^2.11.1 + version: 2.11.1(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3)) '@aws-sdk/client-s3': specifier: 3.896.0 version: 3.896.0 @@ -53,9 +56,6 @@ importers: '@heroicons/react': specifier: 2.2.0 version: 2.2.0(react@19.2.0) - '@next-auth/prisma-adapter': - specifier: 1.0.7 - version: 1.0.7(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3))(next-auth@4.24.11(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@next/bundle-analyzer': specifier: 15.5.5 version: 15.5.5 @@ -111,8 +111,8 @@ importers: specifier: 15.5.9 version: 15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2) next-auth: - specifier: 4.24.11 - version: 4.24.11(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: 5.0.0-beta.30 + version: 5.0.0-beta.30(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0) next-mdx-remote: specifier: 5.0.0 version: 5.0.0(@types/react@19.2.2)(react@19.2.0) @@ -312,6 +312,39 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@auth/core@0.41.0': + resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/core@0.41.1': + resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^7.0.7 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/prisma-adapter@2.11.1': + resolution: {integrity: sha512-Ke7DXP0Fy0Mlmjz/ZJLXwQash2UkA4621xCM0rMtEczr1kppLc/njCbUkHkIQ/PnmILjqSPEKeTjDPsYruvkug==} + peerDependencies: + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -1115,12 +1148,6 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next-auth/prisma-adapter@1.0.7': - resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3' - next-auth: ^4 - '@next/bundle-analyzer@15.5.5': resolution: {integrity: sha512-X9tOAWrgF6rPuI++vs2xfjYPd/+XdsdJzu0rQtjFmOV5qa02uzqGutAAr+qCd0vsB5J3VmnMFfzn2/9xxmM23w==} @@ -2626,10 +2653,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3789,8 +3812,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3947,10 +3970,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -4251,16 +4270,18 @@ packages: resolution: {integrity: sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==} engines: {node: '>=10'} - next-auth@4.24.11: - resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==} + next-auth@5.0.0-beta.30: + resolution: {integrity: sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==} peerDependencies: - '@auth/core': 0.34.2 - next: ^12.2.5 || ^13 || ^14 || ^15 - nodemailer: ^6.6.5 - react: ^17.0.2 || ^18 || ^19 - react-dom: ^17.0.2 || ^18 || ^19 + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 + nodemailer: ^7.0.7 + react: ^18.2.0 || ^19.0.0 peerDependenciesMeta: - '@auth/core': + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': optional: true nodemailer: optional: true @@ -4358,17 +4379,13 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + oauth4webapi@3.8.3: + resolution: {integrity: sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -4404,10 +4421,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - oidc-token-hash@5.1.1: - resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==} - engines: {node: ^10.13.0 || >=12.0.0} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4427,9 +4440,6 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4621,13 +4631,13 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@5.2.6: - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: preact: '>=10' - preact@10.27.2: - resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -4707,9 +4717,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - prism-react-renderer@1.3.5: resolution: {integrity: sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==} peerDependencies: @@ -5548,10 +5555,6 @@ packages: deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true @@ -5763,9 +5766,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} @@ -5820,6 +5820,31 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@auth/core@0.41.0': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.1.3 + oauth4webapi: 3.8.3 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/core@0.41.1': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.1.3 + oauth4webapi: 3.8.3 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/prisma-adapter@2.11.1(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3))': + dependencies: + '@auth/core': 0.41.1 + '@prisma/client': 6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3) + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -6959,11 +6984,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next-auth/prisma-adapter@1.0.7(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3))(next-auth@4.24.11(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': - dependencies: - '@prisma/client': 6.17.1(prisma@6.17.1(magicast@0.3.5)(typescript@5.5.3))(typescript@5.5.3) - next-auth: 4.24.11(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@next/bundle-analyzer@15.5.5': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -8721,8 +8741,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie@0.7.2: {} - core-util-is@1.0.3: {} countup.js@2.8.0: {} @@ -10109,7 +10127,7 @@ snapshots: jiti@2.6.1: {} - jose@4.15.9: {} + jose@6.1.3: {} js-tokens@4.0.0: {} @@ -10274,10 +10292,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - lz-string@1.5.0: {} magic-string@0.30.19: @@ -10832,20 +10846,11 @@ snapshots: new-github-issue-url@0.2.1: {} - next-auth@4.24.11(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next-auth@5.0.0-beta.30(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0): dependencies: - '@babel/runtime': 7.28.4 - '@panva/hkdf': 1.2.1 - cookie: 0.7.2 - jose: 4.15.9 + '@auth/core': 0.41.0 next: 15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2) - oauth: 0.9.15 - openid-client: 5.7.1 - preact: 10.27.2 - preact-render-to-string: 5.2.6(preact@10.27.2) react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - uuid: 8.3.2 next-mdx-remote@5.0.0(@types/react@19.2.2)(react@19.2.0): dependencies: @@ -10942,12 +10947,10 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.1 - oauth@0.9.15: {} + oauth4webapi@3.8.3: {} object-assign@4.1.1: {} - object-hash@2.2.0: {} - object-hash@3.0.0: {} object-inspect@1.13.4: {} @@ -10992,8 +10995,6 @@ snapshots: ohash@2.0.11: {} - oidc-token-hash@5.1.1: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -11013,13 +11014,6 @@ snapshots: opener@1.5.2: {} - openid-client@5.7.1: - dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.1.1 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -11199,12 +11193,11 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact-render-to-string@5.2.6(preact@10.27.2): + preact-render-to-string@6.5.11(preact@10.24.3): dependencies: - preact: 10.27.2 - pretty-format: 3.8.0 + preact: 10.24.3 - preact@10.27.2: {} + preact@10.24.3: {} prelude-ls@1.2.1: {} @@ -11224,8 +11217,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - pretty-format@3.8.0: {} - prism-react-renderer@1.3.5(react@19.2.0): dependencies: react: 19.2.0 @@ -12309,8 +12300,6 @@ snapshots: uuid@3.4.0: {} - uuid@8.3.2: {} - uuid@9.0.0: {} v8-compile-cache-lib@3.0.1: {} @@ -12548,8 +12537,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@2.8.1: {} yn@3.1.1: {} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 7635d3f3..b2ad2472 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,7 +1,3 @@ -import NextAuth from 'next-auth' +import { handlers } from '@/lib/auth' -import { authOptions } from '@/lib/auth' - -const handler = NextAuth(authOptions) - -export { handler as GET, handler as POST } +export const { GET, POST } = handlers diff --git a/src/app/login/ErrorMessage.tsx b/src/app/login/ErrorMessage.tsx index 219bc85b..7bdceb07 100644 --- a/src/app/login/ErrorMessage.tsx +++ b/src/app/login/ErrorMessage.tsx @@ -2,9 +2,19 @@ import { useSearchParams } from 'next/navigation' -import { SignInErrorTypes } from 'next-auth/core/pages/signin' +type SignInErrorTypes = + | 'Signin' + | 'OAuthSignin' + | 'OAuthCallback' + | 'OAuthCreateAccount' + | 'EmailCreateAccount' + | 'Callback' + | 'OAuthAccountNotLinked' + | 'EmailSignin' + | 'CredentialsSignin' + | 'SessionRequired' + | 'default' -// Direct copy from https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/core/pages/signin.tsx const errors: Record = { Signin: 'Try signing in with a different account.', OAuthSignin: 'Try signing in with a different account.', diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 5e24d58e..74e77ef3 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,7 @@ -import { PrismaAdapter } from '@next-auth/prisma-adapter' -import type { NextAuthOptions } from 'next-auth' -import GithubProvider from 'next-auth/providers/github' -import GoogleProvider from 'next-auth/providers/google' +import { PrismaAdapter } from '@auth/prisma-adapter' +import NextAuth from 'next-auth' +import GitHub from 'next-auth/providers/github' +import Google from 'next-auth/providers/google' import prisma from '@/lib/prisma' @@ -9,17 +9,18 @@ if ( !process.env.GITHUB_ID || !process.env.GITHUB_SECRET || !process.env.GOOGLE_CLIENT_ID || - !process.env.GOOGLE_CLIENT_SECRET + !process.env.GOOGLE_CLIENT_SECRET || + !process.env.AUTH_SECRET ) { throw new Error( 'Failed to initialize authentication: Environment Variable Missing' ) } -export const authOptions: NextAuthOptions = { +export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [ - GithubProvider({ + GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, profile(profile) { @@ -32,7 +33,7 @@ export const authOptions: NextAuthOptions = { } } }), - GoogleProvider({ + Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, profile(profile) { @@ -61,4 +62,4 @@ export const authOptions: NextAuthOptions = { } }) } -} +}) diff --git a/src/lib/session.ts b/src/lib/session.ts index 8541e805..a9e6252b 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,9 +1,7 @@ -import { getServerSession } from 'next-auth' - -import { authOptions } from '@/lib/auth' +import { auth } from '@/lib/auth' export async function getServerUser() { - const session = await getServerSession(authOptions) + const session = await auth() return session?.user } diff --git a/src/lib/useRequireAdmin.ts b/src/lib/useRequireAdmin.ts index 24f3bc54..00aba0b0 100644 --- a/src/lib/useRequireAdmin.ts +++ b/src/lib/useRequireAdmin.ts @@ -1,25 +1,28 @@ import { useEffect } from 'react' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' function useRequireAdmin() { - const { data: session } = useSession() + const { data: session, status } = useSession() const router = useRouter() - // If auth.user is false that means we're not - // logged in and should redirect. + // Redirect based on auth status and admin role + // Wait for loading to complete to prevent redirect flashing useEffect(() => { - if (!session && typeof session != 'undefined') { - router.push(`/login`) - } else if (session && !session.user.admin) { - router.push(`/`) + if (status === 'loading') return + + if (status === 'unauthenticated') { + router.push('/login') + } else if (status === 'authenticated' && !session?.user.admin) { + router.push('/') } - }, [session, router]) + }, [status, session, router]) - return session + // Return null during loading to prevent flash of wrong content + return status === 'loading' ? null : session } export default useRequireAdmin diff --git a/src/lib/useRequireAuth.ts b/src/lib/useRequireAuth.ts index 3cf202c4..550d39fc 100644 --- a/src/lib/useRequireAuth.ts +++ b/src/lib/useRequireAuth.ts @@ -1,22 +1,26 @@ import { useEffect } from 'react' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' function useRequireAuth() { - const { data: session } = useSession() + const { data: session, status } = useSession() const router = useRouter() - // If auth.user is false that means we're not - // logged in and should redirect. + + // If not authenticated, redirect to login + // Wait for loading to complete to prevent redirect flashing useEffect(() => { - if (!session && typeof session != 'undefined') { - router.push(`/login`) + if (status === 'loading') return + + if (status === 'unauthenticated') { + router.push('/login') } - }, [session, router]) + }, [status, router]) - return session + // Return null during loading to prevent flash of wrong content + return status === 'loading' ? null : session } export default useRequireAuth diff --git a/src/middleware.ts b/src/middleware.ts index 442b147d..3d8b8657 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,14 +1,13 @@ -import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -export default function middleware(req: NextRequest) { - if ( - req.cookies.get('next-auth.session-token') || - req.cookies.get('__Secure-next-auth.session-token') - ) { +import { auth } from '@/lib/auth' + +export default auth(req => { + // Redirect authenticated users away from login page + if (req.auth) { return NextResponse.redirect(new URL('/', req.url)) } -} +}) export const config = { matcher: ['/login'] diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index d6da29e2..9748f8ac 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -1,4 +1,4 @@ -import { DefaultSession } from 'next-auth' +import 'next-auth' declare module 'next-auth' { interface Session { @@ -9,7 +9,7 @@ declare module 'next-auth' { id?: string | null username?: string | null admin?: boolean | null - } & DefaultSession['user'] + } } interface User {