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

35
public/index.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/../config/database.php';
$requestUri = $_SERVER['REQUEST_URI'];
$path = rtrim(parse_url($requestUri, PHP_URL_PATH), '/') ?: '/';
if (!empty($_SERVER['QUERY_STRING'])) {
header('Location: ' . $path);
exit;
}
if (strpos($path, '/api/') === 0) {
require __DIR__ . '/../api/index.php';
exit;
}
if ($path === '/') {
header('Location: /login');
exit;
}
if ($path === '/login') {
require __DIR__ . '/views/login.php';
} elseif ($path === '/register') {
require __DIR__ . '/views/register.php';
} elseif ($path === '/home') {
require __DIR__ . '/views/home.php';
} elseif ($path === '/logout') {
session_destroy();
header('Location: /login');
exit;
} else {
http_response_code(404);
echo '404 Not Found';
}

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();

607
public/styles/styles.css Normal file
View File

@@ -0,0 +1,607 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-dark: #0a0a0f;
--bg-card: #12121a;
--bg-card-hover: #1a1a25;
--primary: #6366f1;
--primary-hover: #818cf8;
--primary-glow: rgba(99, 102, 241, 0.4);
--danger: #ef4444;
--danger-hover: #f87171;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border: #1e293b;
--gradient-1: #6366f1;
--gradient-2: #8b5cf6;
--gradient-3: #ec4899;
}
body {
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
background-color: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
position: relative;
overflow-x: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.bg-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse 80% 50% at 50% -20%, var(--primary-glow), transparent),
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(236, 72, 153, 0.15), transparent);
pointer-events: none;
z-index: 0;
}
.bg-grid {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
z-index: 0;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
z-index: 100;
backdrop-filter: blur(10px);
background: rgba(10, 10, 15, 0.7);
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 700;
}
.logo-icon {
font-size: 24px;
}
.logo-text {
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.btn-logout {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
color: var(--danger);
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-logout:hover {
background: var(--danger);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
}
.btn-logout svg {
transition: transform 0.3s ease;
}
.btn-logout:hover svg {
transform: translateX(4px);
}
.main {
position: relative;
z-index: 1;
max-width: 800px;
margin: 0 auto;
padding: 140px 24px 60px;
}
.hero {
text-align: center;
margin-bottom: 50px;
}
.hero-badge {
display: inline-block;
padding: 8px 16px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 50px;
font-size: 13px;
font-weight: 600;
color: var(--primary-hover);
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 1px;
}
.hero-title {
font-size: 48px;
font-weight: 800;
margin-bottom: 16px;
line-height: 1.2;
}
.gradient-text {
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2), var(--gradient-3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 18px;
color: var(--text-secondary);
max-width: 500px;
margin: 0 auto;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 24px;
padding: 32px;
margin-bottom: 24px;
transition: all 0.3s ease;
}
.card:hover {
background: var(--bg-card-hover);
border-color: rgba(99, 102, 241, 0.3);
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.card-header {
margin-bottom: 24px;
}
.card-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.card-description {
color: var(--text-secondary);
font-size: 14px;
}
.data-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
}
.input-group textarea {
width: 100%;
padding: 16px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 15px;
font-family: inherit;
resize: vertical;
transition: all 0.3s ease;
}
.input-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--primary-glow);
}
.input-group textarea::placeholder {
color: var(--text-muted);
}
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px 32px;
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
border: none;
border-radius: 14px;
color: white;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
align-self: flex-start;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 15px 35px var(--primary-glow);
}
.btn-primary svg {
transition: transform 0.3s ease;
}
.btn-primary:hover svg {
transform: translate(4px, -4px);
}
.data-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border);
border-radius: 12px;
transition: all 0.2s ease;
}
.data-item:hover {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(0, 0, 0, 0.3);
}
.data-item-content {
font-size: 15px;
color: var(--text-primary);
}
.data-item-time {
font-size: 12px;
color: var(--text-muted);
}
.empty-state {
text-align: center;
color: var(--text-muted);
padding: 40px;
font-size: 14px;
}
.message {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
z-index: 200;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.message.error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--danger-hover);
}
.message.success {
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.message.info {
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
color: var(--primary-hover);
}
.register-link {
text-align: center;
margin-top: 15px;
}
.register-link a {
color: var(--primary-hover);
text-decoration: none;
font-weight: 600;
}
.register-link a:hover {
text-decoration: underline;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.error-message {
display: none;
color: var(--danger);
font-size: 13px;
margin-top: 6px;
}
.form-error {
text-align: center;
margin-bottom: 15px;
}
form.success .form-error {
color: #4ade80;
}
button {
background: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
.section {
display: none;
margin-top: 30px;
}
#loginSection {
display: block;
}
h2 {
margin-top: 0;
}
ul {
padding-left: 20px;
}
.hidden {
display: none !important;
}
.container {
width: 100%;
max-width: 450px;
padding: 40px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 24px;
}
.container h1 {
text-align: center;
margin-bottom: 32px;
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.container h2 {
margin-bottom: 24px;
color: var(--text-primary);
}
.container .form-group input {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
color: var(--text-primary);
}
.container .form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--primary-glow);
}
.container .form-group input::placeholder {
color: var(--text-muted);
}
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrapper input {
padding-right: 48px;
}
.toggle-password {
position: absolute;
right: 12px;
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-password:hover {
color: var(--text-secondary);
background: none;
}
.toggle-password svg {
transition: all 0.2s ease;
}
.container button[type='submit'] {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
border: none;
border-radius: 12px;
color: white;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
}
.container button[type='submit']:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px var(--primary-glow);
}
.container .register-link {
margin-top: 24px;
color: var(--text-secondary);
}
.link-btn {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: inherit;
text-decoration: underline;
padding: 0;
}
.link-btn:hover {
background: none;
color: #0056b3;
}
@media (max-width: 640px) {
.header {
padding: 16px 20px;
}
.logo-text {
display: none;
}
.main {
padding: 100px 16px 40px;
}
.hero-title {
font-size: 32px;
}
.card {
padding: 24px;
border-radius: 16px;
}
.btn-primary {
width: 100%;
}
}
.data-item-left {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.btn-delete {
display: flex;
align-items: center;
justify-content: center;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 8px;
color: var(--danger);
cursor: pointer;
transition: all 0.2s ease;
}
.btn-delete:hover {
background: var(--danger);
color: white;
}

82
public/views/home.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
if (!isset($_SESSION['user_id'])) {
header('Location: /login');
exit;
}
$username = $_SESSION['username'] ?? 'User';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="./styles/styles.css" />
<title>Home - Secure App</title>
</head>
<body>
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
<header class="header">
<div class="logo">
<span class="logo-icon">🔐</span>
<span class="logo-text">SecureVault</span>
</div>
<a href="/logout" class="btn-logout">
<span>Logout</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16,17 21,12 16,7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
</a>
</header>
<main class="main">
<div class="hero">
<div class="hero-badge">Welcome Back</div>
<h1 class="hero-title">
Hello, <span class="gradient-text"><?php echo htmlspecialchars($username); ?></span>
</h1>
<p class="hero-subtitle">Your secure space awaits. Manage your data with confidence.</p>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Submit Data</h2>
<p class="card-description">Store your information securely</p>
</div>
<form id="dataForm" class="data-form">
<div class="input-group">
<label for="dataInput">Your Data</label>
<textarea id="dataInput" name="data" rows="4" placeholder="Enter your data here..." required></textarea>
</div>
<button type="submit" class="btn-primary">
<span>Submit Data</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22,2 15,22 11,13 2,9" />
</svg>
</button>
</form>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Your Data</h2>
<p class="card-description">All your stored information</p>
</div>
<div class="data-list" id="dataList">
<p class="empty-state">No data submitted yet</p>
</div>
</div>
</main>
<div id="message"></div>
<script src="js/request.js"></script>
<script src="js/home.js"></script>
</body>
</html>

58
public/views/login.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
if (isset($_SESSION['user_id'])) {
header('Location: /home');
exit;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="./styles/styles.css" />
<title>Login - Secure App</title>
</head>
<body>
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
<div class="container">
<h1>Secure Application</h1>
<h2>Login</h2>
<form id="loginForm" novalidate>
<div class="form-group">
<label for="loginEmail">Email</label>
<input type="text" id="loginEmail" name="email" autocomplete="email" />
<span class="error-message" id="loginEmailError"></span>
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<div class="password-input-wrapper">
<input type="password" id="loginPassword" name="password" autocomplete="current-password" />
<button type="button" class="toggle-password" data-target="loginPassword">
<svg class="eye-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-closed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
<span class="error-message" id="loginPasswordError"></span>
</div>
<div class="error-message form-error" id="loginFormError"></div>
<button type="submit">Login</button>
</form>
<p class="register-link">
Don't have an account? <a href="/register">Register here</a>
</p>
</div>
<script src="js/request.js"></script>
<script src="js/login.js"></script>
</body>
</html>

57
public/views/register.php Normal file
View File

@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="./styles/styles.css" />
<title>Register - Secure App</title>
</head>
<body>
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
<div class="container">
<h1>Secure Application</h1>
<h2>Register</h2>
<form id="registerForm" novalidate>
<div class="form-group">
<label for="regUsername">Username</label>
<input type="text" id="regUsername" name="username" autocomplete="username" />
<span class="error-message" id="regUsernameError"></span>
</div>
<div class="form-group">
<label for="regEmail">Email</label>
<input type="text" id="regEmail" name="email" autocomplete="email" />
<span class="error-message" id="regEmailError"></span>
</div>
<div class="form-group">
<label for="regPassword">Password</label>
<div class="password-input-wrapper">
<input type="password" id="regPassword" name="password" autocomplete="new-password" />
<button type="button" class="toggle-password" data-target="regPassword">
<svg class="eye-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-closed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
<span class="error-message" id="regPasswordError"></span>
</div>
<div class="error-message form-error" id="registerFormError"></div>
<button type="submit">Register</button>
</form>
<p class="register-link">
Already have an account? <a href="/login">Login here</a>
</p>
</div>
<script src="js/request.js"></script>
<script src="js/register.js"></script>
</body>
</html>