fix: add missing api routes for authentication
This commit is contained in:
parent
cbab5166d8
commit
b96eab67f3
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue