fix: add missing api routes for authentication

This commit is contained in:
fanferr 2026-05-21 01:51:52 +07:00
parent cbab5166d8
commit b96eab67f3
7 changed files with 169 additions and 55 deletions

View File

@ -43,46 +43,14 @@ const StatCard = ({ title, value, icon: Icon, color }) => (
const Dashboard = () => { const Dashboard = () => {
const [athletes, setAthletes] = useState([]); const [athletes, setAthletes] = useState([]);
const [serverStatus, setServerStatus] = useState('Menghubungkan...');
const [sensorStatus, setSensorStatus] = useState('Memeriksa...'); const [sensorStatus, setSensorStatus] = useState('Memeriksa...');
const [serverColor, setServerColor] = useState('text-slate-400 bg-slate-50');
const [sensorColor, setSensorColor] = useState('text-slate-400 bg-slate-50'); const [sensorColor, setSensorColor] = useState('text-slate-400 bg-slate-50');
const [selectedAthlete, setSelectedAthlete] = useState(null); const [selectedAthlete, setSelectedAthlete] = useState(null);
const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [categoryFilter, setCategoryFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all');
// Cek status App Server (Node.js backend) via HTTP ping setiap 4 detik
// Lebih cepat dari Socket.IO ping timeout (20-30 detik)
useEffect(() => {
let pingInterval = null;
const checkServer = async () => {
try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const res = await fetch(`${API_URL}/api/athletes`, {
signal: AbortSignal.timeout(2000) // timeout 2 detik
});
if (res.ok || res.status < 500) {
setServerStatus('Online');
setServerColor('text-emerald-600 bg-emerald-50');
} else {
setServerStatus('Error');
setServerColor('text-orange-500 bg-orange-50');
}
} catch {
setServerStatus('Offline');
setServerColor('text-red-500 bg-red-50');
}
};
// Cek langsung saat komponen mount
checkServer();
// Lalu cek setiap 4 detik
pingInterval = setInterval(checkServer, 4000);
return () => clearInterval(pingInterval);
}, []);
// Cek status sensor IoT dari Firebase RTDB (timestamp heartbeat) // Cek status sensor IoT dari Firebase RTDB (timestamp heartbeat)
@ -169,17 +137,6 @@ const Dashboard = () => {
await remove(ref(rtdb, `test_history/${id}`)); await remove(ref(rtdb, `test_history/${id}`));
await remove(ref(rtdb, `attempts/${id}`)); await remove(ref(rtdb, `attempts/${id}`));
// 3. Sync dengan Backend (SQLite)
// Ini untuk membersihkan data di Leaderboard lokal
try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
await fetch(`${API_URL}/api/athletes/${id}`, {
method: 'DELETE'
});
} catch (backendErr) {
console.warn("Backend sync failed, but Firebase data was removed.", backendErr);
}
console.log(`Athlete ${id} and all associated data deleted successfully.`); console.log(`Athlete ${id} and all associated data deleted successfully.`);
} catch (err) { } catch (err) {
console.error("Failed to delete athlete and data", err); console.error("Failed to delete athlete and data", err);
@ -218,10 +175,9 @@ const Dashboard = () => {
</div> </div>
{/* Quick Stats */} {/* Quick Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<StatCard title="Total Atlet" value={athletes.length} icon={Users} color="text-blue-600 bg-blue-50" /> <StatCard title="Total Atlet" value={athletes.length} icon={Users} color="text-blue-600 bg-blue-50" />
<StatCard title="Rata-rata Umur" value={`${avgAge} th`} icon={TrendingUp} color="text-emerald-600 bg-emerald-50" /> <StatCard title="Rata-rata Umur" value={`${avgAge} th`} icon={TrendingUp} color="text-emerald-600 bg-emerald-50" />
<StatCard title="App Server" value={serverStatus} icon={Activity} color={serverColor} />
<StatCard title="Sensor IoT" value={sensorStatus} icon={Zap} color={sensorColor} /> <StatCard title="Sensor IoT" value={sensorStatus} icon={Zap} color={sensorColor} />
</div> </div>

View File

@ -26,8 +26,7 @@ const Settings = () => {
const fetchAdminUsers = async () => { const fetchAdminUsers = async () => {
try { try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; const response = await fetch(`/api/admin/users`);
const response = await fetch(`${API_URL}/api/admin/users`);
const data = await response.json(); const data = await response.json();
setAdminUsers(data); setAdminUsers(data);
} catch (error) { } catch (error) {
@ -46,8 +45,7 @@ const Settings = () => {
setStatus({ type: '', message: '' }); setStatus({ type: '', message: '' });
try { try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; const response = await fetch(`/api/admin/update-password`, {
const response = await fetch(`${API_URL}/api/admin/update-password`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -74,8 +72,7 @@ const Settings = () => {
const handleAddAdmin = async (e) => { const handleAddAdmin = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; const response = await fetch(`/api/admin/users`, {
const response = await fetch(`${API_URL}/api/admin/users`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newAdmin) body: JSON.stringify(newAdmin)
@ -95,8 +92,7 @@ const Settings = () => {
if (!confirm('Hapus admin ini?')) return; if (!confirm('Hapus admin ini?')) return;
try { try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
await fetch(`${API_URL}/api/admin/users/${id}`, { method: 'DELETE' });
fetchAdminUsers(); fetchAdminUsers();
} catch (error) { } catch (error) {
console.error('Gagal menghapus admin'); console.error('Gagal menghapus admin');

View File

@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import { rtdb } from '@/firebase';
import { ref, get } from 'firebase/database';
export async function POST(request) {
try {
const body = await request.json();
const { username, password } = body;
const adminsRef = ref(rtdb, 'admins');
const snapshot = await get(adminsRef);
if (snapshot.exists()) {
const admins = snapshot.val();
let isAuthenticated = false;
let loggedInUser = null;
for (const key in admins) {
if (admins[key].username === username && admins[key].password === password) {
isAuthenticated = true;
loggedInUser = admins[key].username;
break;
}
}
if (isAuthenticated) {
return NextResponse.json({ success: true, token: 'mock-admin-token-firebase', username: loggedInUser });
}
}
// Fallback default admin if no admins exist in Firebase yet
if (!snapshot.exists() && username === 'admin' && password === 'admin123') {
return NextResponse.json({ success: true, token: 'mock-admin-token-fallback', username: 'admin' });
}
return NextResponse.json({ success: false, message: 'Username atau Password salah' }, { status: 401 });
} catch (error) {
console.error("Login Error:", error);
return NextResponse.json({ success: false, message: 'Internal Server Error' }, { status: 500 });
}
}

View File

@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
import { rtdb } from '@/firebase';
import { ref, get, update } from 'firebase/database';
export async function POST(request) {
try {
const body = await request.json();
const { username, oldPassword, newPassword } = body;
const adminsRef = ref(rtdb, 'admins');
const snapshot = await get(adminsRef);
if (snapshot.exists()) {
const admins = snapshot.val();
let targetAdminId = null;
let isValid = false;
for (const key in admins) {
if (admins[key].username === username && admins[key].password === oldPassword) {
targetAdminId = key;
isValid = true;
break;
}
}
if (isValid && targetAdminId) {
const adminRef = ref(rtdb, `admins/${targetAdminId}`);
await update(adminRef, { password: newPassword });
return NextResponse.json({ success: true, message: 'Password berhasil diubah' });
}
}
// Mock default admin check
if (!snapshot.exists() && username === 'admin' && oldPassword === 'admin123') {
return NextResponse.json({ success: false, message: 'Harap buat akun admin baru terlebih dahulu di menu Admin' }, { status: 400 });
}
return NextResponse.json({ success: false, message: 'Password lama salah atau user tidak ditemukan' }, { status: 400 });
} catch (error) {
console.error("Update Password Error:", error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

View File

@ -0,0 +1,20 @@
import { NextResponse } from 'next/server';
import { rtdb } from '@/firebase';
import { ref, remove } from 'firebase/database';
export async function DELETE(request, { params }) {
try {
const { id } = params;
if (!id || id === 'default') {
return NextResponse.json({ error: 'Cannot delete this admin' }, { status: 400 });
}
const adminRef = ref(rtdb, `admins/${id}`);
await remove(adminRef);
return NextResponse.json({ success: true, message: 'Admin deleted' });
} catch (error) {
console.error("DELETE Admin Error:", error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

View File

@ -0,0 +1,59 @@
import { NextResponse } from 'next/server';
import { rtdb } from '@/firebase';
import { ref, get, push, set } from 'firebase/database';
// GET all admins
export async function GET() {
try {
const adminsRef = ref(rtdb, 'admins');
const snapshot = await get(adminsRef);
let adminsArray = [];
if (snapshot.exists()) {
const data = snapshot.val();
adminsArray = Object.keys(data).map(key => ({
id: key,
username: data[key].username
}));
} else {
// Default admin if empty
adminsArray = [{ id: 'default', username: 'admin' }];
}
return NextResponse.json(adminsArray);
} catch (error) {
console.error("GET Admins Error:", error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
// POST new admin
export async function POST(request) {
try {
const body = await request.json();
const { username, password } = body;
const adminsRef = ref(rtdb, 'admins');
const snapshot = await get(adminsRef);
// Check if username exists
if (snapshot.exists()) {
const admins = snapshot.val();
const exists = Object.values(admins).some(admin => admin.username === username);
if (exists) {
return NextResponse.json({ error: 'Username sudah digunakan' }, { status: 400 });
}
}
const newAdminRef = push(adminsRef);
await set(newAdminRef, {
username,
password
});
return NextResponse.json({ success: true, message: 'Admin baru ditambahkan', id: newAdminRef.key });
} catch (error) {
console.error("POST Admin Error:", error);
return NextResponse.json({ error: 'Gagal menyimpan admin' }, { status: 500 });
}
}

View File

@ -21,8 +21,7 @@ export const AuthProvider = ({ children }) => {
const login = async (username, password) => { const login = async (username, password) => {
try { try {
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; const response = await fetch(`/api/admin/login`, {
const response = await fetch(`${API_URL}/api/admin/login`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),