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:
5
.env.example
Normal file
5
.env.example
Normal 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
15
.eslintrc.json
Normal 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
268
.gitignore
vendored
Normal 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
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
}
|
||||
90
README.md
Normal file
90
README.md
Normal 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
254
api/index.php
Normal 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
13
composer.json
Normal 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
136
config/database.php
Normal 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
22
config/schema.sql
Normal 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
5
cookies.txt
Normal 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
1259
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal 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
35
public/index.php
Normal 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
3
public/js/app.js
Normal file
@@ -0,0 +1,3 @@
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await api.getCSRFToken();
|
||||
});
|
||||
76
public/js/home.js
Normal file
76
public/js/home.js
Normal 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
97
public/js/login.js
Normal 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
132
public/js/register.js
Normal 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
123
public/js/request.js
Normal 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
607
public/styles/styles.css
Normal 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
82
public/views/home.php
Normal 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
58
public/views/login.php
Normal 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
57
public/views/register.php
Normal 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>
|
||||
Reference in New Issue
Block a user