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

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
# Database Configuration
DB_HOST=localhost
DB_NAME=your_database
DB_USER=your_username
DB_PASSWORD=your_secure_password

15
.eslintrc.json Normal file
View File

@@ -0,0 +1,15 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-undef": "error"
}
}

268
.gitignore vendored Normal file
View File

@@ -0,0 +1,268 @@
### Node ###
# Logs
logs
*.log
npm-debug.log*
vendor/
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
# End of https://www.toptal.com/developers/gitignore/api/node,phpstorm
### PHP ###
# PHP session files
SESS_*
sess_*
# PHP error logs
*.log
# PHP cache
.phpunit.cache/
.cache/
# Composer
/vendor/
/composer.lock

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"htmlWhitespaceSensitivity": "css"
}

90
README.md Normal file
View File

@@ -0,0 +1,90 @@
# Secure Application
A secure web application with PHP backend and JavaScript frontend featuring authentication, data management, and modern UI.
## Features
- User registration and login with secure password handling
- CSRF protection
- SQL injection prevention (PDO prepared statements)
- XSS protection
- Real-time form validation
- Password strength requirements
- Show/hide password toggle
- Responsive dark theme UI
## Tech Stack
- **Backend**: PHP 8+ with PostgreSQL
- **Frontend**: Vanilla JavaScript, HTML, CSS
- **Database**: PostgreSQL
## Requirements
- PHP 8.0+
- PostgreSQL
- Node.js (for formatting/linting)
## Setup
1. **Install dependencies**:
```bash
npm install
composer install
```
2. **Configure database**:
Copy `.env.example` to `.env` and update with your database credentials:
```
DB_HOST=localhost
DB_NAME=securecode
DB_USER=your_username
DB_PASSWORD=your_password
DB_PORT=5432
```
3. **Create database**:
```bash
psql -h localhost -U your_username -d postgres -c "CREATE DATABASE securecode;"
```
4. **Run migrations**:
```bash
psql -h localhost -U your_username -d securecode -f config/schema.sql
```
5. **Start development server**:
```bash
php -S localhost:8000 -t public
```
6. **Access the app**: http://localhost:8000
## Development
- **Format code**: `npm run format`
- **Lint code**: `npm run lint`
## Project Structure
```
├── config/
│ ├── database.php # Database connection & helpers
│ └── schema.sql # Database schema
├── api/
│ └── index.php # API endpoints
├── public/
│ ├── index.php # Router
│ ├── views/ # Page templates
│ ├── js/ # JavaScript files
│ └── styles/ # CSS files
└── .env.example # Environment template
```
## Password Requirements
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character

254
api/index.php Normal file
View File

