diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e1ee8e2..01c2f79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.20.3] + node-version: [20.15.1] steps: - name: Setup uses: pnpm/action-setup@v4 diff --git a/.tool-versions b/.tool-versions index c08a31d..427253d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 18.20.3 +nodejs 20.15.1 pnpm 9.4.0 diff --git a/README.md b/README.md index 80421fa..cea0485 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ As the project grows, additional packages may be added to this workspace. ### Prerequisites -- Node.js (v18.20.3) +- Node.js (v20.15.1) - pnpm (v9.4.0) ### Installation diff --git a/package.json b/package.json index a34537f..86d7378 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ } }, "engines": { - "node": ">=18.18.0", + "node": "20.15.1", "pnpm": "9.4.0" }, "devDependencies": { diff --git a/packages/bolt/README.md b/packages/bolt/README.md index 4251b95..e13b872 100644 --- a/packages/bolt/README.md +++ b/packages/bolt/README.md @@ -6,7 +6,7 @@ Bolt is an AI assistant developed by StackBlitz. This package contains the UI in Before you begin, ensure you have the following installed: -- Node.js (v18.20.3) +- Node.js (v20.15.1) - pnpm (v9.4.0) ## Setup diff --git a/packages/bolt/app/lib/.server/login.ts b/packages/bolt/app/lib/.server/login.ts new file mode 100644 index 0000000..8ea8751 --- /dev/null +++ b/packages/bolt/app/lib/.server/login.ts @@ -0,0 +1,7 @@ +import { env } from 'node:process'; + +export function verifyPassword(password: string, cloudflareEnv: Env) { + const loginPassword = env.LOGIN_PASSWORD || cloudflareEnv.LOGIN_PASSWORD; + + return password === loginPassword; +} diff --git a/packages/bolt/app/lib/.server/sessions.ts b/packages/bolt/app/lib/.server/sessions.ts new file mode 100644 index 0000000..92f875d --- /dev/null +++ b/packages/bolt/app/lib/.server/sessions.ts @@ -0,0 +1,55 @@ +import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare'; +import { env } from 'node:process'; + +const USER_SESSION_KEY = 'userId'; + +function createSessionStorage(cloudflareEnv: Env) { + return createCookieSessionStorage({ + cookie: { + name: '__session', + httpOnly: true, + path: '/', + sameSite: 'lax', + secrets: [env.SESSION_SECRET || cloudflareEnv.SESSION_SECRET], + secure: false, + }, + }); +} + +export async function getSession(request: Request, env: Env) { + const sessionStorage = createSessionStorage(env); + const cookie = request.headers.get('Cookie'); + + return { session: await sessionStorage.getSession(cookie), sessionStorage }; +} + +export async function logout(request: Request, env: Env) { + const { session, sessionStorage } = await getSession(request, env); + + return redirect('/login', { + headers: { + 'Set-Cookie': await sessionStorage.destroySession(session), + }, + }); +} + +export async function isAuthenticated(request: Request, env: Env) { + const { session } = await getSession(request, env); + const userId = session.get(USER_SESSION_KEY); + + return !!userId; +} + +export async function createUserSession(request: Request, env: Env): Promise { + const { session, sessionStorage } = await getSession(request, env); + + session.set(USER_SESSION_KEY, 'anonymous_user'); + + return { + headers: { + 'Set-Cookie': await sessionStorage.commitSession(session, { + maxAge: 60 * 60 * 24 * 7, // 7 days, + }), + }, + }; +} diff --git a/packages/bolt/app/routes/_index.tsx b/packages/bolt/app/routes/_index.tsx index 9b2c79d..e700e04 100644 --- a/packages/bolt/app/routes/_index.tsx +++ b/packages/bolt/app/routes/_index.tsx @@ -1,13 +1,24 @@ -import type { MetaFunction } from '@remix-run/cloudflare'; +import { json, redirect, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare'; import { ClientOnly } from 'remix-utils/client-only'; import { BaseChat } from '~/components/chat/BaseChat'; import { Chat } from '~/components/chat/Chat.client'; import { Header } from '~/components/Header'; +import { isAuthenticated } from '~/lib/.server/sessions'; export const meta: MetaFunction = () => { return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; }; +export async function loader({ request, context }: LoaderFunctionArgs) { + const authenticated = await isAuthenticated(request, context.cloudflare.env); + + if (import.meta.env.DEV || authenticated) { + return json({}); + } + + return redirect('/login'); +} + export default function Index() { return (
diff --git a/packages/bolt/app/routes/login.tsx b/packages/bolt/app/routes/login.tsx new file mode 100644 index 0000000..561b75f --- /dev/null +++ b/packages/bolt/app/routes/login.tsx @@ -0,0 +1,90 @@ +import { + json, + redirect, + type ActionFunctionArgs, + type LoaderFunctionArgs, + type TypedResponse, +} from '@remix-run/cloudflare'; +import { Form, useActionData } from '@remix-run/react'; +import { verifyPassword } from '~/lib/.server/login'; +import { createUserSession, isAuthenticated } from '~/lib/.server/sessions'; + +interface Errors { + password?: string; +} + +export async function loader({ request, context }: LoaderFunctionArgs) { + const authenticated = await isAuthenticated(request, context.cloudflare.env); + + if (authenticated) { + return redirect('/'); + } + + return json({}); +} + +export async function action({ request, context }: ActionFunctionArgs): Promise> { + const formData = await request.formData(); + const password = String(formData.get('password')); + + const errors: Errors = {}; + + if (!password) { + errors.password = 'Please provide a password'; + } + + if (!verifyPassword(password, context.cloudflare.env)) { + errors.password = 'Invalid password'; + } + + if (Object.keys(errors).length > 0) { + return json({ errors }); + } + + return redirect('/', await createUserSession(request, context.cloudflare.env)); +} + +export default function Login() { + const actionData = useActionData(); + + return ( +
+
+
+

Login

+
+
+
+ + + {actionData?.errors?.password ? ( + +
+ {actionData?.errors.password} +
+ ) : null} +
+
+ +
+
+
+
+ ); +} diff --git a/packages/bolt/worker-configuration.d.ts b/packages/bolt/worker-configuration.d.ts index 606a4e5..e583e01 100644 --- a/packages/bolt/worker-configuration.d.ts +++ b/packages/bolt/worker-configuration.d.ts @@ -1,3 +1,5 @@ interface Env { ANTHROPIC_API_KEY: string; + SESSION_SECRET: string; + LOGIN_PASSWORD: string; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 912a750..f2709cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: version: 4.0.0 remix-utils: specifier: ^7.6.0 - version: 7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.0(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8) + version: 7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.2(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8) shiki: specifier: ^1.9.1 version: 1.9.1 @@ -1102,6 +1102,15 @@ packages: typescript: optional: true + '@remix-run/node@2.10.2': + resolution: {integrity: sha512-Ni4yMQCf6avK2fz91/luuS3wnHzqtbxsdc19es1gAWEnUKfeCwqq5v1R0kzNwrXyh5NYCRhxaegzVH3tGsdYFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + '@remix-run/react@2.10.2': resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==} engines: {node: '>=18.0.0'} @@ -5558,6 +5567,19 @@ snapshots: optionalDependencies: typescript: 5.5.2 + '@remix-run/node@2.10.2(typescript@5.5.2)': + dependencies: + '@remix-run/server-runtime': 2.10.2(typescript@5.5.2) + '@remix-run/web-fetch': 4.4.2 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.1 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.19.2 + optionalDependencies: + typescript: 5.5.2 + optional: true + '@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)': dependencies: '@remix-run/router': 1.17.1 @@ -8911,12 +8933,12 @@ snapshots: mdast-util-to-markdown: 2.1.0 unified: 11.0.5 - remix-utils@7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.0(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8): + remix-utils@7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.2(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8): dependencies: type-fest: 4.21.0 optionalDependencies: '@remix-run/cloudflare': 2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2) - '@remix-run/node': 2.10.0(typescript@5.5.2) + '@remix-run/node': 2.10.2(typescript@5.5.2) '@remix-run/react': 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2) '@remix-run/router': 1.17.1 react: 18.3.1