Complete mock secure web application with:

- User registration and login with CSRF protection
- SQL injection prevention and XSS protection
- Real-time form validation
- Password strength requirements
- Show/hide password toggle
- Modern dark theme UI
- Routes for /login, /register, /home, /logout
- API endpoints for CRUD operations
- Prettier and ESLint configure
This commit is contained in:
2026-02-21 18:20:41 -05:00
commit dea56a7e80
22 changed files with 3366 additions and 0 deletions

3
public/js/app.js Normal file
View File

@@ -0,0 +1,3 @@
document.addEventListener('DOMContentLoaded', async () => {
await api.getCSRFToken();
});

76
public/js/home.js Normal file
View File

@@ -0,0 +1,76 @@
async function loadUserData() {
const dataList = document.getElementById('dataList');
if (!dataList) return;
try {
const result = await api.getData();
if (result.success) {
if (result.data.length === 0) {
dataList.innerHTML = '<p class="empty-state">No data submitted yet</p>';
} else {
dataList.innerHTML = result.data
.map(
(item) => `
<div class="data-item">
<div class="data-item-left">
<span class="data-item-content">${api.sanitizeHTML(item.data)}</span>
<span class="data-item-time">${new Date(item.created_at).toLocaleString()}</span>
</div>
<button class="btn-delete" data-id="${item.id}" title="Delete">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="color: white;">
<path d="M10 2L9 3L3 3L3 5L4.109375 5L5.8925781 20.255859L5.8925781 20.263672C6.023602 21.250335 6.8803207 22 7.875 22L16.123047 22C17.117726 22 17.974445 21.250322 18.105469 20.263672L18.107422 20.255859L19.890625 5L21 5L21 3L15 3L14 2L10 2zM6.125 5L17.875 5L16.123047 20L7.875 20L6.125 5z"></path>
</svg>
</button>
</div>`
)
.join('');
document.querySelectorAll('.btn-delete').forEach((btn) => {
btn.addEventListener('click', async (e) => {
const id = e.currentTarget.dataset.id;
try {
await api.deleteData(id);
loadUserData();
} catch (error) {
console.error('Failed to delete:', error);
}
});
});
}
}
} catch (error) {
console.error('Failed to load data:', error);
}
}
function initDataForm() {
const dataForm = document.getElementById('dataForm');
const messageDiv = document.getElementById('message');
if (dataForm) {
dataForm.addEventListener('submit', async (e) => {
e.preventDefault();
messageDiv.textContent = 'Processing...';
messageDiv.className = 'message info';
const data = dataForm.data.value.trim();
try {
const result = await api.submitData(data);
messageDiv.textContent = result.message;
messageDiv.className = 'message success';
dataForm.reset();
loadUserData();
} catch (error) {
messageDiv.textContent = error.message;
messageDiv.className = 'message error';
}
});
}
}
document.addEventListener('DOMContentLoaded', async () => {
await api.getCSRFToken();
initDataForm();
loadUserData();
});

97
public/js/login.js Normal file
View File

@@ -0,0 +1,97 @@
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
if (errorEl) {
errorEl.textContent = message;
errorEl.style.display = message ? 'block' : 'none';
}
}
function clearErrors() {
document.querySelectorAll('.error-message').forEach((el) => {
el.textContent = '';
el.style.display = 'none';
});
}
function initPasswordToggle() {
document.querySelectorAll('.toggle-password').forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
const input = document.getElementById(targetId);
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
btn.querySelector('.eye-open').style.display = isPassword ? 'none' : 'block';
btn.querySelector('.eye-closed').style.display = isPassword ? 'block' : 'none';
}
});
});
}
function initLoginForm() {
const loginForm = document.getElementById('loginForm');
const emailInput = document.getElementById('loginEmail');
const passwordInput = document.getElementById('loginPassword');
if (emailInput) {
emailInput.addEventListener('input', () => {
const email = emailInput.value.trim();
const error =
!validateEmail(email) && email.length > 0 ? 'Please enter a valid email address' : '';
showError('loginEmailError', error);
});
}
if (passwordInput) {
passwordInput.addEventListener('input', () => {
const password = passwordInput.value;
const error = password.length > 0 && password.length < 1 ? 'Password is required' : '';
showError('loginPasswordError', error);
});
}
if (loginForm) {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
clearErrors();
const email = loginForm.email.value.trim();
const password = loginForm.password.value;
if (!validateEmail(email)) {
showError('loginEmailError', 'Please enter a valid email address');
return;
}
if (!password) {
showError('loginPasswordError', 'Password is required');
return;
}
showError('loginFormError', 'Processing...');
try {
const result = await api.login(email, password);
showError('loginFormError', result.message);
loginForm.classList.add('success');
loginForm.reset();
setTimeout(() => {
window.location.href = '/home';
}, 1000);
} catch (error) {
showError('loginFormError', error.message);
}
});
}
}
document.addEventListener('DOMContentLoaded', async () => {
await api.getCSRFToken();
initPasswordToggle();
initLoginForm();
});

