feat: integrate Logto auth with middleware and login page (#36) #43
9 changed files with 276 additions and 0 deletions
161
web/package-lock.json
generated
161
web/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@logto/next": "^4.2.9",
|
||||
"@types/pg": "^8.20.0",
|
||||
"dotenv": "^17.4.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
|
|
@ -288,6 +289,15 @@
|
|||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@edge-runtime/cookies": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@edge-runtime/cookies/-/cookies-5.0.2.tgz",
|
||||
"integrity": "sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
|
|
@ -1911,6 +1921,53 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/client": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@logto/client/-/client-3.1.7.tgz",
|
||||
"integrity": "sha512-t/5wXMhiXtmbmP6Cmcl4uMsYetq21vSZuYZztPHXv6QX0dx7lSKBvYi/65ERoS+fmNmtV2/i4Ojf1U41o0TLPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/js": "^6.1.1",
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"jose": "^5.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/js": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@logto/js/-/js-6.1.1.tgz",
|
||||
"integrity": "sha512-G0lRS7VyOXdB06WYajEh9Kq2E3m11JshiKIKLj6LRPI1qZ06JYQ+Jsej3K60/4OIZMSzUas4FVnY+ORrhDdktA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"camelcase-keys": "^9.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/next": {
|
||||
"version": "4.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@logto/next/-/next-4.2.9.tgz",
|
||||
"integrity": "sha512-5HPt6UaQ23u+SNvmMN1Qhvxcuv9Yb7Ldz/I078Y8pLVCUWjhbgmZp/qCGg49fHIdilV3u1rjPFf5ndvj06Y8wg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@edge-runtime/cookies": "^5.0.0",
|
||||
"@logto/node": "^3.1.9",
|
||||
"cookie": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/node": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@logto/node/-/node-3.1.9.tgz",
|
||||
"integrity": "sha512-ApbUf3tWZYtMt6KJZo+bfms+5WcR7Cuz3dE9mVoPuo/joA08aU18fSe3L7VXqFu0nUJnG8BZi7ngoqCJEkQTig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/client": "^3.1.7",
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"js-base64": "^3.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
|
|
@ -2123,6 +2180,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@silverhand/essentials": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@silverhand/essentials/-/essentials-2.9.3.tgz",
|
||||
"integrity": "sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12.0",
|
||||
"pnpm": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
|
|
@ -3469,6 +3536,36 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
|
||||
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-keys": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz",
|
||||
"integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase": "^8.0.0",
|
||||
"map-obj": "5.0.0",
|
||||
"quick-lru": "^6.1.1",
|
||||
"type-fest": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001786",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
|
||||
|
|
@ -3546,6 +3643,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -5528,6 +5638,21 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "3.7.8",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
|
||||
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -5972,6 +6097,18 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/map-obj": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz",
|
||||
"integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -6654,6 +6791,18 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
|
|
@ -7971,6 +8120,18 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/next": "^4.2.9",
|
||||
"@types/pg": "^8.20.0",
|
||||
"dotenv": "^17.4.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
|
|
|
|||
17
web/src/app/api/logto/callback/route.ts
Normal file
17
web/src/app/api/logto/callback/route.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { handleSignIn } from '@logto/next/server-actions';
|
||||
import { logtoConfig } from '@/lib/logto';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
|
||||
try {
|
||||
await handleSignIn(logtoConfig, searchParams);
|
||||
redirect('/');
|
||||
} catch {
|
||||
redirect('/?error=auth');
|
||||
}
|
||||
}
|
||||
9
web/src/app/api/logto/sign-in/route.ts
Normal file
9
web/src/app/api/logto/sign-in/route.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { signIn } from '@logto/next/server-actions';
|
||||
import { logtoConfig } from '@/lib/logto';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
await signIn(logtoConfig, `${logtoConfig.baseUrl}/api/logto/callback`);
|
||||
}
|
||||
8
web/src/app/api/logto/sign-out/route.ts
Normal file
8
web/src/app/api/logto/sign-out/route.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { signOut } from '@logto/next/server-actions';
|
||||
import { logtoConfig } from '@/lib/logto';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
await signOut(logtoConfig, `${logtoConfig.baseUrl}`);
|
||||
}
|
||||
20
web/src/app/auth/page.tsx
Normal file
20
web/src/app/auth/page.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#FFF8F0]">
|
||||
<div className="text-center space-y-6 p-8">
|
||||
<h1 className="text-3xl font-bold text-[#1A1A1A]">Simpl-Liste</h1>
|
||||
<p className="text-[#6B6B6B]">
|
||||
Connectez-vous avec votre Compte Maximus
|
||||
</p>
|
||||
<Link
|
||||
href="/api/logto/sign-in"
|
||||
className="inline-block px-6 py-3 bg-[#4A90A4] text-white rounded-lg font-medium hover:bg-[#3A7389] transition-colors"
|
||||
>
|
||||
Se connecter
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
web/src/lib/auth.ts
Normal file
16
web/src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { getLogtoContext } from '@logto/next/server-actions';
|
||||
import { logtoConfig } from './logto';
|
||||
|
||||
export async function getAuthenticatedUser() {
|
||||
const context = await getLogtoContext(logtoConfig);
|
||||
|
||||
if (!context.isAuthenticated || !context.claims?.sub) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: context.claims.sub,
|
||||
email: context.claims.email,
|
||||
name: context.claims.name,
|
||||
};
|
||||
}
|
||||
11
web/src/lib/logto.ts
Normal file
11
web/src/lib/logto.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { LogtoNextConfig } from '@logto/next';
|
||||
|
||||
export const logtoConfig: LogtoNextConfig = {
|
||||
endpoint: process.env.LOGTO_ENDPOINT!,
|
||||
appId: process.env.LOGTO_APP_ID!,
|
||||
appSecret: process.env.LOGTO_APP_SECRET!,
|
||||
baseUrl: process.env.LOGTO_BASE_URL || 'http://localhost:3000',
|
||||
cookieSecret: process.env.LOGTO_COOKIE_SECRET!,
|
||||
cookieSecure: process.env.NODE_ENV === 'production',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
};
|
||||
33
web/src/middleware.ts
Normal file
33
web/src/middleware.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Let Logto API routes and health endpoint pass through
|
||||
if (pathname.startsWith('/api/logto') || pathname.startsWith('/api/health')) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Protected routes: check for Logto session cookie
|
||||
// The Logto SDK stores session data in a cookie named `logto:<appId>`
|
||||
const hasSession = request.cookies.getAll().some(
|
||||
(cookie) => cookie.name.startsWith('logto:')
|
||||
);
|
||||
|
||||
if (!hasSession && !pathname.startsWith('/auth')) {
|
||||
return NextResponse.redirect(new URL('/auth', request.url));
|
||||
}
|
||||
|
||||
if (hasSession && pathname === '/auth') {
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|api/logto|api/health).*)',
|
||||
],
|
||||
};
|
||||
Loading…
Reference in a new issue