Membuat Aplikasi Soal dengan Node.Js sebagai backend dan HTML,CSS dan Javascript sebagai frontend
🗃️ Langkah 1: Pembaruan Database MySQL
Kita perlu menambahkan tabel untuk admin dan kolom skor di tabel siswa.
Tambahkan Tabel admins:
Jalankan kueri ini untuk membuat tabel yang akan menyimpan username dan password admin. Password akan di-hash demi keamanan.
SQLUSE db_aplikasi_soal; CREATE TABLE admins ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL );
Buat Contoh Admin:
Kita akan membuat satu admin dengan username admin dan password admin123. Nanti, di Node.js, password ini akan kita hash.
SQL-- Password 'admin123' akan di-hash oleh BCRYPT di server nanti INSERT INTO admins (username, password) VALUES ('admin', 'admin123');
Tambahkan Kolom score di Tabel students:
Kolom ini akan menyimpan nilai akhir siswa.
SQLALTER TABLE students ADD COLUMN score DECIMAL(5, 2) DEFAULT 0.00;
Database Anda sekarang siap untuk fitur-fitur baru.
⚙️ Langkah 2: Pembaruan Backend (Node.js)
Ini adalah bagian terbesarnya. Kita akan menginstal library baru dan memodifikasi server.js
secara signifikan.
Install Library Baru:
Buka terminal di folder proyek Anda dan jalankan:
Bashnpm install jsonwebtoken bcryptjs
jsonwebtoken
: Untuk membuat token otentikasi (JWT).bcryptjs
: Untuk mengenkripsi (hash) password admin.
Update File server.js:
Ganti seluruh isi file server.js Anda dengan kode di bawah ini. Kode ini sudah ditambahkan komentar untuk menjelaskan setiap bagian baru.
JavaScriptconst express = require('express'); const mysql = require('mysql2'); const cors = require('cors'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const app = express(); const port = 3000; const JWT_SECRET = 'rahasia-banget-jangan-disebar'; // Ganti dengan secret key yang lebih kompleks // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Koneksi Database const db = mysql.createConnection({ host: 'localhost', user: 'root', password: '', database: 'db_aplikasi_soal' }).promise(); // (BARU) Fungsi untuk Hash Password Admin saat pertama kali dijalankan async function hashInitialPassword() { try { const [admins] = await db.query("SELECT * FROM admins WHERE username = 'admin'"); if (admins.length > 0 && !admins[0].password.startsWith('$2a$')) { const hashedPassword = await bcrypt.hash(admins[0].password, 10); await db.query("UPDATE admins SET password = ? WHERE username = 'admin'", [hashedPassword]); console.log('Password admin awal berhasil di-hash.'); } } catch (err) { console.error('Gagal hash password awal:', err); } } // (BARU) Middleware untuk verifikasi token JWT function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN if (token == null) return res.sendStatus(401); // Unauthorized jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); // Forbidden req.user = user; next(); }); } // --- API ENDPOINTS --- // === ENDPOINT PUBLIK (UNTUK SISWA) === // 1. Mengambil soal untuk dikerjakan siswa app.get('/api/questions', async (req, res) => { try { const [rows] = await db.query('SELECT id, question_text, option_a, option_b, option_c FROM questions'); res.json(rows); } catch (err) { res.status(500).json({ message: 'Gagal mengambil soal', error: err }); } }); // 2. Mengirim jawaban dan menghitung skor (DIMODIFIKASI) app.post('/api/submit', async (req, res) => { const { name, answers } = req.body; if (!name || !answers) { return res.status(400).json({ message: 'Nama dan jawaban tidak boleh kosong' }); } try { const [correctAnswers] = await db.query('SELECT id, correct_answer FROM questions'); const correctAnswersMap = correctAnswers.reduce((map, item) => { map[item.id] = item.correct_answer; return map; }, {}); let score = 0; answers.forEach(answer => { if (correctAnswersMap[answer.questionId] === answer.answer) { score++; } }); const totalQuestions = correctAnswers.length; const finalScore = (score / totalQuestions) * 100; const [studentResult] = await db.query('INSERT INTO students (name, score) VALUES (?, ?)', [name, finalScore]); const studentId = studentResult.insertId; const answerPromises = answers.map(answer => { const isCorrect = correctAnswersMap[answer.questionId] === answer.answer; return db.query( 'INSERT INTO answers (student_id, question_id, student_answer, is_correct) VALUES (?, ?, ?, ?)', [studentId, answer.questionId, answer.answer, isCorrect] ); }); await Promise.all(answerPromises); res.status(201).json({ message: 'Jawaban berhasil disimpan!', score: finalScore }); } catch (err) { res.status(500).json({ message: 'Gagal menyimpan jawaban', error: err }); } }); // === ENDPOINT ADMIN (DIPROTEKSI) === // 3. Login Admin (BARU) app.post('/api/admin/login', async (req, res) => { const { username, password } = req.body; try { const [admins] = await db.query('SELECT * FROM admins WHERE username = ?', [username]); if (admins.length === 0) { return res.status(401).json({ message: 'Username atau password salah' }); } const admin = admins[0]; const isPasswordValid = await bcrypt.compare(password, admin.password); if (!isPasswordValid) { return res.status(401).json({ message: 'Username atau password salah' }); } const token = jwt.sign({ id: admin.id, username: admin.username }, JWT_SECRET, { expiresIn: '8h' }); res.json({ message: 'Login berhasil', token }); } catch (err) { res.status(500).json({ message: 'Server error', error: err }); } }); // 4. Melihat hasil jawaban (DIMODIFIKASI, ditambahkan skor dan proteksi) app.get('/api/results', authenticateToken, async (req, res) => { try { const query = ` SELECT s.name AS student_name, s.score, q.question_text, a.student_answer, q.correct_answer, a.is_correct FROM answers a JOIN students s ON a.student_id = s.id JOIN questions q ON a.question_id = q.id ORDER BY s.id DESC, q.id; `; const [results] = await db.query(query); res.json(results); } catch (err) { res.status(500).json({ message: 'Gagal mengambil hasil', error: err }); } }); // 5. Mengambil semua soal untuk halaman manajemen (BARU) app.get('/api/questions/manage', authenticateToken, async (req, res) => { try { const [rows] = await db.query('SELECT * FROM questions'); res.json(rows); } catch (err) { res.status(500).json({ message: 'Gagal mengambil soal', error: err }); } }); // 6. Menambah soal baru (BARU) app.post('/api/questions', authenticateToken, async (req, res) => { const { question_text, option_a, option_b, option_c, correct_answer } = req.body; try { await db.query( 'INSERT INTO questions (question_text, option_a, option_b, option_c, correct_answer) VALUES (?, ?, ?, ?, ?)', [question_text, option_a, option_b, option_c, correct_answer] ); res.status(201).json({ message: 'Soal berhasil ditambahkan' }); } catch (err) { res.status(500).json({ message: 'Gagal menambah soal', error: err }); } }); // 7. Mengupdate soal (BARU) app.put('/api/questions/:id', authenticateToken, async (req, res) => { const { id } = req.params; const { question_text, option_a, option_b, option_c, correct_answer } = req.body; try { await db.query( 'UPDATE questions SET question_text = ?, option_a = ?, option_b = ?, option_c = ?, correct_answer = ? WHERE id = ?', [question_text, option_a, option_b, option_c, correct_answer, id] ); res.json({ message: 'Soal berhasil diperbarui' }); } catch (err) { res.status(500).json({ message: 'Gagal memperbarui soal', error: err }); } }); // 8. Menghapus soal (BARU) app.delete('/api/questions/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { // Hapus dulu jawaban terkait soal ini agar tidak error await db.query('DELETE FROM answers WHERE question_id = ?', [id]); await db.query('DELETE FROM questions WHERE id = ?', [id]); res.json({ message: 'Soal berhasil dihapus' }); } catch (err) { res.status(500).json({ message: 'Gagal menghapus soal', error: err }); } }); app.listen(port, async () => { await hashInitialPassword(); console.log(`🚀 Server berjalan di http://localhost:${port}`); });
🖥️ Langkah 3: Pembaruan Frontend
Kita akan membuat file HTML & JS baru dan memodifikasi yang sudah ada.
A. Halaman Login Admin
Buat file
public/login.html
:HTML<!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <title>Admin Login</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container" style="max-width: 400px;"> <h2>Admin Login</h2> <form id="login-form"> <label for="username">Username:</label> <input type="text" id="username" required> <label for="password">Password:</label> <input type="password" id="password" required> <button type="submit">Login</button> </form> <p id="message" style="text-align: center; margin-top: 15px;"></p> </div> <script src="login.js"></script> </body> </html>
Buat file
public/login.js
:JavaScriptdocument.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const messageEl = document.getElementById('message'); try { const response = await fetch('http://localhost:3000/api/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const result = await response.json(); if (response.ok) { // Simpan token di localStorage localStorage.setItem('authToken', result.token); messageEl.textContent = 'Login berhasil! Mengalihkan...'; messageEl.style.color = 'green'; // Arahkan ke halaman admin setelah 1 detik setTimeout(() => { window.location.href = '/admin.html'; }, 1000); } else { messageEl.textContent = result.message; messageEl.style.color = 'red'; } } catch (error) { messageEl.textContent = 'Gagal terhubung ke server.'; messageEl.style.color = 'red'; } });
B. Halaman Soal Siswa (Update Skor)
Modifikasi public/script.js:
Ubah eventListener untuk submit agar bisa menampilkan skor.
JavaScript// ... (kode loadQuestions tetap sama) ... // Ganti event listener yang lama dengan ini quizForm.addEventListener('submit', async (e) => { e.preventDefault(); // ... (kode untuk mengambil nama dan jawaban tetap sama) ... const name = document.getElementById('studentName').value; const answers = []; const optionsDivs = document.querySelectorAll('.options'); optionsDivs.forEach(div => { const questionId = div.dataset.questionId; const selectedOption = div.querySelector(`input[name="question${questionId}"]:checked`); if (selectedOption) { answers.push({ questionId: parseInt(questionId), answer: selectedOption.value }); } }); if (answers.length !== optionsDivs.length) { messageEl.textContent = 'Harap jawab semua pertanyaan!'; messageEl.style.color = 'red'; return; } try { const response = await fetch('http://localhost:3000/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, answers }) }); const result = await response.json(); if (response.ok) { // Tampilkan skor! quizContainer.innerHTML = ` <h2>Terima Kasih, ${name}!</h2> <h3>Skor Anda: ${result.score.toFixed(2)}</h3> <p>Jawaban Anda telah berhasil disimpan.</p> `; quizForm.style.display = 'none'; // Sembunyikan form messageEl.textContent = ''; } else { messageEl.textContent = result.message; messageEl.style.color = 'red'; } } catch (error) { messageEl.textContent = 'Terjadi kesalahan saat mengirim jawaban.'; messageEl.style.color = 'red'; } }); loadQuestions();
C. Halaman Hasil Admin (Update Skor & Navigasi)
Modifikasi public/admin.html:
Tambahkan kolom skor, link ke halaman manajemen soal, dan tombol logout.
HTML<!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <title>Hasil Jawaban Siswa</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <div style="display: flex; justify-content: space-between; align-items: center;"> <h1>Laporan Hasil Siswa</h1> <div> <a href="/manage-questions.html" style="margin-right: 15px;">Kelola Soal</a> <button id="logout-btn" style="width: auto; padding: 8px 15px; background-color: #dc3545;">Logout</button> </div> </div> <table id="results-table"> <thead> <tr> <th>Nama Siswa</th> <th>Skor Akhir</th> </tr> </thead> <tbody id="results-body"> </tbody> </table> </div> <script src="admin.js"></script> </body> </html>
Modifikasi public/admin.js:
Tambahkan pengecekan token, logika logout, dan ubah cara menampilkan data agar lebih ringkas (menampilkan skor akhir per siswa).
JavaScriptdocument.addEventListener('DOMContentLoaded', async () => { const token = localStorage.getItem('authToken'); if (!token) { window.location.href = '/login.html'; return; } const resultsBody = document.getElementById('results-body'); try { const response = await fetch('http://localhost:3000/api/results', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.status === 401 || response.status === 403) { localStorage.removeItem('authToken'); window.location.href = '/login.html'; return; } const results = await response.json(); if (results.length === 0) { resultsBody.innerHTML = '<tr><td colspan="2">Belum ada data jawaban.</td></tr>'; return; } // Kelompokkan hasil berdasarkan nama siswa const studentScores = results.reduce((acc, curr) => { if (!acc[curr.student_name]) { acc[curr.student_name] = { score: curr.score }; } return acc; }, {}); let resultsHTML = ''; for (const studentName in studentScores) { resultsHTML += ` <tr> <td>${studentName}</td> <td>${studentScores[studentName].score.toFixed(2)}</td> </tr> `; } resultsBody.innerHTML = resultsHTML; } catch (error) { resultsBody.innerHTML = '<tr><td colspan="2">Gagal memuat data.</td></tr>'; } // Logout document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; }); });
D. Halaman Manajemen Soal (CRUD)
Buat file
public/manage-questions.html
:HTML<!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <title>Kelola Soal</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <div style="display: flex; justify-content: space-between; align-items: center;"> <h1>Kelola Soal</h1> <div> <a href="/admin.html" style="margin-right: 15px;">Lihat Hasil Siswa</a> <button id="logout-btn" style="width: auto; padding: 8px 15px; background-color: #dc3545;">Logout</button> </div> </div> <form id="question-form"> <input type="hidden" id="question-id"> <label for="question-text">Teks Pertanyaan:</label> <textarea id="question-text" rows="3" required></textarea> <label for="option-a">Pilihan A:</label> <input type="text" id="option-a" required> <label for="option-b">Pilihan B:</label> <input type="text" id="option-b" required> <label for="option-c">Pilihan C:</label> <input type="text" id="option-c" required> <label for="correct-answer">Jawaban Benar:</label> <select id="correct-answer" required> <option value="a">A</option> <option value="b">B</option> <option value="c">C</option> </select> <button type="submit" id="submit-btn">Tambah Soal</button> <button type="button" id="cancel-btn" style="display:none; background-color: #6c757d;">Batal Edit</button> </form> <hr style="margin: 30px 0;"> <h2>Daftar Soal</h2> <table id="questions-table"> <thead> <tr> <th>Pertanyaan</th> <th>Jawaban Benar</th> <th>Aksi</th> </tr> </thead> <tbody id="questions-body"></tbody> </table> </div> <script src="manage-questions.js"></script> </body> </html>
Buat file public/manage-questions.js:
Ini adalah file yang paling kompleks, berisi logika untuk menampilkan, menambah, mengedit, dan menghapus soal.
JavaScriptdocument.addEventListener('DOMContentLoaded', () => { const token = localStorage.getItem('authToken'); if (!token) { window.location.href = '/login.html'; return; } const questionForm = document.getElementById('question-form'); const questionsBody = document.getElementById('questions-body'); const questionIdField = document.getElementById('question-id'); const questionTextField = document.getElementById('question-text'); const optionAField = document.getElementById('option-a'); const optionBField = document.getElementById('option-b'); const optionCField = document.getElementById('option-c'); const correctAnswerField = document.getElementById('correct-answer'); const submitBtn = document.getElementById('submit-btn'); const cancelBtn = document.getElementById('cancel-btn'); const apiHeaders = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }; // Fungsi untuk mengambil dan menampilkan semua soal async function loadQuestions() { try { const response = await fetch('http://localhost:3000/api/questions/manage', { headers: apiHeaders }); if (!response.ok) throw new Error('Gagal memuat soal'); const questions = await response.json(); questionsBody.innerHTML = ''; questions.forEach(q => { const row = ` <tr> <td>${q.question_text.substring(0, 50)}...</td> <td>${q.correct_answer.toUpperCase()}</td> <td> <button class="edit-btn" data-id='${JSON.stringify(q)}'>Edit</button> <button class="delete-btn" data-id="${q.id}" style="background-color: #dc3545;">Hapus</button> </td> </tr> `; questionsBody.innerHTML += row; }); } catch (error) { console.error(error); questionsBody.innerHTML = '<tr><td colspan="3">Gagal memuat soal.</td></tr>'; } } // Event listener untuk form submit (Tambah atau Update) questionForm.addEventListener('submit', async (e) => { e.preventDefault(); const questionData = { question_text: questionTextField.value, option_a: optionAField.value, option_b: optionBField.value, option_c: optionCField.value, correct_answer: correctAnswerField.value, }; const questionId = questionIdField.value; const isEditing = !!questionId; const url = isEditing ? `http://localhost:3000/api/questions/${questionId}` : 'http://localhost:3000/api/questions'; const method = isEditing ? 'PUT' : 'POST'; try { const response = await fetch(url, { method, headers: apiHeaders, body: JSON.stringify(questionData) }); const result = await response.json(); alert(result.message); if (response.ok) { resetForm(); loadQuestions(); } } catch (error) { alert('Terjadi kesalahan.'); } }); // Event delegation untuk tombol Edit dan Hapus questionsBody.addEventListener('click', async (e) => { // Tombol Edit if (e.target.classList.contains('edit-btn')) { const questionData = JSON.parse(e.target.dataset.id); questionIdField.value = questionData.id; questionTextField.value = questionData.question_text; optionAField.value = questionData.option_a; optionBField.value = questionData.option_b; optionCField.value = questionData.option_c; correctAnswerField.value = questionData.correct_answer; submitBtn.textContent = 'Update Soal'; cancelBtn.style.display = 'inline-block'; window.scrollTo(0, 0); // Scroll ke atas } // Tombol Hapus if (e.target.classList.contains('delete-btn')) { const questionId = e.target.dataset.id; if (confirm('Anda yakin ingin menghapus soal ini? Semua jawaban terkait juga akan dihapus.')) { try { const response = await fetch(`http://localhost:3000/api/questions/${questionId}`, { method: 'DELETE', headers: apiHeaders }); const result = await response.json(); alert(result.message); if (response.ok) loadQuestions(); } catch (error) { alert('Gagal menghapus soal.'); } } } }); // Tombol Batal Edit cancelBtn.addEventListener('click', resetForm); function resetForm() { questionForm.reset(); questionIdField.value = ''; submitBtn.textContent = 'Tambah Soal'; cancelBtn.style.display = 'none'; } // Logout document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; }); // Muat soal saat halaman dibuka loadQuestions(); });
🚀 Cara Menjalankan Aplikasi yang Sudah Diupdate
Hentikan server lama jika masih berjalan (Ctrl + C di terminal).
Jalankan kembali server Node.js dengan:
Bashnode server.js
Server akan otomatis melakukan hash password admin yang Anda masukkan di database tadi.
Akses Aplikasi:
Login Admin: Buka
http://localhost:3000/login.html
.Lihat Hasil: Setelah login, Anda akan diarahkan ke
http://localhost:3000/admin.html
.Kelola Soal: Dari halaman hasil, klik link "Kelola Soal" untuk pindah ke
http://localhost:3000/manage-questions.html
.Halaman Siswa: Tetap sama,
http://localhost:3000/index.html
.
Contoh Aplikasinya https://drive.google.com/drive/folders/1uG7TMYaETkpItD8fCx130A04HtVMZbno?usp=sharing
Posting Komentar untuk "Membuat Aplikasi Soal dengan Node.Js sebagai backend dan HTML,CSS dan Javascript sebagai frontend"