Dominic Elm
commited on
Commit
·
d2b36e8
1
Parent(s):
6927c07
feat: add login
Browse files- .github/workflows/ci.yaml +1 -1
- .tool-versions +1 -1
- README.md +1 -1
- package.json +1 -1
- packages/bolt/README.md +1 -1
- packages/bolt/app/lib/.server/login.ts +7 -0
- packages/bolt/app/lib/.server/sessions.ts +55 -0
- packages/bolt/app/routes/_index.tsx +12 -1
- packages/bolt/app/routes/login.tsx +90 -0
- packages/bolt/worker-configuration.d.ts +2 -0
- pnpm-lock.yaml +25 -3
.github/workflows/ci.yaml
CHANGED
@@ -12,7 +12,7 @@ jobs:
|
|
12 |
runs-on: ubuntu-latest
|
13 |
strategy:
|
14 |
matrix:
|
15 |
-
node-version: [
|
16 |
steps:
|
17 |
- name: Setup
|
18 |
uses: pnpm/action-setup@v4
|
|
|
12 |
runs-on: ubuntu-latest
|
13 |
strategy:
|
14 |
matrix:
|
15 |
+
node-version: [20.15.1]
|
16 |
steps:
|
17 |
- name: Setup
|
18 |
uses: pnpm/action-setup@v4
|
.tool-versions
CHANGED
@@ -1,2 +1,2 @@
|
|
1 |
-
nodejs
|
2 |
pnpm 9.4.0
|
|
|
1 |
+
nodejs 20.15.1
|
2 |
pnpm 9.4.0
|
README.md
CHANGED
@@ -14,7 +14,7 @@ As the project grows, additional packages may be added to this workspace.
|
|
14 |
|
15 |
### Prerequisites
|
16 |
|
17 |
-
- Node.js (
|
18 |
- pnpm (v9.4.0)
|
19 |
|
20 |
### Installation
|
|
|
14 |
|
15 |
### Prerequisites
|
16 |
|
17 |
+
- Node.js (v20.15.1)
|
18 |
- pnpm (v9.4.0)
|
19 |
|
20 |
### Installation
|
package.json
CHANGED
@@ -22,7 +22,7 @@
|
|
22 |
}
|
23 |
},
|
24 |
"engines": {
|
25 |
-
"node": "
|
26 |
"pnpm": "9.4.0"
|
27 |
},
|
28 |
"devDependencies": {
|
|
|
22 |
}
|
23 |
},
|
24 |
"engines": {
|
25 |
+
"node": "20.15.1",
|
26 |
"pnpm": "9.4.0"
|
27 |
},
|
28 |
"devDependencies": {
|
packages/bolt/README.md
CHANGED
@@ -6,7 +6,7 @@ Bolt is an AI assistant developed by StackBlitz. This package contains the UI in
|
|
6 |
|
7 |
Before you begin, ensure you have the following installed:
|
8 |
|
9 |
-
- Node.js (
|
10 |
- pnpm (v9.4.0)
|
11 |
|
12 |
## Setup
|
|
|
6 |
|
7 |
Before you begin, ensure you have the following installed:
|
8 |
|
9 |
+
- Node.js (v20.15.1)
|
10 |
- pnpm (v9.4.0)
|
11 |
|
12 |
## Setup
|
packages/bolt/app/lib/.server/login.ts
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { env } from 'node:process';
|
2 |
+
|
3 |
+
export function verifyPassword(password: string, cloudflareEnv: Env) {
|
4 |
+
const loginPassword = env.LOGIN_PASSWORD || cloudflareEnv.LOGIN_PASSWORD;
|
5 |
+
|
6 |
+
return password === loginPassword;
|
7 |
+
}
|
packages/bolt/app/lib/.server/sessions.ts
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
2 |
+
import { env } from 'node:process';
|
3 |
+
|
4 |
+
const USER_SESSION_KEY = 'userId';
|
5 |
+
|
6 |
+
function createSessionStorage(cloudflareEnv: Env) {
|
7 |
+
return createCookieSessionStorage({
|
8 |
+
cookie: {
|
9 |
+
name: '__session',
|
10 |
+
httpOnly: true,
|
11 |
+
path: '/',
|
12 |
+
sameSite: 'lax',
|
13 |
+
secrets: [env.SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
|
14 |
+
secure: false,
|
15 |
+
},
|
16 |
+
});
|
17 |
+
}
|
18 |
+
|
19 |
+
export async function getSession(request: Request, env: Env) {
|
20 |
+
const sessionStorage = createSessionStorage(env);
|
21 |
+
const cookie = request.headers.get('Cookie');
|
22 |
+
|
23 |
+
return { session: await sessionStorage.getSession(cookie), sessionStorage };
|
24 |
+
}
|
25 |
+
|
26 |
+
export async function logout(request: Request, env: Env) {
|
27 |
+
const { session, sessionStorage } = await getSession(request, env);
|
28 |
+
|
29 |
+
return redirect('/login', {
|
30 |
+
headers: {
|
31 |
+
'Set-Cookie': await sessionStorage.destroySession(session),
|
32 |
+
},
|
33 |
+
});
|
34 |
+
}
|
35 |
+
|
36 |
+
export async function isAuthenticated(request: Request, env: Env) {
|
37 |
+
const { session } = await getSession(request, env);
|
38 |
+
const userId = session.get(USER_SESSION_KEY);
|
39 |
+
|
40 |
+
return !!userId;
|
41 |
+
}
|
42 |
+
|
43 |
+
export async function createUserSession(request: Request, env: Env): Promise<ResponseInit> {
|
44 |
+
const { session, sessionStorage } = await getSession(request, env);
|
45 |
+
|
46 |
+
session.set(USER_SESSION_KEY, 'anonymous_user');
|
47 |
+
|
48 |
+
return {
|
49 |
+
headers: {
|
50 |
+
'Set-Cookie': await sessionStorage.commitSession(session, {
|
51 |
+
maxAge: 60 * 60 * 24 * 7, // 7 days,
|
52 |
+
}),
|
53 |
+
},
|
54 |
+
};
|
55 |
+
}
|
packages/bolt/app/routes/_index.tsx
CHANGED
@@ -1,13 +1,24 @@
|
|
1 |
-
import type
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { BaseChat } from '~/components/chat/BaseChat';
|
4 |
import { Chat } from '~/components/chat/Chat.client';
|
5 |
import { Header } from '~/components/Header';
|
|
|
6 |
|
7 |
export const meta: MetaFunction = () => {
|
8 |
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
9 |
};
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
export default function Index() {
|
12 |
return (
|
13 |
<div className="flex flex-col h-full w-full">
|
|
|
1 |
+
import { json, redirect, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare';
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { BaseChat } from '~/components/chat/BaseChat';
|
4 |
import { Chat } from '~/components/chat/Chat.client';
|
5 |
import { Header } from '~/components/Header';
|
6 |
+
import { isAuthenticated } from '~/lib/.server/sessions';
|
7 |
|
8 |
export const meta: MetaFunction = () => {
|
9 |
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
10 |
};
|
11 |
|
12 |
+
export async function loader({ request, context }: LoaderFunctionArgs) {
|
13 |
+
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
14 |
+
|
15 |
+
if (import.meta.env.DEV || authenticated) {
|
16 |
+
return json({});
|
17 |
+
}
|
18 |
+
|
19 |
+
return redirect('/login');
|
20 |
+
}
|
21 |
+
|
22 |
export default function Index() {
|
23 |
return (
|
24 |
<div className="flex flex-col h-full w-full">
|
packages/bolt/app/routes/login.tsx
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
json,
|
3 |
+
redirect,
|
4 |
+
type ActionFunctionArgs,
|
5 |
+
type LoaderFunctionArgs,
|
6 |
+
type TypedResponse,
|
7 |
+
} from '@remix-run/cloudflare';
|
8 |
+
import { Form, useActionData } from '@remix-run/react';
|
9 |
+
import { verifyPassword } from '~/lib/.server/login';
|
10 |
+
import { createUserSession, isAuthenticated } from '~/lib/.server/sessions';
|
11 |
+
|
12 |
+
interface Errors {
|
13 |
+
password?: string;
|
14 |
+
}
|
15 |
+
|
16 |
+
export async function loader({ request, context }: LoaderFunctionArgs) {
|
17 |
+
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
18 |
+
|
19 |
+
if (authenticated) {
|
20 |
+
return redirect('/');
|
21 |
+
}
|
22 |
+
|
23 |
+
return json({});
|
24 |
+
}
|
25 |
+
|
26 |
+
export async function action({ request, context }: ActionFunctionArgs): Promise<TypedResponse<{ errors?: Errors }>> {
|
27 |
+
const formData = await request.formData();
|
28 |
+
const password = String(formData.get('password'));
|
29 |
+
|
30 |
+
const errors: Errors = {};
|
31 |
+
|
32 |
+
if (!password) {
|
33 |
+
errors.password = 'Please provide a password';
|
34 |
+
}
|
35 |
+
|
36 |
+
if (!verifyPassword(password, context.cloudflare.env)) {
|
37 |
+
errors.password = 'Invalid password';
|
38 |
+
}
|
39 |
+
|
40 |
+
if (Object.keys(errors).length > 0) {
|
41 |
+
return json({ errors });
|
42 |
+
}
|
43 |
+
|
44 |
+
return redirect('/', await createUserSession(request, context.cloudflare.env));
|
45 |
+
}
|
46 |
+
|
47 |
+
export default function Login() {
|
48 |
+
const actionData = useActionData<typeof action>();
|
49 |
+
|
50 |
+
return (
|
51 |
+
<div className="min-h-screen flex items-center justify-center">
|
52 |
+
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
|
53 |
+
<div>
|
54 |
+
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
55 |
+
</div>
|
56 |
+
<Form className="mt-8 space-y-6" method="post" noValidate>
|
57 |
+
<div>
|
58 |
+
<label htmlFor="password" className="sr-only">
|
59 |
+
Password
|
60 |
+
</label>
|
61 |
+
<input
|
62 |
+
id="password"
|
63 |
+
name="password"
|
64 |
+
type="password"
|
65 |
+
autoComplete="off"
|
66 |
+
data-1p-ignore
|
67 |
+
required
|
68 |
+
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"
|
69 |
+
placeholder="Password"
|
70 |
+
/>
|
71 |
+
{actionData?.errors?.password ? (
|
72 |
+
<em className="flex items-center space-x-1.5 p-2 mt-2 bg-negative-200 text-negative-600 rounded-lg">
|
73 |
+
<div className="i-ph:x-circle text-xl"></div>
|
74 |
+
<span>{actionData?.errors.password}</span>
|
75 |
+
</em>
|
76 |
+
) : null}
|
77 |
+
</div>
|
78 |
+
<div>
|
79 |
+
<button
|
80 |
+
type="submit"
|
81 |
+
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"
|
82 |
+
>
|
83 |
+
Login
|
84 |
+
</button>
|
85 |
+
</div>
|
86 |
+
</Form>
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
);
|
90 |
+
}
|
packages/bolt/worker-configuration.d.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1 |
interface Env {
|
2 |
ANTHROPIC_API_KEY: string;
|
|
|
|
|
3 |
}
|
|
|
1 |
interface Env {
|
2 |
ANTHROPIC_API_KEY: string;
|
3 |
+
SESSION_SECRET: string;
|
4 |
+
LOGIN_PASSWORD: string;
|
5 |
}
|
pnpm-lock.yaml
CHANGED
@@ -100,7 +100,7 @@ importers:
|
|
100 |
version: 4.0.0
|
101 |
remix-utils:
|
102 |
specifier: ^7.6.0
|
103 |
-
version: 7.6.0(@remix-run/[email protected](@cloudflare/[email protected])([email protected]))(@remix-run/[email protected].
|
104 |
shiki:
|
105 |
specifier: ^1.9.1
|
106 |
version: 1.9.1
|
@@ -1102,6 +1102,15 @@ packages:
|
|
1102 |
typescript:
|
1103 |
optional: true
|
1104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1105 |
'@remix-run/[email protected]':
|
1106 |
resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
|
1107 |
engines: {node: '>=18.0.0'}
|
@@ -5558,6 +5567,19 @@ snapshots:
|
|
5558 |
optionalDependencies:
|
5559 |
typescript: 5.5.2
|
5560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5561 |
'@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected])':
|
5562 |
dependencies:
|
5563 |
'@remix-run/router': 1.17.1
|
@@ -8911,12 +8933,12 @@ snapshots:
|
|
8911 |
mdast-util-to-markdown: 2.1.0
|
8912 |
unified: 11.0.5
|
8913 |
|
8914 |
-
[email protected](@remix-run/[email protected](@cloudflare/[email protected])([email protected]))(@remix-run/[email protected].
|
8915 |
dependencies:
|
8916 |
type-fest: 4.21.0
|
8917 |
optionalDependencies:
|
8918 |
'@remix-run/cloudflare': 2.10.2(@cloudflare/[email protected])([email protected])
|
8919 |
-
'@remix-run/node': 2.10.
|
8920 |
'@remix-run/react': 2.10.2([email protected]([email protected]))([email protected])([email protected])
|
8921 |
'@remix-run/router': 1.17.1
|
8922 |
react: 18.3.1
|
|
|
100 |
version: 4.0.0
|
101 |
remix-utils:
|
102 |
specifier: ^7.6.0
|
103 |
+
version: 7.6.0(@remix-run/[email protected](@cloudflare/[email protected])([email protected]))(@remix-run/[email protected].2([email protected]))(@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected]))(@remix-run/[email protected])([email protected])([email protected])
|
104 |
shiki:
|
105 |
specifier: ^1.9.1
|
106 |
version: 1.9.1
|
|
|
1102 |
typescript:
|
1103 |
optional: true
|
1104 |
|
1105 |
+
'@remix-run/[email protected]':
|
1106 |
+
resolution: {integrity: sha512-Ni4yMQCf6avK2fz91/luuS3wnHzqtbxsdc19es1gAWEnUKfeCwqq5v1R0kzNwrXyh5NYCRhxaegzVH3tGsdYFg==}
|
1107 |
+
engines: {node: '>=18.0.0'}
|
1108 |
+
peerDependencies:
|
1109 |
+
typescript: ^5.1.0
|
1110 |
+
peerDependenciesMeta:
|
1111 |
+
typescript:
|
1112 |
+
optional: true
|
1113 |
+
|
1114 |
'@remix-run/[email protected]':
|
1115 |
resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
|
1116 |
engines: {node: '>=18.0.0'}
|
|
|
5567 |
optionalDependencies:
|
5568 |
typescript: 5.5.2
|
5569 |
|
5570 |
+
'@remix-run/[email protected]([email protected])':
|
5571 |
+
dependencies:
|
5572 |
+
'@remix-run/server-runtime': 2.10.2([email protected])
|
5573 |
+
'@remix-run/web-fetch': 4.4.2
|
5574 |
+
'@web3-storage/multipart-parser': 1.0.0
|
5575 |
+
cookie-signature: 1.2.1
|
5576 |
+
source-map-support: 0.5.21
|
5577 |
+
stream-slice: 0.1.2
|
5578 |
+
undici: 6.19.2
|
5579 |
+
optionalDependencies:
|
5580 |
+
typescript: 5.5.2
|
5581 |
+
optional: true
|
5582 |
+
|
5583 |
'@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected])':
|
5584 |
dependencies:
|
5585 |
'@remix-run/router': 1.17.1
|
|
|
8933 |
mdast-util-to-markdown: 2.1.0
|
8934 |
unified: 11.0.5
|
8935 |
|
8936 |
+
[email protected](@remix-run/[email protected](@cloudflare/[email protected])([email protected]))(@remix-run/[email protected].2([email protected]))(@remix-run/[email protected]([email protected]([email protected]))([email protected])([email protected]))(@remix-run/[email protected])([email protected])([email protected]):
|
8937 |
dependencies:
|
8938 |
type-fest: 4.21.0
|
8939 |
optionalDependencies:
|
8940 |
'@remix-run/cloudflare': 2.10.2(@cloudflare/[email protected])([email protected])
|
8941 |
+
'@remix-run/node': 2.10.2([email protected])
|
8942 |
'@remix-run/react': 2.10.2([email protected]([email protected]))([email protected])([email protected])
|
8943 |
'@remix-run/router': 1.17.1
|
8944 |
react: 18.3.1
|