Lompat ke konten Lompat ke sidebar Lompat ke footer

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.

  1. Tambahkan Tabel admins:

    Jalankan kueri ini untuk membuat tabel yang akan menyimpan username dan password admin. Password akan di-hash demi keamanan.

    SQL
    USE db_aplikasi_soal;
    
    CREATE TABLE admins (
        id INT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(100) NOT NULL UNIQUE,
        password VARCHAR(255) NOT NULL
    );
    
  2. 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');
    
  3. Tambahkan Kolom score di Tabel students:

    Kolom ini akan menyimpan nilai akhir siswa.

    SQL
    ALTER 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.

  1. Install Library Baru:

    Buka terminal di folder proyek Anda dan jalankan:

    Bash
    npm install jsonwebtoken bcryptjs
    
    • jsonwebtoken: Untuk membuat token otentikasi (JWT).

    • bcryptjs: Untuk mengenkripsi (hash) password admin.

  2. 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.

    JavaScript
    const 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

  1. 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>
    
  2. Buat file public/login.js:

    JavaScript
    document.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)

  1. 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)

  1. 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>
    
  2. Modifikasi public/admin.js:

    Tambahkan pengecekan token, logika logout, dan ubah cara menampilkan data agar lebih ringkas (menampilkan skor akhir per siswa).

    JavaScript
    document.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)

  1. 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>
    
  2. Buat file public/manage-questions.js:

    Ini adalah file yang paling kompleks, berisi logika untuk menampilkan, menambah, mengedit, dan menghapus soal.

    JavaScript
    document.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

  1. Hentikan server lama jika masih berjalan (Ctrl + C di terminal).

  2. Jalankan kembali server Node.js dengan:

    Bash
    node server.js
    

    Server akan otomatis melakukan hash password admin yang Anda masukkan di database tadi.

  3. 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"