@@ -0,0 +1,254 @@
<?php
require_once __DIR__ . '/../config/database.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Requested-With');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
$method = $_SERVER['REQUEST_METHOD'];
$database = new Database();
if ($method === 'POST') {
$action = $_POST['action'] ?? '';
$actions = [
'csrf_token' => fn() => print json_encode(['token' => generateToken()]),
'create' => fn() => handleRegistration($database),
'login' => fn() => handleLogin($database),
'submit_data' => fn() => handleDataSubmission($database),
'get_data' => fn() => handleGetData($database),
'delete_data' => fn() => handleDeleteData($database),
];
if (isset($actions[$action])) {
$actions[$action]();
} else {
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
function handleRegistration($database) {
$token = $_POST['csrf_token'] ?? '';
if (!verifyToken($token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
return;
}
$username = sanitizeInput($_POST['username'] ?? '');
$email = sanitizeInput($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($email) || empty($password)) {
http_response_code(400);
echo json_encode(['error' => 'All fields are required']);
return;
}
if (!validateEmail($email)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid email format']);
return;
}
if (!validateString($username, 50)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid username format']);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid email format']);
return;
}
$passwordErrors = validatePassword($password);
if (!empty($passwordErrors)) {
http_response_code(400);
echo json_encode(['error' => $passwordErrors[0]]);
return;
}
try {
$checkSql = "SELECT id FROM users WHERE email = ? OR username = ?";
$stmt = $database->query($checkSql, [$email, $username]);
if ($stmt->fetch()) {
http_response_code(409);
echo json_encode(['error' => 'User already exists']);
return;
}
$hashedPassword = hashPassword($password);
$insertSql = "INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())";
$database->query($insertSql, [sanitizeInput($username), $email, $hashedPassword]);
echo json_encode(['success' => true, 'message' => 'User created successfully']);
} catch (Exception $e) {
error_log("Registration error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Failed to create user']);
}
}
function handleLogin($database) {
$token = $_POST['csrf_token'] ?? '';
if (!verifyToken($token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
return;
}
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
http_response_code(400);
echo json_encode(['error' => 'Email and password are required']);
return;
}
try {
$sql = "SELECT id, username, email, password FROM users WHERE email = ?";
$stmt = $database->query($sql, [$email]);
$user = $stmt->fetch();
if (!$user || !verifyPassword($password, $user['password'])) {
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
return;
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
echo json_encode(['success' => true, 'message' => 'Login successful']);
} catch (Exception $e) {
error_log("Login error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Login failed']);
}
}
function handleDataSubmission($database) {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return;
}
$token = $_POST['csrf_token'] ?? '';
if (!verifyToken($token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
return;
}
$data = sanitizeInput($_POST['data'] ?? '');
if (empty($data)) {
http_response_code(400);
echo json_encode(['error' => 'Data is required']);
return;
}
if (!validateString($data, 5000)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid data format']);
return;
}
try {
$sql = "INSERT INTO user_data (user_id, data, created_at) VALUES (?, ?, NOW())";
$database->query($sql, [$_SESSION['user_id'], $data]);
echo json_encode(['success' => true, 'message' => 'Data saved successfully']);
} catch (Exception $e) {
error_log("Data submission error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Failed to save data']);
}
}
function handleGetData($database) {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return;
}
try {
$sql = "SELECT id, data, created_at FROM user_data WHERE user_id = ? ORDER BY created_at DESC";
$stmt = $database->query($sql, [$_SESSION['user_id']]);
$data = $stmt->fetchAll();
$sanitizedData = array_map(function($item) {
return [
'id' => $item['id'],
'data' => escapeHtml($item['data']),
'created_at' => $item['created_at']
];
}, $data);
echo json_encode(['success' => true, 'data' => $sanitizedData]);
} catch (Exception $e) {
error_log("Get data error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Failed to retrieve data']);
}
}
function handleDeleteData($database) {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return;
}
$token = $_POST['csrf_token'] ?? '';
if (!verifyToken($token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
return;
}
$id = filter_var($_POST['id'] ?? '', FILTER_VALIDATE_INT);
if (empty($id)) {
http_response_code(400);
echo json_encode(['error' => 'ID is required']);
return;
}
try {
$sql = "DELETE FROM user_data WHERE id = ? AND user_id = ?";
$database->query($sql, [$id, $_SESSION['user_id']]);
echo json_encode(['success' => true, 'message' => 'Data deleted successfully']);
} catch (Exception $e) {
error_log("Delete data error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Failed to delete data']);
}
}

13
composer.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "user/secure-web-app",
"description": "Secure PHP/JavaScript web application",
"require-dev": {
"squizlabs/php_codesniffer": "^3.9"
},
"config": {
"php_codesniffer": {
"standard": "PSR12",
"encoding": "utf-8"
}
}
}

136
config/database.php Normal file
View File

@@ -0,0 +1,136 @@
<?php
session_start();
class Database {
private $host;
private $db;
private $user;
private $pass;
private $port;
private $charset;
private $pdo;
public function __construct() {
$this->host = getenv('DB_HOST') ?: 'localhost';
$this->db = getenv('DB_NAME') ?: 'securecode';
$this->user = getenv('DB_USER') ?: 'carlos';
$this->pass = getenv('DB_PASSWORD') ?: '';
$this->port = getenv('DB_PORT') ?: '5432';
$this->charset = 'utf8';
}
public function getConnection() {
if ($this->pdo !== null) {
return $this->pdo;
}
$dsn = "pgsql:host={$this->host};port={$this->port};dbname={$this->db};options='--client_encoding={$this->charset}'";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$this->pdo = new PDO($dsn, $this->user, $this->pass, $options);
} catch (\PDOException $e) {
error_log("Database connection error: " . $e->getMessage());
throw new \Exception("Database connection failed");
}
return $this->pdo;
}
public function query($sql, $params = []) {
$pdo = $this->getConnection();
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
}
function sanitizeInput($data) {
if (is_array($data)) {
return array_map('sanitizeInput', $data);
}
return htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
}
function escapeHtml($data) {
if (is_array($data)) {
return array_map('escapeHtml', $data);
}
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
function validateString($data, $maxLength = 1000) {
if (is_array($data)) {
return array_map(fn($item) => validateString($item, $maxLength), $data);
}
$data = trim($data);
if (strlen($data) > $maxLength) {
return false;
}
return preg_match('/^[\p{L}\p{N}\s\-_.,!?@]+$/u', $data) === 1 || empty($data);
}
function sanitizeForDatabase($data) {
if (is_array($data)) {
return array_map('sanitizeForDatabase', $data);
}
return preg_replace('/[^\p{L}\p{N}\s\-_.,!?@]/u', '', $data);
}
function validatePassword($password) {
$errors = [];
if (strlen($password) < 8) {
$errors[] = 'Password must be at least 8 characters';
}
if (strlen($password) > 128) {
$errors[] = 'Password must be less than 128 characters';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = 'Password must contain at least one lowercase letter';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'Password must contain at least one uppercase letter';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = 'Password must contain at least one number';
}
if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
$errors[] = 'Password must contain at least one special character';
}
return $errors;
}
function generateToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function verifyToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
function hashPassword($password) {
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}

22
config/schema.sql Normal file
View File

@@ -0,0 +1,22 @@
-- PostgreSQL schema for secure application
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Create user_data table
CREATE TABLE IF NOT EXISTS user_data (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_data_user_id ON user_data(user_id);

5
cookies.txt Normal file
View File

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
localhost FALSE / FALSE 0 PHPSESSID 5dde60a9b8dd9c20d2ff97d021f326b3

1259
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "secure-web-app",
"version": "1.0.0",
"description": "Secure PHP/JavaScript web application",
"author": "Carlos Gutierrez <cgutierrez44833@ucumberlands.edu>",
"repository": {
"type": "git",
"url": "https://github.com/CarGDev/MSCS535_Assignment14"
},
"scripts": {
"lint": "eslint public/js/ && ./vendor/bin/phpcs --standard=PSR12 api/ config/",
"lint:fix": "eslint public/js/ --fix",
"format": "prettier --write \"public/**/*.{html,js,css}\"",
"format:check": "prettier --check \"public/**/*.{html,js,css}\"",
"format:all": "prettier --write \"public/**/*.{html,js,css}\" && ./vendor/bin/phpcbf api/ config/"
},
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "^3.2.5"
}
}

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>