diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0a98352 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@/auth'; + +export const { GET, POST } = handlers; diff --git a/app/globals.css b/app/globals.css index 1ebf9b5..4074452 100644 --- a/app/globals.css +++ b/app/globals.css @@ -302,3 +302,32 @@ header select option { text-align: left; } .content th { color: var(--green); background: #111; } + +/* ── login page ── */ +.login-page { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.login-box { + border: 1px solid var(--border); + padding: 40px 48px; + text-align: center; +} + +.login-box button { + background: transparent; + color: var(--green); + border: 1px solid var(--green); + font-family: inherit; + font-size: 14px; + padding: 8px 16px; + cursor: pointer; +} + +.login-box button:hover { + background: var(--green); + color: var(--bg); +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..1105711 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,17 @@ +import { signIn } from '@/auth'; + +export default function LoginPage() { + return ( +
+
+
GIPPIDY
+
{ + 'use server'; + await signIn('google', { redirectTo: '/' }); + }}> + +
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 0b85a51..f0ee49e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; +import { signOut } from 'next-auth/react'; import { renderMarkdown } from '@/lib/markdown'; type Role = 'user' | 'assistant'; @@ -148,6 +149,7 @@ export default function Home() { {messages.length > 0 && ( )} + {showSettings && ( diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..f1ce05c --- /dev/null +++ b/auth.ts @@ -0,0 +1,16 @@ +import NextAuth from 'next-auth'; +import Google from 'next-auth/providers/google'; + +export const { handlers, auth, signIn, signOut } = NextAuth({ + providers: [ + Google({ + clientId: process.env.GOOGLE_ID!, + clientSecret: process.env.GOOGLE_SECRET!, + }), + ], + callbacks: { + signIn({ user }) { + return user.email === process.env.ALLOWED_EMAIL; + }, + }, +}); diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..fbf51da --- /dev/null +++ b/middleware.ts @@ -0,0 +1,15 @@ +import { auth } from '@/auth'; +import { NextResponse } from 'next/server'; + +export default auth((req) => { + if (!req.auth) { + if (req.nextUrl.pathname.startsWith('/api/')) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + return NextResponse.redirect(new URL('/login', req.url)); + } +}); + +export const config = { + matcher: ['/((?!login|api/auth|_next/static|_next/image|favicon.ico).*)'], +}; diff --git a/package.json b/package.json index 6f4f680..6bba850 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "marked": "^17.0.4", "marked-highlight": "^2.2.3", "next": "^15.2.3", + "next-auth": "5.0.0-beta.30", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ceef1e..2923034 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: next: specifier: ^15.2.3 version: 15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-auth: + specifier: 5.0.0-beta.30 + version: 5.0.0-beta.30(next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react: specifier: ^19.0.0 version: 19.2.4 @@ -42,6 +45,20 @@ importers: packages: + '@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 + '@emnapi/runtime@1.9.0': resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} @@ -233,6 +250,9 @@ packages: cpu: [x64] os: [win32] + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -264,6 +284,9 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + marked-highlight@2.2.3: resolution: {integrity: sha512-FCfZRxW/msZAiasCML4isYpxyQWKEEx44vOgdn5Kloae+Qc3q4XR7WjpKKf8oMLk7JP9ZCRd2vhtclJFdwxlWQ==} peerDependencies: @@ -279,6 +302,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next-auth@5.0.0-beta.30: + resolution: {integrity: sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==} + peerDependencies: + '@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: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + next@15.5.12: resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -300,6 +339,9 @@ packages: sass: optional: true + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -307,6 +349,14 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -358,6 +408,14 @@ packages: snapshots: + '@auth/core@0.41.0': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.2.1 + oauth4webapi: 3.8.5 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + '@emnapi/runtime@1.9.0': dependencies: tslib: 2.8.1 @@ -486,6 +544,8 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.12': optional: true + '@panva/hkdf@1.2.1': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -513,6 +573,8 @@ snapshots: highlight.js@11.11.1: {} + jose@6.2.1: {} + marked-highlight@2.2.3(marked@17.0.4): dependencies: marked: 17.0.4 @@ -521,6 +583,12 @@ snapshots: nanoid@3.3.11: {} + next-auth@5.0.0-beta.30(next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + '@auth/core': 0.41.0 + next: 15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 @@ -544,6 +612,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + oauth4webapi@3.8.5: {} + picocolors@1.1.1: {} postcss@8.4.31: @@ -552,6 +622,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4