mirror of
https://github.com/stackblitz/bolt.new
synced 2025-02-05 12:35:53 +00:00
feat: oauth-based login (#7)
This commit is contained in:
parent
b8a197ed16
commit
7ebc805ffa
@ -32,6 +32,12 @@ cd bolt
|
|||||||
pnpm i
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. Optionally, init git hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpmx husky
|
||||||
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
To start developing the Bolt UI:
|
To start developing the Bolt UI:
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
"playground:dev": "pnpm run --filter=playground dev",
|
"playground:dev": "pnpm run --filter=playground dev",
|
||||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"test": "pnpm run -r test",
|
"test": "pnpm run -r test",
|
||||||
"typecheck": "pnpm run -r typecheck",
|
"typecheck": "pnpm run -r typecheck"
|
||||||
"prepare": "husky"
|
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
@ -36,12 +36,11 @@ Optionally, you an set the debug level:
|
|||||||
VITE_LOG_LEVEL=debug
|
VITE_LOG_LEVEL=debug
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to test the login locally you need to add the following variables:
|
If you want to run authentication against a local StackBlitz instance, add:
|
||||||
|
|
||||||
```
|
```
|
||||||
SESSION_SECRET=XXX
|
VITE_CLIENT_ORIGIN=https://local.stackblitz.com:3000
|
||||||
LOGIN_PASSWORD=XXX
|
|
||||||
```
|
```
|
||||||
|
`
|
||||||
|
|
||||||
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ClientOnly } from 'remix-utils/client-only';
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
||||||
|
import { IconButton } from '~/components/ui/IconButton';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
return (
|
return (
|
||||||
@ -7,8 +8,11 @@ export function Header() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex gap-2">
|
||||||
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
||||||
|
<a href="/logout">
|
||||||
|
<IconButton icon="i-ph:sign-out" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@ -8,12 +8,34 @@ export function verifyPassword(password: string, cloudflareEnv: Env) {
|
|||||||
return password === loginPassword;
|
return password === loginPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleAuthRequest({ request, context }: LoaderFunctionArgs, body: object = {}) {
|
type RequestArgs = Pick<LoaderFunctionArgs, 'request' | 'context'>;
|
||||||
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV || authenticated) {
|
export async function handleAuthRequest<T extends RequestArgs>(args: T, body: object = {}) {
|
||||||
return json(body);
|
const { request, context } = args;
|
||||||
|
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
return json(body, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect('/login');
|
return redirect('/login', response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleWithAuth<T extends RequestArgs>(args: T, handler: (args: T) => Promise<Response>) {
|
||||||
|
const { request, context } = args;
|
||||||
|
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
const handlerResponse = await handler(args);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
for (const [key, value] of Object.entries(response.headers)) {
|
||||||
|
handlerResponse.headers.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({}, { status: 401 });
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,89 @@
|
|||||||
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
||||||
import { env } from 'node:process';
|
import { request as doRequest } from '~/lib/fetch';
|
||||||
|
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
||||||
|
import { logger } from '~/utils/logger';
|
||||||
|
import { decode } from 'jsonwebtoken';
|
||||||
|
|
||||||
const USER_SESSION_KEY = 'userId';
|
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
|
||||||
|
|
||||||
function createSessionStorage(cloudflareEnv: Env) {
|
interface SessionData {
|
||||||
return createCookieSessionStorage({
|
refresh: string;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isAuthenticated(request: Request, env: Env) {
|
||||||
|
const { session, sessionStorage } = await getSession(request, env);
|
||||||
|
const token = session.get('refresh');
|
||||||
|
|
||||||
|
const header = async (cookie: Promise<string>) => ({ headers: { 'Set-Cookie': await cookie } });
|
||||||
|
const destroy = () => header(sessionStorage.destroySession(session));
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
return { authenticated: false as const, response: await destroy() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresAt = session.get('expiresAt') ?? 0;
|
||||||
|
|
||||||
|
if (Date.now() < expiresAt) {
|
||||||
|
return { authenticated: true as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Awaited<ReturnType<typeof refreshToken>> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = await refreshToken(token);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
const expiresAt = cookieExpiration(data.expires_in, data.created_at);
|
||||||
|
session.set('expiresAt', expiresAt);
|
||||||
|
|
||||||
|
return { authenticated: true as const, response: await header(sessionStorage.commitSession(session)) };
|
||||||
|
} else {
|
||||||
|
return { authenticated: false as const, response: await destroy() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserSession(
|
||||||
|
request: Request,
|
||||||
|
env: Env,
|
||||||
|
tokens: { refresh: string; expires_in: number; created_at: number },
|
||||||
|
): Promise<ResponseInit> {
|
||||||
|
const { session, sessionStorage } = await getSession(request, env);
|
||||||
|
|
||||||
|
const expiresAt = cookieExpiration(tokens.expires_in, tokens.created_at);
|
||||||
|
|
||||||
|
session.set('refresh', tokens.refresh);
|
||||||
|
session.set('expiresAt', expiresAt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': await sessionStorage.commitSession(session, {
|
||||||
|
maxAge: 3600 * 24 * 30, // 1 month
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionStorage(cloudflareEnv: Env) {
|
||||||
|
return createCookieSessionStorage<SessionData>({
|
||||||
cookie: {
|
cookie: {
|
||||||
name: '__session',
|
name: '__session',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'lax',
|
secrets: [DEV_SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
|
||||||
secrets: [env.SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
|
secure: import.meta.env.PROD,
|
||||||
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) {
|
export async function logout(request: Request, env: Env) {
|
||||||
const { session, sessionStorage } = await getSession(request, env);
|
const { session, sessionStorage } = await getSession(request, env);
|
||||||
|
|
||||||
|
revokeToken(session.get('refresh'));
|
||||||
|
|
||||||
return redirect('/login', {
|
return redirect('/login', {
|
||||||
headers: {
|
headers: {
|
||||||
'Set-Cookie': await sessionStorage.destroySession(session),
|
'Set-Cookie': await sessionStorage.destroySession(session),
|
||||||
@ -33,23 +91,76 @@ export async function logout(request: Request, env: Env) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isAuthenticated(request: Request, env: Env) {
|
export function validateAccessToken(access: string) {
|
||||||
const { session } = await getSession(request, env);
|
const jwtPayload = decode(access);
|
||||||
const userId = session.get(USER_SESSION_KEY);
|
|
||||||
|
|
||||||
return !!userId;
|
const boltEnabled = typeof jwtPayload === 'object' && jwtPayload != null && jwtPayload.bolt === true;
|
||||||
|
|
||||||
|
return boltEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUserSession(request: Request, env: Env): Promise<ResponseInit> {
|
async function getSession(request: Request, env: Env) {
|
||||||
const { session, sessionStorage } = await getSession(request, env);
|
const sessionStorage = getSessionStorage(env);
|
||||||
|
const cookie = request.headers.get('Cookie');
|
||||||
|
|
||||||
session.set(USER_SESSION_KEY, 'anonymous_user');
|
return { session: await sessionStorage.getSession(cookie), sessionStorage };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
async function refreshToken(refresh: string): Promise<{ expires_in: number; created_at: number }> {
|
||||||
headers: {
|
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/token`, {
|
||||||
'Set-Cookie': await sessionStorage.commitSession(session, {
|
method: 'POST',
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 days,
|
body: urlParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, refresh_token: refresh }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unable to refresh token\n${JSON.stringify(body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { access_token: access } = body;
|
||||||
|
|
||||||
|
if (!validateAccessToken(access)) {
|
||||||
|
throw new Error('User is no longer authorized for Bolt');
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieExpiration(expireIn: number, createdAt: number) {
|
||||||
|
return (expireIn + createdAt - 10 * 60) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeToken(refresh?: string) {
|
||||||
|
if (refresh == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/revoke`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: urlParams({
|
||||||
|
token: refresh,
|
||||||
|
token_type_hint: 'refresh_token',
|
||||||
|
client_id: CLIENT_ID,
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
};
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unable to revoke token: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlParams(data: Record<string, string>) {
|
||||||
|
const encoded = new URLSearchParams();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
encoded.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoded;
|
||||||
}
|
}
|
||||||
|
4
packages/bolt/app/lib/auth.ts
Normal file
4
packages/bolt/app/lib/auth.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export function forgetAuth() {
|
||||||
|
// FIXME: use dedicated method
|
||||||
|
localStorage.removeItem('__wc_api_tokens__');
|
||||||
|
}
|
2
packages/bolt/app/lib/constants.ts
Normal file
2
packages/bolt/app/lib/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const CLIENT_ID = 'bolt';
|
||||||
|
export const CLIENT_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://stackblitz.com';
|
14
packages/bolt/app/lib/fetch.ts
Normal file
14
packages/bolt/app/lib/fetch.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
type CommonRequest = Omit<RequestInit, 'body'> & { body?: URLSearchParams };
|
||||||
|
|
||||||
|
export async function request(url: string, init?: CommonRequest) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const nodeFetch = await import('node-fetch');
|
||||||
|
const https = await import('node:https');
|
||||||
|
|
||||||
|
const agent = url.startsWith('https') ? new https.Agent({ rejectUnauthorized: false }) : undefined;
|
||||||
|
|
||||||
|
return nodeFetch.default(url, { ...init, agent });
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, init);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { WebContainer } from '@webcontainer/api';
|
import { WebContainer } from '@webcontainer/api';
|
||||||
import { WORK_DIR_NAME } from '~/utils/constants';
|
import { WORK_DIR_NAME } from '~/utils/constants';
|
||||||
|
import { forgetAuth } from '~/lib/auth';
|
||||||
|
|
||||||
interface WebContainerContext {
|
interface WebContainerContext {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
@ -21,7 +22,10 @@ if (!import.meta.env.SSR) {
|
|||||||
webcontainer =
|
webcontainer =
|
||||||
import.meta.hot?.data.webcontainer ??
|
import.meta.hot?.data.webcontainer ??
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => WebContainer.boot({ workdirName: WORK_DIR_NAME }))
|
.then(() => {
|
||||||
|
forgetAuth();
|
||||||
|
return WebContainer.boot({ workdirName: WORK_DIR_NAME });
|
||||||
|
})
|
||||||
.then((webcontainer) => {
|
.then((webcontainer) => {
|
||||||
webcontainerContext.loaded = true;
|
webcontainerContext.loaded = true;
|
||||||
return webcontainer;
|
return webcontainer;
|
||||||
|
@ -4,8 +4,13 @@ import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
|
|||||||
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
|
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
|
||||||
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
|
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
|
||||||
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
||||||
|
import { handleWithAuth } from '~/lib/.server/login';
|
||||||
|
|
||||||
export async function action({ context, request }: ActionFunctionArgs) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
|
return handleWithAuth(args, chatAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||||
const { messages } = await request.json<{ messages: Messages }>();
|
const { messages } = await request.json<{ messages: Messages }>();
|
||||||
|
|
||||||
const stream = new SwitchableStream();
|
const stream = new SwitchableStream();
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||||
import { StreamingTextResponse, parseStreamPart } from 'ai';
|
import { StreamingTextResponse, parseStreamPart } from 'ai';
|
||||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||||
|
import { handleWithAuth } from '~/lib/.server/login';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
import { stripIndents } from '~/utils/stripIndent';
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
export async function action({ context, request }: ActionFunctionArgs) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
|
return handleWithAuth(args, enhancerAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||||
const { message } = await request.json<{ message: string }>();
|
const { message } = await request.json<{ message: string }>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -3,49 +3,96 @@ import {
|
|||||||
redirect,
|
redirect,
|
||||||
type ActionFunctionArgs,
|
type ActionFunctionArgs,
|
||||||
type LoaderFunctionArgs,
|
type LoaderFunctionArgs,
|
||||||
type TypedResponse,
|
redirectDocument,
|
||||||
} from '@remix-run/cloudflare';
|
} from '@remix-run/cloudflare';
|
||||||
import { Form, useActionData } from '@remix-run/react';
|
import { useFetcher, useLoaderData } from '@remix-run/react';
|
||||||
import { verifyPassword } from '~/lib/.server/login';
|
import { auth, type AuthAPI } from '@webcontainer/api';
|
||||||
import { createUserSession, isAuthenticated } from '~/lib/.server/sessions';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
||||||
interface Errors {
|
import { request as doRequest } from '~/lib/fetch';
|
||||||
password?: string;
|
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
||||||
}
|
import { logger } from '~/utils/logger';
|
||||||
|
|
||||||
export async function loader({ request, context }: LoaderFunctionArgs) {
|
export async function loader({ request, context }: LoaderFunctionArgs) {
|
||||||
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
||||||
|
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
return redirect('/');
|
return redirect('/', response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({});
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
redirected: url.searchParams.has('code') || url.searchParams.has('error'),
|
||||||
|
},
|
||||||
|
response,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request, context }: ActionFunctionArgs): Promise<TypedResponse<{ errors?: Errors }>> {
|
export async function action({ request, context }: ActionFunctionArgs) {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const password = String(formData.get('password'));
|
|
||||||
|
|
||||||
const errors: Errors = {};
|
const payload = {
|
||||||
|
access: String(formData.get('access')),
|
||||||
|
refresh: String(formData.get('refresh')),
|
||||||
|
};
|
||||||
|
|
||||||
if (!password) {
|
let response: Awaited<ReturnType<typeof doRequest>> | undefined;
|
||||||
errors.password = 'Please provide a password';
|
|
||||||
|
try {
|
||||||
|
response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, {
|
||||||
|
headers: { authorization: `Bearer ${payload.access}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw await response.json();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Authentication failure');
|
||||||
|
logger.warn(error);
|
||||||
|
|
||||||
|
return json({ error: 'invalid-token' as const }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyPassword(password, context.cloudflare.env)) {
|
const boltEnabled = validateAccessToken(payload.access);
|
||||||
errors.password = 'Invalid password';
|
|
||||||
|
if (!boltEnabled) {
|
||||||
|
return json({ error: 'bolt-access' as const }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(errors).length > 0) {
|
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
||||||
return json({ errors });
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect('/', await createUserSession(request, context.cloudflare.env));
|
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo });
|
||||||
|
|
||||||
|
return redirectDocument('/', init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginState =
|
||||||
|
| {
|
||||||
|
kind: 'error';
|
||||||
|
error: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
| { kind: 'pending' };
|
||||||
|
|
||||||
|
const ERRORS = {
|
||||||
|
'bolt-access': 'You do not have access to Bolt.',
|
||||||
|
'invalid-token': 'Authentication failed.',
|
||||||
|
};
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const actionData = useActionData<typeof action>();
|
const { redirected } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!import.meta.hot?.data.wcAuth) {
|
||||||
|
auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.data.wcAuth = true;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@ -53,38 +100,93 @@ export default function Login() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
||||||
</div>
|
</div>
|
||||||
<Form className="mt-8 space-y-6" method="post" noValidate>
|
|
||||||
<div>
|
{redirected ? 'Processing auth...' : <LoginForm />}
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LoginForm() {
|
||||||
|
const [login, setLogin] = useState<LoginState | null>(null);
|
||||||
|
|
||||||
|
const fetcher = useFetcher<typeof action>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
auth.logout({ ignoreRevokeError: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetcher.data?.error) {
|
||||||
|
auth.logout({ ignoreRevokeError: true });
|
||||||
|
|
||||||
|
setLogin({
|
||||||
|
kind: 'error' as const,
|
||||||
|
...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetcher.data]);
|
||||||
|
|
||||||
|
async function attemptLogin() {
|
||||||
|
startAuthFlow();
|
||||||
|
|
||||||
|
function startAuthFlow() {
|
||||||
|
auth.startAuthFlow({ popup: true });
|
||||||
|
|
||||||
|
Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => {
|
||||||
|
if (error) {
|
||||||
|
setLogin({ kind: 'error', ...error });
|
||||||
|
} else {
|
||||||
|
onTokens();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTokens() {
|
||||||
|
const tokens = auth.tokens()!;
|
||||||
|
|
||||||
|
fetcher.submit(tokens, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
setLogin({ kind: 'pending' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
onClick={attemptLogin}
|
||||||
|
disabled={login?.kind === 'pending'}
|
||||||
|
>
|
||||||
|
{login?.kind === 'pending' ? 'Authenticating...' : 'Continue with StackBlitz'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{login?.kind === 'error' && (
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<code>{login.error}</code>
|
||||||
|
</h2>
|
||||||
|
<p>{login.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthError {
|
||||||
|
error: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function authEvent(auth: AuthAPI, event: 'logged-out'): Promise<void>;
|
||||||
|
function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise<AuthError>;
|
||||||
|
function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const unsubscribe = auth.on(event as any, (arg: any) => {
|
||||||
|
unsubscribe();
|
||||||
|
resolve(arg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
10
packages/bolt/app/routes/logout.tsx
Normal file
10
packages/bolt/app/routes/logout.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||||
|
import { logout } from '~/lib/.server/sessions';
|
||||||
|
|
||||||
|
export async function loader({ request, context }: LoaderFunctionArgs) {
|
||||||
|
return logout(request, context.cloudflare.env);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Logout() {
|
||||||
|
return '';
|
||||||
|
}
|
@ -11,7 +11,7 @@ interface Logger {
|
|||||||
setLevel: (level: DebugLevel) => void;
|
setLevel: (level: DebugLevel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? 'warn';
|
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
|
||||||
|
|
||||||
export const logger: Logger = {
|
export const logger: Logger = {
|
||||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"framer-motion": "^11.2.12",
|
"framer-motion": "^11.2.12",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
"istextorbinary": "^9.5.0",
|
"istextorbinary": "^9.5.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -64,9 +65,11 @@
|
|||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@remix-run/dev": "^2.10.0",
|
"@remix-run/dev": "^2.10.0",
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unocss": "^0.61.3",
|
"unocss": "^0.61.3",
|
||||||
|
141
pnpm-lock.yaml
141
pnpm-lock.yaml
@ -131,6 +131,9 @@ importers:
|
|||||||
istextorbinary:
|
istextorbinary:
|
||||||
specifier: ^9.5.0
|
specifier: ^9.5.0
|
||||||
version: 9.5.0
|
version: 9.5.0
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.2
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.10.3
|
specifier: ^0.10.3
|
||||||
version: 0.10.3
|
version: 0.10.3
|
||||||
@ -174,6 +177,9 @@ importers:
|
|||||||
'@types/diff':
|
'@types/diff':
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.6
|
||||||
|
version: 9.0.6
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.20
|
specifier: ^18.2.20
|
||||||
version: 18.3.3
|
version: 18.3.3
|
||||||
@ -183,6 +189,9 @@ importers:
|
|||||||
fast-glob:
|
fast-glob:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
node-fetch:
|
||||||
|
specifier: ^3.3.2
|
||||||
|
version: 3.3.2
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.2
|
specifier: ^5.5.2
|
||||||
version: 5.5.2
|
version: 5.5.2
|
||||||
@ -1484,6 +1493,9 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.6':
|
||||||
|
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
||||||
|
|
||||||
'@types/mdast@3.0.15':
|
'@types/mdast@3.0.15':
|
||||||
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
|
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
|
||||||
|
|
||||||
@ -1949,6 +1961,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@ -2234,6 +2249,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
data-uri-to-buffer@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
date-fns@3.6.0:
|
date-fns@3.6.0:
|
||||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||||
|
|
||||||
@ -2353,6 +2372,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
editions@6.21.0:
|
editions@6.21.0:
|
||||||
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -2615,6 +2637,10 @@ packages:
|
|||||||
fault@2.0.1:
|
fault@2.0.1:
|
||||||
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@ -2653,6 +2679,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
engines: {node: '>=0.4.x'}
|
||||||
|
|
||||||
|
formdata-polyfill@4.0.10:
|
||||||
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -3144,6 +3174,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||||
engines: {'0': node >= 0.2.0}
|
engines: {'0': node >= 0.2.0}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jwa@1.4.1:
|
||||||
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@ -3190,9 +3230,24 @@ packages:
|
|||||||
lodash.debounce@4.0.8:
|
lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
lodash.isplainobject@4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
|
||||||
lodash.kebabcase@4.1.1:
|
lodash.kebabcase@4.1.1:
|
||||||
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
|
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
|
||||||
|
|
||||||
@ -3202,6 +3257,9 @@ packages:
|
|||||||
lodash.mergewith@4.6.2:
|
lodash.mergewith@4.6.2:
|
||||||
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
|
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
|
||||||
|
|
||||||
|
lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
lodash.snakecase@4.1.1:
|
lodash.snakecase@4.1.1:
|
||||||
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
||||||
|
|
||||||
@ -3687,9 +3745,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
|
||||||
node-fetch-native@1.6.4:
|
node-fetch-native@1.6.4:
|
||||||
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
node-forge@1.3.1:
|
node-forge@1.3.1:
|
||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
@ -4679,8 +4745,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
|
|
||||||
undici@6.19.2:
|
undici@6.19.4:
|
||||||
resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==}
|
resolution: {integrity: sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
unenv-nightly@1.10.0-1717606461.a117952:
|
unenv-nightly@1.10.0-1717606461.a117952:
|
||||||
@ -6189,7 +6255,7 @@ snapshots:
|
|||||||
cookie-signature: 1.2.1
|
cookie-signature: 1.2.1
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
stream-slice: 0.1.2
|
stream-slice: 0.1.2
|
||||||
undici: 6.19.2
|
undici: 6.19.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.2
|
typescript: 5.5.2
|
||||||
|
|
||||||
@ -6201,7 +6267,7 @@ snapshots:
|
|||||||
cookie-signature: 1.2.1
|
cookie-signature: 1.2.1
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
stream-slice: 0.1.2
|
stream-slice: 0.1.2
|
||||||
undici: 6.19.2
|
undici: 6.19.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.2
|
typescript: 5.5.2
|
||||||
optional: true
|
optional: true
|
||||||
@ -6416,6 +6482,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.14.9
|
||||||
|
|
||||||
'@types/mdast@3.0.15':
|
'@types/mdast@3.0.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.10
|
'@types/unist': 2.0.10
|
||||||
@ -7091,6 +7161,8 @@ snapshots:
|
|||||||
node-releases: 2.0.14
|
node-releases: 2.0.14
|
||||||
update-browserslist-db: 1.0.16(browserslist@4.23.1)
|
update-browserslist-db: 1.0.16(browserslist@4.23.1)
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer-xor@1.0.3: {}
|
buffer-xor@1.0.3: {}
|
||||||
@ -7396,6 +7468,8 @@ snapshots:
|
|||||||
|
|
||||||
data-uri-to-buffer@3.0.1: {}
|
data-uri-to-buffer@3.0.1: {}
|
||||||
|
|
||||||
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
date-fns@3.6.0: {}
|
date-fns@3.6.0: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
@ -7490,6 +7564,10 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
editions@6.21.0:
|
editions@6.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
version-range: 4.14.0
|
version-range: 4.14.0
|
||||||
@ -7876,6 +7954,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
format: 0.2.2
|
format: 0.2.2
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 3.3.3
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
@ -7925,6 +8008,10 @@ snapshots:
|
|||||||
|
|
||||||
format@0.2.2: {}
|
format@0.2.2: {}
|
||||||
|
|
||||||
|
formdata-polyfill@4.0.10:
|
||||||
|
dependencies:
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
framer-motion@11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
framer-motion@11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
@ -8406,6 +8493,30 @@ snapshots:
|
|||||||
|
|
||||||
jsonparse@1.3.1: {}
|
jsonparse@1.3.1: {}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.6.2
|
||||||
|
|
||||||
|
jwa@1.4.1:
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@ -8444,14 +8555,26 @@ snapshots:
|
|||||||
|
|
||||||
lodash.debounce@4.0.8: {}
|
lodash.debounce@4.0.8: {}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4: {}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3: {}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1: {}
|
||||||
|
|
||||||
lodash.kebabcase@4.1.1: {}
|
lodash.kebabcase@4.1.1: {}
|
||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
lodash.mergewith@4.6.2: {}
|
lodash.mergewith@4.6.2: {}
|
||||||
|
|
||||||
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
lodash.snakecase@4.1.1: {}
|
lodash.snakecase@4.1.1: {}
|
||||||
|
|
||||||
lodash.startcase@4.4.0: {}
|
lodash.startcase@4.4.0: {}
|
||||||
@ -9317,8 +9440,16 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch-native@1.6.4: {}
|
node-fetch-native@1.6.4: {}
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 4.0.1
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-forge@1.3.1: {}
|
node-forge@1.3.1: {}
|
||||||
|
|
||||||
node-releases@2.0.14: {}
|
node-releases@2.0.14: {}
|
||||||
@ -10393,7 +10524,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/busboy': 2.1.1
|
'@fastify/busboy': 2.1.1
|
||||||
|
|
||||||
undici@6.19.2: {}
|
undici@6.19.4: {}
|
||||||
|
|
||||||
unenv-nightly@1.10.0-1717606461.a117952:
|
unenv-nightly@1.10.0-1717606461.a117952:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user