forked from hangshuo652/aurak
feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export default function Login() {
|
||||
const { login, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||
const [loginUsername, setLoginUsername] = useState('');
|
||||
const [loginPassword, setLoginPassword] = useState('');
|
||||
const [loginMode, setLoginMode] = useState<'password' | 'apikey'>('password');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Redirect if already logged in
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (loginMode === 'apikey') {
|
||||
if (!apiKeyInput.trim()) {
|
||||
setError('API Key is required');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify API key is valid by calling the profile endpoint
|
||||
const res = await fetch('/api/users/me', {
|
||||
headers: { 'x-api-key': apiKeyInput }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const userData = await res.json();
|
||||
login(apiKeyInput, { ...userData, role: userData.role ?? 'USER' });
|
||||
} else {
|
||||
setError('Invalid API Key');
|
||||
}
|
||||
} else {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: loginUsername, password: loginPassword })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// data has { access_token, user }
|
||||
// Get the API key using the JWT token
|
||||
const keyRes = await fetch('/api/users/api-key', {
|
||||
headers: { 'Authorization': `Bearer ${data.access_token}` }
|
||||
});
|
||||
if (keyRes.ok) {
|
||||
const keyData = await keyRes.json();
|
||||
login(keyData.apiKey, { ...data.user, role: data.user.role ?? 'USER' });
|
||||
navigate('/');
|
||||
} else {
|
||||
// Fall back to using JWT access token as the key
|
||||
login(data.access_token, { ...data.user, role: data.user.role ?? 'USER' });
|
||||
navigate('/');
|
||||
}
|
||||
} else {
|
||||
setError('Invalid username or password');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An error occurred during login. Please try again.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-slate-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full max-w-md p-8 bg-white border border-slate-200 shadow-xl rounded-2xl"
|
||||
>
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="p-3 mb-4 bg-blue-600 rounded-xl">
|
||||
<BookOpen className="text-white" size={32} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">AuraK V2</h1>
|
||||
<p className="text-slate-500">Sign in to your workspace</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 p-1 mb-6 bg-slate-100 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setLoginMode('password'); setError(''); }}
|
||||
className={cn(
|
||||
"flex-1 py-1.5 text-sm font-medium rounded-md transition-all",
|
||||
loginMode === 'password' ? "bg-white shadow-sm text-blue-600" : "text-slate-500 hover:text-slate-700"
|
||||
)}
|
||||
>
|
||||
Password
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setLoginMode('apikey'); setError(''); }}
|
||||
className={cn(
|
||||
"flex-1 py-1.5 text-sm font-medium rounded-md transition-all",
|
||||
loginMode === 'apikey' ? "bg-white shadow-sm text-blue-600" : "text-slate-500 hover:text-slate-700"
|
||||
)}
|
||||
>
|
||||
API Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 text-sm text-red-600 bg-red-50 border border-red-100 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
{loginMode === 'password' ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-slate-700">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={loginUsername}
|
||||
onChange={(e) => setLoginUsername(e.target.value)}
|
||||
placeholder="admin"
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-slate-700">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-slate-700">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKeyInput}
|
||||
onChange={(e) => setApiKeyInput(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-2.5 mt-2 text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-70 disabled:cursor-not-allowed rounded-lg font-semibold transition-colors flex items-center justify-center"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { AgentsView } from '../../components/views/AgentsView';
|
||||
|
||||
const AgentsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<AgentsView />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsPage;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { AssessmentView } from '../../../components/views/AssessmentView';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
export default function AssessmentPage() {
|
||||
const { apiKey, logout, user } = useAuth();
|
||||
|
||||
return (
|
||||
<AssessmentView
|
||||
onLogout={logout}
|
||||
onNavigate={() => { }}
|
||||
isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { ChatView } from '../../../components/views/ChatView';
|
||||
import { ModelConfig, DEFAULT_MODELS } from '../../../types';
|
||||
import { modelConfigService } from '../../../services/modelConfigService';
|
||||
|
||||
export default function ChatPage() {
|
||||
const { apiKey, logout, user } = useAuth();
|
||||
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>(DEFAULT_MODELS);
|
||||
|
||||
const fetchModels = useCallback(async () => {
|
||||
if (!apiKey) return;
|
||||
try {
|
||||
const backendModels = await modelConfigService.getAll(apiKey);
|
||||
const map = new Map<string, ModelConfig>();
|
||||
DEFAULT_MODELS.forEach(m => map.set(m.id, m));
|
||||
backendModels.forEach(m => map.set(m.id, m));
|
||||
setModelConfigs(Array.from(map.values()));
|
||||
} catch {
|
||||
setModelConfigs(DEFAULT_MODELS);
|
||||
}
|
||||
}, [apiKey]);
|
||||
|
||||
useEffect(() => { fetchModels(); }, [fetchModels]);
|
||||
|
||||
return (
|
||||
<ChatView
|
||||
authToken={apiKey}
|
||||
onLogout={logout}
|
||||
modelConfigs={modelConfigs}
|
||||
onNavigate={() => { }}
|
||||
isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { KnowledgeBaseView } from '../../../components/views/KnowledgeBaseView';
|
||||
import { modelConfigService } from '../../../services/modelConfigService';
|
||||
import { ModelConfig } from '../../../types';
|
||||
|
||||
export default function KnowledgePage() {
|
||||
const { apiKey, user, logout, activeTenant } = useAuth();
|
||||
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchModels = async () => {
|
||||
if (!apiKey) return;
|
||||
try {
|
||||
const models = await modelConfigService.getAll(apiKey);
|
||||
setModelConfigs(models);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch model configs:', error);
|
||||
}
|
||||
};
|
||||
fetchModels();
|
||||
}, [apiKey]);
|
||||
|
||||
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
|
||||
|
||||
return (
|
||||
<KnowledgeBaseView
|
||||
authToken={apiKey || ''}
|
||||
onLogout={logout}
|
||||
onNavigate={() => { }}
|
||||
modelConfigs={modelConfigs}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { MemosView } from '../../../components/views/MemosView';
|
||||
|
||||
export default function MemosPage() {
|
||||
const { apiKey, user, activeTenant } = useAuth();
|
||||
|
||||
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
|
||||
|
||||
return (
|
||||
<MemosView
|
||||
authToken={apiKey}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { NotebooksView } from '../../../components/views/NotebooksView';
|
||||
|
||||
const NotebooksPage: React.FC = () => {
|
||||
const { apiKey, user, activeTenant } = useAuth()
|
||||
|
||||
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<NotebooksView
|
||||
authToken={apiKey}
|
||||
onChatWithContext={() => { }}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotebooksPage;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { PluginsView } from '../../components/views/PluginsView';
|
||||
|
||||
const PluginsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<PluginsView />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginsPage;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { SettingsView } from '../../../components/views/SettingsView';
|
||||
import { ModelConfig, DEFAULT_MODELS } from '../../../types';
|
||||
import { modelConfigService } from '../../../services/modelConfigService';
|
||||
|
||||
interface SettingsPageProps {
|
||||
initialTab?: 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base';
|
||||
}
|
||||
|
||||
export default function SettingsPage({ initialTab }: SettingsPageProps) {
|
||||
const { apiKey, user, activeTenant } = useAuth();
|
||||
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>(DEFAULT_MODELS);
|
||||
|
||||
const fetchModels = useCallback(async () => {
|
||||
if (!apiKey) return;
|
||||
try {
|
||||
const backendModels = await modelConfigService.getAll(apiKey);
|
||||
const map = new Map<string, ModelConfig>();
|
||||
DEFAULT_MODELS.forEach(m => map.set(m.id, m));
|
||||
backendModels.forEach(m => map.set(m.id, m));
|
||||
setModelConfigs(Array.from(map.values()));
|
||||
} catch {
|
||||
setModelConfigs(DEFAULT_MODELS);
|
||||
}
|
||||
}, [apiKey]);
|
||||
|
||||
useEffect(() => { fetchModels(); }, [fetchModels]);
|
||||
|
||||
const handleUpdateModels = useCallback(async (action: 'create' | 'update' | 'delete', model: ModelConfig) => {
|
||||
if (!apiKey) return;
|
||||
const { id, ...data } = model;
|
||||
if (action === 'create') await modelConfigService.create(apiKey, data);
|
||||
else if (action === 'update') await modelConfigService.update(apiKey, id, data);
|
||||
else if (action === 'delete') await modelConfigService.remove(apiKey, id);
|
||||
await fetchModels();
|
||||
}, [apiKey, fetchModels]);
|
||||
|
||||
return (
|
||||
<SettingsView
|
||||
models={modelConfigs}
|
||||
onUpdateModels={handleUpdateModels}
|
||||
authToken={apiKey}
|
||||
isAdmin={user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN'}
|
||||
currentUser={user}
|
||||
initialTab={initialTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user