132
public/js/register.js Normal file
View File

@@ -0,0 +1,132 @@
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validatePassword(password) {
if (password.length === 0) return '';
if (password.length < 8) return 'Password must be at least 8 characters';
if (password.length > 128) return 'Password must be less than 128 characters';
if (!/[a-z]/.test(password)) return 'Password must contain at least one lowercase letter';
if (!/[A-Z]/.test(password)) return 'Password must contain at least one uppercase letter';
if (!/[0-9]/.test(password)) return 'Password must contain at least one number';
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password))
return 'Password must contain at least one special character';
return '';
}
function validateUsername(username) {
if (username.length === 0) return '';
if (username.length < 3) return 'Username must be at least 3 characters';
return '';
}
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
if (errorEl) {
errorEl.textContent = message;
errorEl.style.display = message ? 'block' : 'none';
}
}
function clearErrors() {
document.querySelectorAll('.error-message').forEach((el) => {
el.textContent = '';
el.style.display = 'none';
});
}
function initPasswordToggle() {
document.querySelectorAll('.toggle-password').forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
const input = document.getElementById(targetId);
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
btn.querySelector('.eye-open').style.display = isPassword ? 'none' : 'block';
btn.querySelector('.eye-closed').style.display = isPassword ? 'block' : 'none';
}
});
});
}
function initRegisterForm() {
const registerForm = document.getElementById('registerForm');
const usernameInput = document.getElementById('regUsername');
const emailInput = document.getElementById('regEmail');
const passwordInput = document.getElementById('regPassword');
if (usernameInput) {
usernameInput.addEventListener('input', () => {
const username = usernameInput.value.trim();
const error = validateUsername(username);
showError('regUsernameError', error);
});
}
if (emailInput) {
emailInput.addEventListener('input', () => {
const email = emailInput.value.trim();
const error =
!validateEmail(email) && email.length > 0 ? 'Please enter a valid email address' : '';
showError('regEmailError', error);
});
}
if (passwordInput) {
passwordInput.addEventListener('input', () => {
const password = passwordInput.value;
const error = validatePassword(password);
showError('regPasswordError', error);
});
}
if (registerForm) {
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
clearErrors();
const username = registerForm.username.value.trim();
const email = registerForm.email.value.trim();
const password = registerForm.password.value;
const usernameError = validateUsername(username);
if (usernameError) {
showError('regUsernameError', usernameError);
return;
}
if (!validateEmail(email)) {
showError('regEmailError', 'Please enter a valid email address');
return;
}
const passwordError = validatePassword(password);
if (passwordError) {
showError('regPasswordError', passwordError);
return;
}
showError('registerFormError', 'Processing...');
try {
const result = await api.create(username, email, password);
showError('registerFormError', result.message);
registerForm.classList.add('success');
registerForm.reset();
setTimeout(() => {
window.location.href = '/login';
}, 1500);
} catch (error) {
showError('registerFormError', error.message);
}
});
}
}
document.addEventListener('DOMContentLoaded', async () => {
await api.getCSRFToken();
initPasswordToggle();
initRegisterForm();
});

123
public/js/request.js Normal file
View File

@@ -0,0 +1,123 @@
const API_BASE = 'api/index.php';
class ApiRequest {
constructor() {
this.csrfToken = null;
}
async getCSRFToken() {
try {
const formData = new FormData();
formData.append('action', 'csrf_token');
const response = await fetch(API_BASE, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
const data = await response.json();
this.csrfToken = data.token;
return this.csrfToken;
} catch (error) {
console.error('Failed to get CSRF token:', error);
throw error;
}
}
sanitizeHTML(str) {
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
async request(action, formData) {
if (!this.csrfToken) {
await this.getCSRFToken();
}
formData.append('action', action);
formData.append('csrf_token', this.csrfToken);
try {
const response = await fetch(API_BASE, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
if (error.message === 'Invalid CSRF token') {
await this.getCSRFToken();
formData.set('csrf_token', this.csrfToken);
const response = await fetch(API_BASE, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
return data;
}
throw error;
}
}
async create(username, email, password) {
const formData = new FormData();
formData.append('username', username);
formData.append('email', email);
formData.append('password', password);
return this.request('create', formData);
}
async login(email, password) {
const formData = new FormData();
formData.append('email', email);
formData.append('password', password);
return this.request('login', formData);
}
async submitData(data) {
const formData = new FormData();
formData.append('data', data);
return this.request('submit_data', formData);
}
async getData() {
const formData = new FormData();
formData.append('action', 'get_data');
try {
const response = await fetch(API_BASE, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
return await response.json();
} catch (error) {
console.error('Failed to get data:', error);
throw error;
}
}
async deleteData(id) {
const formData = new FormData();
formData.append('id', id);
return this.request('delete_data', formData);
}
}
const api = new ApiRequest();