mirror of
				https://github.com/stackblitz-labs/bolt.diy
				synced 2025-06-26 18:26:38 +00:00 
			
		
		
		
	feat: add login
This commit is contained in:
		
							parent
							
								
									6927c07451
								
							
						
					
					
						commit
						d2b36e8fb2
					
				
							
								
								
									
										2
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@ -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
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
nodejs 18.20.3
 | 
			
		||||
nodejs 20.15.1
 | 
			
		||||
pnpm 9.4.0
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=18.18.0",
 | 
			
		||||
    "node": "20.15.1",
 | 
			
		||||
    "pnpm": "9.4.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								packages/bolt/app/lib/.server/login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/bolt/app/lib/.server/login.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								packages/bolt/app/lib/.server/sessions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								packages/bolt/app/lib/.server/sessions.ts
									
									
									
									
									
										Normal file
									
								
							@ -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<ResponseInit> {
 | 
			
		||||
  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,
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <div className="flex flex-col h-full w-full">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								packages/bolt/app/routes/login.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								packages/bolt/app/routes/login.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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<TypedResponse<{ errors?: Errors }>> {
 | 
			
		||||
  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<typeof action>();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="min-h-screen flex items-center justify-center">
 | 
			
		||||
      <div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
 | 
			
		||||
        <div>
 | 
			
		||||
          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Form className="mt-8 space-y-6" method="post" noValidate>
 | 
			
		||||
          <div>
 | 
			
		||||
            <label htmlFor="password" className="sr-only">
 | 
			
		||||
              Password
 | 
			
		||||
            </label>
 | 
			
		||||
            <input
 | 
			
		||||
              id="password"
 | 
			
		||||
              name="password"
 | 
			
		||||
              type="password"
 | 
			
		||||
              autoComplete="off"
 | 
			
		||||
              data-1p-ignore
 | 
			
		||||
              required
 | 
			
		||||
              className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none"
 | 
			
		||||
              placeholder="Password"
 | 
			
		||||
            />
 | 
			
		||||
            {actionData?.errors?.password ? (
 | 
			
		||||
              <em className="flex items-center space-x-1.5 p-2 mt-2 bg-negative-200 text-negative-600 rounded-lg">
 | 
			
		||||
                <div className="i-ph:x-circle text-xl"></div>
 | 
			
		||||
                <span>{actionData?.errors.password}</span>
 | 
			
		||||
              </em>
 | 
			
		||||
            ) : null}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              className="w-full text-white bg-accent-600 hover:bg-accent-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
 | 
			
		||||
            >
 | 
			
		||||
              Login
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/bolt/worker-configuration.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								packages/bolt/worker-configuration.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,5 @@
 | 
			
		||||
interface Env {
 | 
			
		||||
  ANTHROPIC_API_KEY: string;
 | 
			
		||||
  SESSION_SECRET: string;
 | 
			
		||||
  LOGIN_PASSWORD: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user