feat: add login

This commit is contained in:
Dominic Elm
2024-07-11 21:25:19 +02:00
parent 6927c07451
commit d2b36e8fb2
11 changed files with 196 additions and 9 deletions

View File

@@ -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

View 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;
}

View 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,
}),
},
};
}

View File

@@ -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">

View 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>
);
}

View File

@@ -1,3 +1,5 @@
interface Env {
ANTHROPIC_API_KEY: string;
SESSION_SECRET: string;
LOGIN_PASSWORD: string;
}