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:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
+178
View File
@@ -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>
);
}