mirror of
https://github.com/stackblitz/bolt.new
synced 2025-02-05 12:35:53 +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
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.20.3]
|
node-version: [20.15.1]
|
||||||
steps:
|
steps:
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
nodejs 18.20.3
|
nodejs 20.15.1
|
||||||
pnpm 9.4.0
|
pnpm 9.4.0
|
||||||
|
@ -14,7 +14,7 @@ As the project grows, additional packages may be added to this workspace.
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js (v18.20.3)
|
- Node.js (v20.15.1)
|
||||||
- pnpm (v9.4.0)
|
- pnpm (v9.4.0)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0",
|
"node": "20.15.1",
|
||||||
"pnpm": "9.4.0"
|
"pnpm": "9.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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:
|
Before you begin, ensure you have the following installed:
|
||||||
|
|
||||||
- Node.js (v18.20.3)
|
- Node.js (v20.15.1)
|
||||||
- pnpm (v9.4.0)
|
- pnpm (v9.4.0)
|
||||||
|
|
||||||
## Setup
|
## 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 { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { BaseChat } from '~/components/chat/BaseChat';
|
import { BaseChat } from '~/components/chat/BaseChat';
|
||||||
import { Chat } from '~/components/chat/Chat.client';
|
import { Chat } from '~/components/chat/Chat.client';
|
||||||
import { Header } from '~/components/Header';
|
import { Header } from '~/components/Header';
|
||||||
|
import { isAuthenticated } from '~/lib/.server/sessions';
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
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() {
|
export default function Index() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full">
|
<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 {
|
interface Env {
|
||||||
ANTHROPIC_API_KEY: string;
|
ANTHROPIC_API_KEY: string;
|
||||||
|
SESSION_SECRET: string;
|
||||||
|
LOGIN_PASSWORD: string;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ importers:
|
|||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
remix-utils:
|
remix-utils:
|
||||||
specifier: ^7.6.0
|
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:
|
shiki:
|
||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.1
|
version: 1.9.1
|
||||||
@ -1102,6 +1102,15 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
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':
|
'@remix-run/react@2.10.2':
|
||||||
resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
|
resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -5558,6 +5567,19 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.2
|
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)':
|
'@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@remix-run/router': 1.17.1
|
'@remix-run/router': 1.17.1
|
||||||
@ -8911,12 +8933,12 @@ snapshots:
|
|||||||
mdast-util-to-markdown: 2.1.0
|
mdast-util-to-markdown: 2.1.0
|
||||||
unified: 11.0.5
|
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:
|
dependencies:
|
||||||
type-fest: 4.21.0
|
type-fest: 4.21.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@remix-run/cloudflare': 2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2)
|
'@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/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
|
'@remix-run/router': 1.17.1
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
Loading…
Reference in New Issue
Block a user