mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Add login key system
This commit is contained in:
parent
38d389a42e
commit
63dcd6702e
@ -29,6 +29,7 @@ import { getCurrentMouseData } from '../workbench/PointSelector';
|
||||
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
import { shouldIncludeFile } from '~/utils/fileUtils';
|
||||
import { getNutLoginKey } from '~/lib/replay/Problems';
|
||||
|
||||
const toastAnimation = cssTransition({
|
||||
enter: 'animated fadeInRight',
|
||||
@ -334,11 +335,18 @@ export const ChatImpl = memo(
|
||||
return;
|
||||
}
|
||||
|
||||
const loginKey = getNutLoginKey();
|
||||
if (!loginKey) {
|
||||
toast.error('Please set a login key in the "User Info" settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
const anthropicApiKey = Cookies.get(anthropicApiKeyCookieName);
|
||||
if (!anthropicApiKey) {
|
||||
|
||||
if (!loginKey && !anthropicApiKey) {
|
||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||
if (numFreeUses >= MaxFreeUses) {
|
||||
toast.error('All free uses consumed. Please set an Anthropic API key in the settings.');
|
||||
toast.error('All free uses consumed. Please set a login key or Anthropic API key in the "User Info" settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -412,7 +420,7 @@ export const ChatImpl = memo(
|
||||
image: imageData,
|
||||
})),
|
||||
] as any, // Type assertion to bypass compiler check
|
||||
}, { body: { simulationEnhancedPrompt, anthropicApiKey } });
|
||||
}, { body: { simulationEnhancedPrompt, anthropicApiKey, loginKey } });
|
||||
|
||||
if (fileModifications !== undefined) {
|
||||
/**
|
||||
|
@ -2,16 +2,16 @@ import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Cookies from 'js-cookie';
|
||||
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
|
||||
import { setNutAdminKey, setProblemsUsername, getNutAdminKey, getProblemsUsername } from '~/lib/replay/Problems';
|
||||
import { saveNutLoginKey, saveProblemsUsername, getNutLoginKey, getProblemsUsername } from '~/lib/replay/Problems';
|
||||
|
||||
export default function ConnectionsTab() {
|
||||
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
|
||||
const [username, setUsername] = useState(getProblemsUsername() || '');
|
||||
const [adminKey, setAdminKey] = useState(getNutAdminKey() || '');
|
||||
const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
|
||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||
|
||||
const handleSaveAPIKey = async (key: string) => {
|
||||
if (!key || !key.startsWith('sk-ant-')) {
|
||||
if (key && !key.startsWith('sk-ant-')) {
|
||||
toast.error('Please provide a valid Anthropic API key');
|
||||
return;
|
||||
}
|
||||
@ -21,13 +21,19 @@ export default function ConnectionsTab() {
|
||||
};
|
||||
|
||||
const handleSaveUsername = async (username: string) => {
|
||||
setProblemsUsername(username);
|
||||
saveProblemsUsername(username);
|
||||
setUsername(username);
|
||||
};
|
||||
|
||||
const handleSaveAdminKey = async (key: string) => {
|
||||
setNutAdminKey(key);
|
||||
setAdminKey(key);
|
||||
const handleSaveLoginKey = async (key: string) => {
|
||||
setLoginKey(key);
|
||||
|
||||
try {
|
||||
await saveNutLoginKey(key);
|
||||
toast.success('Login key saved');
|
||||
} catch (error) {
|
||||
toast.error('Failed to save login key');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -61,13 +67,13 @@ export default function ConnectionsTab() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Admin Key</h3>
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Login Key</h3>
|
||||
<div className="flex mb-4">
|
||||
<div className="flex-1 mr-2">
|
||||
<input
|
||||
type="text"
|
||||
value={adminKey}
|
||||
onChange={(e) => handleSaveAdminKey(e.target.value)}
|
||||
value={loginKey}
|
||||
onChange={(e) => handleSaveLoginKey(e.target.value)}
|
||||
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@ import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import { SaveProblem } from './SaveProblem';
|
||||
import { SaveSolution } from './SaveSolution';
|
||||
import { hasNutAdminKey } from '~/lib/replay/Problems';
|
||||
import { getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
@ -140,7 +140,7 @@ export const Menu = () => {
|
||||
Problems
|
||||
</a>
|
||||
<SaveProblem />
|
||||
{hasNutAdminKey() && <SaveSolution />}
|
||||
{getNutIsAdmin() && <SaveSolution />}
|
||||
<a
|
||||
href="/about"
|
||||
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
|
||||
|
@ -27,6 +27,7 @@ function convertContentToAnthropic(content: any): ContentBlockParam[] {
|
||||
export interface AnthropicApiKey {
|
||||
key: string;
|
||||
isUser: boolean;
|
||||
userLoginKey?: string;
|
||||
}
|
||||
export interface AnthropicCall {
|
||||
systemPrompt: string;
|
||||
@ -52,6 +53,7 @@ const callAnthropic = wrapWithSpan(
|
||||
"llm.chat.calls": 1, // so we can SUM(llm.chat.calls) without doing a COUNT + filter
|
||||
"llm.chat.num_messages": messages.length,
|
||||
"llm.chat.is_user_api_key": apiKey.isUser,
|
||||
"llm.chat.user_login_key": apiKey.userLoginKey,
|
||||
});
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: apiKey.key });
|
||||
|
@ -111,15 +111,17 @@ export async function submitProblem(problem: BoltProblemInput): Promise<string |
|
||||
|
||||
export async function updateProblem(problemId: string, problem: BoltProblemInput) {
|
||||
try {
|
||||
const adminKey = Cookies.get(nutAdminKeyCookieName);
|
||||
if (!adminKey) {
|
||||
toast.error("Admin key not specified");
|
||||
if (!getNutIsAdmin()) {
|
||||
toast.error("Admin user required");
|
||||
return;
|
||||
}
|
||||
|
||||
const loginKey = Cookies.get(nutLoginKeyCookieName);
|
||||
await sendCommandDedicatedClient({
|
||||
method: "Recording.globalExperimentalCommand",
|
||||
params: {
|
||||
name: "updateBoltProblem",
|
||||
params: { problemId, problem, adminKey },
|
||||
params: { problemId, problem, loginKey },
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -128,18 +130,40 @@ export async function updateProblem(problemId: string, problem: BoltProblemInput
|
||||
}
|
||||
}
|
||||
|
||||
const nutAdminKeyCookieName = 'nutAdminKey';
|
||||
const nutLoginKeyCookieName = 'nutLoginKey';
|
||||
const nutIsAdminCookieName = 'nutIsAdmin';
|
||||
|
||||
export function getNutAdminKey(): string | undefined {
|
||||
return Cookies.get(nutAdminKeyCookieName);
|
||||
export function getNutLoginKey(): string | undefined {
|
||||
return Cookies.get(nutLoginKeyCookieName);
|
||||
}
|
||||
|
||||
export function hasNutAdminKey(): boolean {
|
||||
return !!getNutAdminKey();
|
||||
export function getNutIsAdmin(): boolean {
|
||||
return Cookies.get(nutIsAdminCookieName) === 'true';
|
||||
}
|
||||
|
||||
export function setNutAdminKey(key: string) {
|
||||
Cookies.set(nutAdminKeyCookieName, key);
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
loginKey: string;
|
||||
details: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
export async function saveNutLoginKey(key: string) {
|
||||
const { rval: { userInfo } } = await sendCommandDedicatedClient({
|
||||
method: "Recording.globalExperimentalCommand",
|
||||
params: {
|
||||
name: "getUserInfo",
|
||||
params: { loginKey: key },
|
||||
},
|
||||
}) as { rval: { userInfo: UserInfo } };
|
||||
console.log("UserInfo", userInfo);
|
||||
|
||||
Cookies.set(nutLoginKeyCookieName, key);
|
||||
Cookies.set(nutIsAdminCookieName, userInfo.admin ? 'true' : 'false');
|
||||
}
|
||||
|
||||
export function setNutIsAdmin(isAdmin: boolean) {
|
||||
Cookies.set(nutIsAdminCookieName, isAdmin ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const nutProblemsUsernameCookieName = 'nutProblemsUsername';
|
||||
@ -148,7 +172,7 @@ export function getProblemsUsername(): string | undefined {
|
||||
return Cookies.get(nutProblemsUsernameCookieName);
|
||||
}
|
||||
|
||||
export function setProblemsUsername(username: string) {
|
||||
export function saveProblemsUsername(username: string) {
|
||||
Cookies.set(nutProblemsUsernameCookieName, username);
|
||||
}
|
||||
|
||||
|
@ -18,12 +18,13 @@ Focus specifically on fixing this bug. Do not guess about other problems.
|
||||
async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
ensureOpenTelemetryInitialized(context);
|
||||
|
||||
const { messages, files, promptId, simulationEnhancedPrompt, anthropicApiKey: clientAnthropicApiKey } = await request.json<{
|
||||
const { messages, files, promptId, simulationEnhancedPrompt, anthropicApiKey: clientAnthropicApiKey, loginKey } = await request.json<{
|
||||
messages: Messages;
|
||||
files: FileMap;
|
||||
promptId?: string;
|
||||
simulationEnhancedPrompt?: string;
|
||||
anthropicApiKey?: string;
|
||||
loginKey?: string;
|
||||
}>();
|
||||
|
||||
let finished: (v?: any) => void;
|
||||
@ -49,6 +50,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
const anthropicApiKey: AnthropicApiKey = {
|
||||
key: apiKey,
|
||||
isUser: !!clientAnthropicApiKey,
|
||||
userLoginKey: loginKey,
|
||||
};
|
||||
|
||||
const resultStream = new ReadableStream({
|
||||
|
@ -5,11 +5,10 @@ import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { ToastContainerWrapper, Status, Keywords } from './problems';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from '@remix-run/react';
|
||||
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, hasNutAdminKey } from '~/lib/replay/Problems';
|
||||
import type { BoltProblem, BoltProblemComment, BoltProblemInput } from '~/lib/replay/Problems';
|
||||
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
|
||||
|
||||
function Comments({ comments }: { comments: BoltProblemComment[] }) {
|
||||
return (
|
||||
@ -152,6 +151,8 @@ function UpdateProblemForms({ updateProblem }: { updateProblem: UpdateProblemCal
|
||||
)
|
||||
}
|
||||
|
||||
const Nothing = () => null;
|
||||
|
||||
function ViewProblemPage() {
|
||||
const params = useParams();
|
||||
const problemId = params.id;
|
||||
@ -177,6 +178,7 @@ function ViewProblemPage() {
|
||||
}, [problemId]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Nothing />}>
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
@ -190,12 +192,13 @@ function ViewProblemPage() {
|
||||
</div>)
|
||||
: <ProblemViewer problem={problemData} />}
|
||||
</div>
|
||||
{hasNutAdminKey() && problemData && (
|
||||
{getNutIsAdmin() && problemData && (
|
||||
<UpdateProblemForms updateProblem={updateProblem} />
|
||||
)}
|
||||
<ToastContainerWrapper />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,7 @@ import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { cssTransition, ToastContainer } from 'react-toastify';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { BoltProblemStatus, listAllProblems } from '~/lib/replay/Problems';
|
||||
import type { BoltProblemDescription } from '~/lib/replay/Problems';
|
||||
|
||||
@ -91,6 +90,8 @@ function getProblemStatus(problem: BoltProblemDescription): BoltProblemStatus {
|
||||
return problem.status ?? BoltProblemStatus.Pending;
|
||||
}
|
||||
|
||||
const Nothing = () => null;
|
||||
|
||||
function ProblemsPage() {
|
||||
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState<BoltProblemStatus | 'all'>(BoltProblemStatus.Solved);
|
||||
@ -104,6 +105,7 @@ function ProblemsPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Nothing />}>
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
@ -164,6 +166,7 @@ function ProblemsPage() {
|
||||
<ToastContainerWrapper />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user