Dominic Elm commited on
Commit
d2b36e8
·
1 Parent(s): 6927c07

feat: add login

Browse files
.github/workflows/ci.yaml CHANGED
@@ -12,7 +12,7 @@ jobs:
12
  runs-on: ubuntu-latest
13
  strategy:
14
  matrix:
15
- node-version: [18.20.3]
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 18.20.3
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 (v18.20.3)
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": ">=18.18.0",
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 (v18.20.3)
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 { 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
 
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
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
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
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.0([email protected])
8920
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
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
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
5584
  dependencies:
5585
  '@remix-run/router': 1.17.1
 
8933
  mdast-util-to-markdown: 2.1.0
8934
  unified: 11.0.5
8935
 
8936
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
8943
  '@remix-run/router': 1.17.1
8944
  react: 18.3.1