akra35567 commited on
Commit
343aa99
·
verified ·
1 Parent(s): 9f8f4a5

Upload 5 files

Browse files
Files changed (5) hide show
  1. src/Bootstrap.php +513 -0
  2. src/EmailService.php +168 -0
  3. src/ReactLoader.php +0 -0
  4. src/User.php +458 -0
  5. src/_init_.php +153 -0
src/Bootstrap.php ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ declare(strict_types=1);
3
+
4
+ namespace SoftEdge;
5
+
6
+ use PDO;
7
+ use PDOException;
8
+ use Throwable;
9
+
10
+ // Try to load Composer autoload if not already loaded
11
+ if (!class_exists(\Composer\Autoload\ClassLoader::class)) {
12
+ $autoload = __DIR__ . '/../vendor/autoload.php';
13
+ if (is_file($autoload)) {
14
+ require_once $autoload;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Env loader with Dotenv support and fallback parser
20
+ */
21
+ final class Env
22
+ {
23
+ private static bool $loaded = false;
24
+ private static array $cache = [];
25
+
26
+ public static function load(?string $dir = null): void
27
+ {
28
+ if (self::$loaded) return;
29
+ $dir = $dir ?? dirname(__DIR__);
30
+
31
+ // Load from $_ENV/$_SERVER first
32
+ self::$cache = array_merge($_SERVER, $_ENV);
33
+
34
+ // Prefer vlucas/phpdotenv when available
35
+ if (class_exists(\Dotenv\Dotenv::class)) {
36
+ try {
37
+ $dotenv = \Dotenv\Dotenv::createImmutable($dir, ['.env']);
38
+ $dotenv->safeLoad();
39
+ self::$cache = array_merge(self::$cache, $_ENV);
40
+ } catch (Throwable $e) {
41
+ // ignore, fallback to manual parse
42
+ }
43
+ } else {
44
+ // Fallback: manual parse basic .env
45
+ $envFile = $dir . DIRECTORY_SEPARATOR . '.env';
46
+ if (is_file($envFile)) {
47
+ $lines = @file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
48
+ foreach ($lines as $line) {
49
+ if (preg_match('/^\s*#/',$line)) continue;
50
+ if (!str_contains($line, '=')) continue;
51
+ [$k,$v] = array_map('trim', explode('=', $line, 2));
52
+ $v = trim($v, "\"' ");
53
+ self::$cache[$k] = $v;
54
+ }
55
+ }
56
+ }
57
+
58
+ // Defaults
59
+ self::$cache['APP_ENV'] = self::$cache['APP_ENV'] ?? 'production';
60
+ self::$cache['TIMEZONE'] = self::$cache['TIMEZONE'] ?? 'Africa/Luanda';
61
+ self::$cache['JWT_SECRET'] = self::$cache['JWT_SECRET'] ?? base64_encode(random_bytes(32));
62
+ self::$cache['APP_KEY'] = self::$cache['APP_KEY'] ?? base64_encode(random_bytes(32));
63
+
64
+ self::$loaded = true;
65
+ }
66
+
67
+ public static function get(string $key, ?string $default = null): ?string
68
+ {
69
+ return self::$cache[$key] ?? $default;
70
+ }
71
+
72
+ public static function bool(string $key, bool $default = false): bool
73
+ {
74
+ $v = self::get($key);
75
+ if ($v === null) return $default;
76
+ $v = strtolower($v);
77
+ return in_array($v, ['1','true','yes','on'], true);
78
+ }
79
+
80
+ public static function int(string $key, int $default = 0): int
81
+ {
82
+ $v = self::get($key);
83
+ return $v !== null && is_numeric($v) ? (int)$v : $default;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Security headers
89
+ */
90
+ final class SecurityHeaders
91
+ {
92
+ public static function apply(): void
93
+ {
94
+ // Only add if headers not sent
95
+ if (headers_sent()) return;
96
+
97
+ $csp = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
98
+ header('Content-Security-Policy: ' . $csp);
99
+ header('X-Frame-Options: DENY');
100
+ header('X-Content-Type-Options: nosniff');
101
+ header('Referrer-Policy: strict-origin-when-cross-origin');
102
+ header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
103
+
104
+ // HSTS only when HTTPS
105
+ if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
106
+ header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Standardized JSON responses
113
+ */
114
+ final class Response
115
+ {
116
+ public static function json(array $data, int $status = 200): void
117
+ {
118
+ if (!headers_sent()) {
119
+ http_response_code($status);
120
+ header('Content-Type: application/json; charset=utf-8');
121
+ }
122
+ echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Simple validation helpers
128
+ */
129
+ final class Validation
130
+ {
131
+ public static function required(array $data, array $fields): array
132
+ {
133
+ $errors = [];
134
+ foreach ($fields as $f) {
135
+ if (!isset($data[$f]) || $data[$f] === '') {
136
+ $errors[$f] = 'Campo obrigatório';
137
+ }
138
+ }
139
+ return $errors;
140
+ }
141
+
142
+ public static function email(string $email): bool
143
+ {
144
+ return (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Very light file/APCu based rate limiter
150
+ */
151
+ final class RateLimiter
152
+ {
153
+ public static function allow(string $key, int $maxRequests, int $windowSeconds): bool
154
+ {
155
+ $now = time();
156
+ $id = 'rate_' . sha1($key);
157
+
158
+ if (function_exists('apcu_fetch')) {
159
+ $entry = apcu_fetch($id, $success);
160
+ if (!$success || !is_array($entry) || $entry['reset'] <= $now) {
161
+ apcu_store($id, ['count' => 1, 'reset' => $now + $windowSeconds], $windowSeconds);
162
+ return true;
163
+ }
164
+ if ($entry['count'] < $maxRequests) {
165
+ $entry['count']++;
166
+ apcu_store($id, $entry, $entry['reset'] - $now);
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+
172
+ // Fallback: file-based
173
+ $dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'storage';
174
+ if (!is_dir($dir)) @mkdir($dir, 0775, true);
175
+ $file = $dir . DIRECTORY_SEPARATOR . $id . '.json';
176
+ $entry = ['count' => 0, 'reset' => $now + $windowSeconds];
177
+ if (is_file($file)) {
178
+ $content = json_decode((string)@file_get_contents($file), true) ?: [];
179
+ if (isset($content['reset']) && $content['reset'] > $now) {
180
+ $entry = $content;
181
+ }
182
+ }
183
+ if ($entry['reset'] <= $now) {
184
+ $entry = ['count' => 1, 'reset' => $now + $windowSeconds];
185
+ @file_put_contents($file, json_encode($entry));
186
+ return true;
187
+ }
188
+ if ($entry['count'] < $maxRequests) {
189
+ $entry['count']++;
190
+ @file_put_contents($file, json_encode($entry));
191
+ return true;
192
+ }
193
+ return false;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * CSRF token utilities for forms and fetch requests
199
+ */
200
+ final class Csrf
201
+ {
202
+ private const KEY = '_csrf';
203
+
204
+ public static function ensure(): void
205
+ {
206
+ if (!isset($_SESSION[self::KEY])) {
207
+ $_SESSION[self::KEY] = bin2hex(random_bytes(32));
208
+ }
209
+ }
210
+
211
+ public static function token(): string
212
+ {
213
+ return (string)($_SESSION[self::KEY] ?? '');
214
+ }
215
+
216
+ public static function validate(?string $token): bool
217
+ {
218
+ return is_string($token) && hash_equals(self::token(), $token);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * JWT service using firebase/php-jwt when available
224
+ */
225
+ final class JwtService
226
+ {
227
+ public static function sign(array $payload, ?int $ttlSeconds = 3600): string
228
+ {
229
+ $now = time();
230
+ $payload = array_merge([
231
+ 'iat' => $now,
232
+ 'nbf' => $now,
233
+ 'exp' => $now + (int)$ttlSeconds,
234
+ 'iss' => $_SERVER['HTTP_HOST'] ?? 'localhost',
235
+ ], $payload);
236
+
237
+ $secret = Env::get('JWT_SECRET');
238
+ if (class_exists(\Firebase\JWT\JWT::class)) {
239
+ return \Firebase\JWT\JWT::encode($payload, $secret, 'HS256');
240
+ }
241
+ // Minimal fallback: base64 signature (NOT recommended for production if library missing)
242
+ $header = base64_encode(json_encode(['alg' => 'HS256','typ' => 'JWT']));
243
+ $body = base64_encode(json_encode($payload));
244
+ $sig = rtrim(strtr(base64_encode(hash_hmac('sha256', "$header.$body", $secret, true)), '+/', '-_'), '=');
245
+ return "$header.$body.$sig";
246
+ }
247
+ }
248
+
249
+ /**
250
+ * PDO connection provider + migrations
251
+ */
252
+ final class DB
253
+ {
254
+ private static ?PDO $pdo = null;
255
+
256
+ public static function pdo(): PDO
257
+ {
258
+ if (self::$pdo) return self::$pdo;
259
+ $url = Env::get('DATABASE_URL');
260
+
261
+ try {
262
+ if ($url && str_starts_with($url, 'mysql://')) {
263
+ // mysql://user:pass@host:port/db
264
+ $parts = parse_url($url);
265
+ $user = $parts['user'] ?? '';
266
+ $pass = $parts['pass'] ?? '';
267
+ $host = $parts['host'] ?? '127.0.0.1';
268
+ $port = $parts['port'] ?? 3306;
269
+ $db = ltrim($parts['path'] ?? '/softedge', '/');
270
+ $dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
271
+ $pdo = new PDO($dsn, $user, $pass, [
272
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
273
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
274
+ PDO::ATTR_EMULATE_PREPARES => false,
275
+ ]);
276
+ self::$pdo = $pdo;
277
+ } else {
278
+ // Default to SQLite file in /storage
279
+ $dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'storage';
280
+ if (!is_dir($dir)) @mkdir($dir, 0775, true);
281
+ $path = $dir . DIRECTORY_SEPARATOR . 'softedge.sqlite';
282
+ $dsn = 'sqlite:' . $path;
283
+ $pdo = new PDO($dsn, null, null, [
284
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
285
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
286
+ PDO::ATTR_EMULATE_PREPARES => false,
287
+ ]);
288
+ // Ensures WAL for better concurrency
289
+ $pdo->exec('PRAGMA journal_mode = WAL');
290
+ $pdo->exec('PRAGMA foreign_keys = ON');
291
+ self::$pdo = $pdo;
292
+ }
293
+ } catch (PDOException $e) {
294
+ http_response_code(500);
295
+ die('Database connection error');
296
+ }
297
+ return self::$pdo;
298
+ }
299
+
300
+ public static function migrate(): void
301
+ {
302
+ $pdo = self::pdo();
303
+ $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
304
+
305
+ if ($driver === 'sqlite') {
306
+ $pdo->exec(<<<SQL
307
+ CREATE TABLE IF NOT EXISTS users (
308
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
309
+ email TEXT NOT NULL UNIQUE,
310
+ password TEXT NOT NULL,
311
+ name TEXT,
312
+ role TEXT DEFAULT 'user',
313
+ status TEXT DEFAULT 'active',
314
+ last_login TEXT,
315
+ last_ip TEXT,
316
+ login_attempts INTEGER DEFAULT 0,
317
+ locked_until TEXT,
318
+ created_at TEXT DEFAULT (datetime('now'))
319
+ );
320
+ SQL);
321
+
322
+ $pdo->exec(<<<SQL
323
+ CREATE TABLE IF NOT EXISTS contact_submissions (
324
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
325
+ name TEXT,
326
+ email TEXT,
327
+ phone TEXT,
328
+ company TEXT,
329
+ subject TEXT,
330
+ message TEXT NOT NULL,
331
+ status TEXT DEFAULT 'new',
332
+ created_at TEXT DEFAULT (datetime('now'))
333
+ );
334
+ SQL);
335
+ } else {
336
+ // Basic MySQL-compatible schema (minimal)
337
+ $pdo->exec(<<<SQL
338
+ CREATE TABLE IF NOT EXISTS users (
339
+ id INT AUTO_INCREMENT PRIMARY KEY,
340
+ email VARCHAR(255) NOT NULL UNIQUE,
341
+ password VARCHAR(255) NOT NULL,
342
+ name VARCHAR(255),
343
+ role VARCHAR(50) DEFAULT 'user',
344
+ status VARCHAR(50) DEFAULT 'active',
345
+ last_login DATETIME NULL,
346
+ last_ip VARCHAR(45) NULL,
347
+ login_attempts INT DEFAULT 0,
348
+ locked_until DATETIME NULL,
349
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
350
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
351
+ SQL);
352
+
353
+ $pdo->exec(<<<SQL
354
+ CREATE TABLE IF NOT EXISTS contact_submissions (
355
+ id INT AUTO_INCREMENT PRIMARY KEY,
356
+ name VARCHAR(255),
357
+ email VARCHAR(255),
358
+ phone VARCHAR(20),
359
+ company VARCHAR(255),
360
+ subject VARCHAR(500),
361
+ message TEXT NOT NULL,
362
+ status VARCHAR(50) DEFAULT 'new',
363
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
364
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
365
+ SQL);
366
+ }
367
+ }
368
+
369
+ public static function seedAdmin(): void
370
+ {
371
+ $pdo = self::pdo();
372
+ $email = Env::get('ADMIN_EMAIL', 'admin@softedge.com');
373
+ $name = Env::get('ADMIN_NAME', 'Admin');
374
+ $pass = Env::get('ADMIN_PASSWORD', 'Admin@123456');
375
+ $hash = password_hash($pass, PASSWORD_BCRYPT, ['cost' => 10]);
376
+
377
+ $stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
378
+ $stmt->execute([':email' => $email]);
379
+ if (!$stmt->fetch()) {
380
+ $ins = $pdo->prepare('INSERT INTO users (email, password, name, role, status) VALUES (:email,:password,:name,\'super_admin\',\'active\')');
381
+ $ins->execute([':email' => $email, ':password' => $hash, ':name' => $name]);
382
+ }
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Authentication service
388
+ */
389
+ final class AuthService
390
+ {
391
+ public static function login(string $email, string $password): array
392
+ {
393
+ $pdo = DB::pdo();
394
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '';
395
+ $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
396
+
397
+ // Rate limit by IP & email
398
+ $key = 'login:' . $ip . ':' . strtolower($email);
399
+ if (!RateLimiter::allow($key, Env::int('RATE_LIMIT_LOGIN', 5), 60)) {
400
+ return ['ok' => false, 'status' => 429, 'error' => 'Muitas tentativas. Tente novamente mais tarde.'];
401
+ }
402
+
403
+ $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
404
+ $stmt->execute([':email' => $email]);
405
+ $user = $stmt->fetch();
406
+ if (!$user) {
407
+ return ['ok' => false, 'status' => 401, 'error' => 'Credenciais inválidas'];
408
+ }
409
+
410
+ // Check lockout
411
+ if (!empty($user['locked_until'])) {
412
+ $now = time();
413
+ $locked = strtotime((string)$user['locked_until']);
414
+ if ($locked && $locked > $now) {
415
+ return ['ok' => false, 'status' => 423, 'error' => 'Conta temporariamente bloqueada'];
416
+ }
417
+ }
418
+
419
+ if (!password_verify($password, (string)$user['password'])) {
420
+ // increment attempts
421
+ $attempts = (int)$user['login_attempts'] + 1;
422
+ $lockFor = 0;
423
+ $max = Env::int('MAX_LOGIN_ATTEMPTS', 5);
424
+ if ($attempts >= $max) {
425
+ $lockSeconds = Env::int('LOCKOUT_SECONDS', 900);
426
+ $lockFor = time() + $lockSeconds;
427
+ }
428
+ $pdo->prepare('UPDATE users SET login_attempts = :a, locked_until = :l WHERE id = :id')
429
+ ->execute([':a' => $attempts, ':l' => $lockFor ? date('Y-m-d H:i:s', $lockFor) : null, ':id' => $user['id']]);
430
+ return ['ok' => false, 'status' => 401, 'error' => 'Credenciais inválidas'];
431
+ }
432
+
433
+ // Reset attempts and update metadata
434
+ $pdo->prepare('UPDATE users SET login_attempts = 0, locked_until = NULL, last_login = :ll, last_ip = :ip WHERE id = :id')
435
+ ->execute([':ll' => date('Y-m-d H:i:s'), ':ip' => $ip, ':id' => $user['id']]);
436
+
437
+ // Session security
438
+ self::secureSession();
439
+ $_SESSION['uid'] = (int)$user['id'];
440
+ $_SESSION['role'] = (string)$user['role'];
441
+ session_regenerate_id(true);
442
+
443
+ // Optional JWT for API usage
444
+ $jwt = JwtService::sign(['sub' => $user['id'], 'email' => $user['email'], 'role' => $user['role']], Env::int('JWT_TTL', 3600));
445
+
446
+ return [
447
+ 'ok' => true,
448
+ 'status' => 200,
449
+ 'user' => [
450
+ 'id' => (int)$user['id'],
451
+ 'email' => (string)$user['email'],
452
+ 'name' => (string)($user['name'] ?? ''),
453
+ 'role' => (string)$user['role'],
454
+ ],
455
+ 'token' => $jwt,
456
+ ];
457
+ }
458
+
459
+ public static function logout(): void
460
+ {
461
+ if (session_status() === PHP_SESSION_ACTIVE) {
462
+ $_SESSION = [];
463
+ if (ini_get('session.use_cookies')) {
464
+ $params = session_get_cookie_params();
465
+ setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
466
+ }
467
+ session_destroy();
468
+ }
469
+ }
470
+
471
+ public static function secureSession(): void
472
+ {
473
+ if (session_status() === PHP_SESSION_NONE) {
474
+ $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
475
+ session_set_cookie_params([
476
+ 'lifetime' => Env::int('SESSION_LIFETIME', 86400),
477
+ 'path' => '/',
478
+ 'domain' => '',
479
+ 'secure' => $secure,
480
+ 'httponly' => true,
481
+ 'samesite' => 'Lax',
482
+ ]);
483
+ session_start();
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Application bootstrapper
490
+ */
491
+ final class Bootstrap
492
+ {
493
+ public static function init(): void
494
+ {
495
+ // Timezone
496
+ date_default_timezone_set(Env::get('TIMEZONE', 'Africa/Luanda'));
497
+
498
+ // Start secure session
499
+ AuthService::secureSession();
500
+
501
+ // CSRF token
502
+ Csrf::ensure();
503
+
504
+ // Security headers
505
+ SecurityHeaders::apply();
506
+
507
+ // Database and migrations
508
+ DB::migrate();
509
+
510
+ // Seed admin user if missing
511
+ DB::seedAdmin();
512
+ }
513
+ }
src/EmailService.php ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SoftEdge;
4
+
5
+ use PHPMailer\PHPMailer\PHPMailer;
6
+ use PHPMailer\PHPMailer\Exception;
7
+
8
+ /**
9
+ * Email Service for SoftEdge Corporation
10
+ * Handles contact form submissions and email sending
11
+ */
12
+ class EmailService
13
+ {
14
+ private PHPMailer $mailer;
15
+ private array $config;
16
+
17
+ public function __construct()
18
+ {
19
+ $this->config = $this->loadConfig();
20
+ $this->setupMailer();
21
+ }
22
+
23
+ /**
24
+ * Load email configuration from environment
25
+ */
26
+ private function loadConfig(): array
27
+ {
28
+ return [
29
+ 'host' => $_ENV['SMTP_HOST'] ?? 'smtp.gmail.com',
30
+ 'port' => (int)($_ENV['SMTP_PORT'] ?? 587),
31
+ 'username' => $_ENV['SMTP_USERNAME'] ?? '',
32
+ 'password' => $_ENV['SMTP_PASSWORD'] ?? '',
33
+ 'encryption' => $_ENV['SMTP_ENCRYPTION'] ?? 'tls',
34
+ 'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'softedgecorporation@gmail.com',
35
+ 'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'SoftEdge Corporation'
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * Setup PHPMailer instance
41
+ */
42
+ private function setupMailer(): void
43
+ {
44
+ $this->mailer = new PHPMailer(true);
45
+
46
+ // Server settings
47
+ $this->mailer->isSMTP();
48
+ $this->mailer->Host = $this->config['host'];
49
+ $this->mailer->SMTPAuth = true;
50
+ $this->mailer->Username = $this->config['username'];
51
+ $this->mailer->Password = $this->config['password'];
52
+ $this->mailer->SMTPSecure = $this->config['encryption'];
53
+ $this->mailer->Port = $this->config['port'];
54
+
55
+ // Sender
56
+ $this->mailer->setFrom($this->config['from_email'], $this->config['from_name']);
57
+
58
+ // Encoding and charset
59
+ $this->mailer->CharSet = 'UTF-8';
60
+ $this->mailer->Encoding = 'base64';
61
+ }
62
+
63
+ /**
64
+ * Send contact email
65
+ */
66
+ public function sendContactEmail(array $data): bool
67
+ {
68
+ try {
69
+ // Validate required fields
70
+ $this->validateContactData($data);
71
+
72
+ // Recipients
73
+ $this->mailer->addAddress('softedgecorporation@gmail.com', 'SoftEdge Corporation');
74
+
75
+ // Content
76
+ $this->mailer->isHTML(false);
77
+ $this->mailer->Subject = '🚀 Novo Contato do Site - ' . $data['nome'];
78
+ $this->mailer->Body = $this->buildContactEmailBody($data);
79
+
80
+ // Send email
81
+ $result = $this->mailer->send();
82
+
83
+ // Log success
84
+ error_log("Email sent successfully to: softedgecorporation@gmail.com from: {$data['email']}");
85
+
86
+ return $result;
87
+
88
+ } catch (Exception $e) {
89
+ error_log("Email sending failed: " . $this->mailer->ErrorInfo);
90
+ throw new \RuntimeException('Erro ao enviar email: ' . $e->getMessage());
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Validate contact form data
96
+ */
97
+ private function validateContactData(array $data): void
98
+ {
99
+ $required = ['nome', 'email', 'mensagem'];
100
+ $missing = [];
101
+
102
+ foreach ($required as $field) {
103
+ if (empty(trim($data[$field] ?? ''))) {
104
+ $missing[] = $field;
105
+ }
106
+ }
107
+
108
+ if (!empty($missing)) {
109
+ throw new \InvalidArgumentException('Campos obrigatórios não preenchidos: ' . implode(', ', $missing));
110
+ }
111
+
112
+ if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
113
+ throw new \InvalidArgumentException('E-mail inválido');
114
+ }
115
+
116
+ // Additional validation
117
+ if (strlen($data['nome']) < 2) {
118
+ throw new \InvalidArgumentException('Nome deve ter pelo menos 2 caracteres');
119
+ }
120
+
121
+ if (strlen($data['mensagem']) < 10) {
122
+ throw new \InvalidArgumentException('Mensagem deve ter pelo menos 10 caracteres');
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Build contact email body
128
+ */
129
+ private function buildContactEmailBody(array $data): string
130
+ {
131
+ $empresa = $data['empresa'] ?? '(não informado)';
132
+ $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
133
+ $timestamp = date('d/m/Y H:i:s');
134
+
135
+ return "═══════════════════════════════════════\n" .
136
+ " NOVO CONTATO DO SITE SOFTEDGE\n" .
137
+ "═══════════════════════════════════════\n\n" .
138
+ "👤 NOME:\n {$data['nome']}\n\n" .
139
+ "📧 E-MAIL:\n {$data['email']}\n\n" .
140
+ "🏢 EMPRESA/PROJETO:\n {$empresa}\n\n" .
141
+ "💬 MENSAGEM:\n " . str_replace("\n", "\n ", $data['mensagem']) . "\n\n" .
142
+ "═══════════════════════════════════════\n" .
143
+ "📅 Data: {$timestamp}\n" .
144
+ "🌐 IP: {$ip}\n" .
145
+ "═══════════════════════════════════════\n";
146
+ }
147
+
148
+ /**
149
+ * Send notification email (for internal use)
150
+ */
151
+ public function sendNotification(string $subject, string $message): bool
152
+ {
153
+ try {
154
+ $this->mailer->clearAddresses();
155
+ $this->mailer->addAddress('softedgecorporation@gmail.com', 'SoftEdge Corporation');
156
+
157
+ $this->mailer->isHTML(false);
158
+ $this->mailer->Subject = '🔔 ' . $subject;
159
+ $this->mailer->Body = $message;
160
+
161
+ return $this->mailer->send();
162
+
163
+ } catch (Exception $e) {
164
+ error_log("Notification email failed: " . $this->mailer->ErrorInfo);
165
+ return false;
166
+ }
167
+ }
168
+ }
src/ReactLoader.php ADDED
File without changes
src/User.php ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SoftEdge;
4
+
5
+ use PDO;
6
+ use PDOException;
7
+
8
+ /**
9
+ * User Management Class
10
+ * Handles user registration, authentication, and profile management
11
+ */
12
+ class User
13
+ {
14
+ private PDO $db;
15
+ private array $config;
16
+
17
+ public function __construct(PDO $db = null)
18
+ {
19
+ $this->config = $this->loadConfig();
20
+ $this->db = $db ?? $this->getDatabaseConnection();
21
+ }
22
+
23
+ /**
24
+ * Load configuration
25
+ */
26
+ private function loadConfig(): array
27
+ {
28
+ return [
29
+ 'db_host' => $_ENV['DB_HOST'] ?? 'localhost',
30
+ 'db_name' => $_ENV['DB_NAME'] ?? 'softedge_db',
31
+ 'db_user' => $_ENV['DB_USER'] ?? 'root',
32
+ 'db_pass' => $_ENV['DB_PASS'] ?? '',
33
+ 'jwt_secret' => $_ENV['JWT_SECRET'] ?? 'your-jwt-secret-key',
34
+ 'google_client_id' => $_ENV['GOOGLE_CLIENT_ID'] ?? '',
35
+ 'google_client_secret' => $_ENV['GOOGLE_CLIENT_SECRET'] ?? '',
36
+ 'github_client_id' => $_ENV['GITHUB_CLIENT_ID'] ?? '',
37
+ 'github_client_secret' => $_ENV['GITHUB_CLIENT_SECRET'] ?? ''
38
+ ];
39
+ }
40
+
41
+ /**
42
+ * Get database connection
43
+ */
44
+ private function getDatabaseConnection(): PDO
45
+ {
46
+ try {
47
+ $dsn = "mysql:host={$this->config['db_host']};dbname={$this->config['db_name']};charset=utf8mb4";
48
+ $pdo = new PDO($dsn, $this->config['db_user'], $this->config['db_pass']);
49
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
50
+ $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
51
+ return $pdo;
52
+ } catch (PDOException $e) {
53
+ error_log("Database connection failed: " . $e->getMessage());
54
+ throw new \RuntimeException('Database connection failed');
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Create users table if it doesn't exist
60
+ */
61
+ public function createTables(): void
62
+ {
63
+ try {
64
+ // Users table
65
+ $this->db->exec("
66
+ CREATE TABLE IF NOT EXISTS users (
67
+ id INT AUTO_INCREMENT PRIMARY KEY,
68
+ name VARCHAR(255) NOT NULL,
69
+ email VARCHAR(255) UNIQUE NOT NULL,
70
+ password VARCHAR(255),
71
+ avatar VARCHAR(500),
72
+ provider VARCHAR(50) DEFAULT 'local',
73
+ provider_id VARCHAR(255),
74
+ role ENUM('user', 'admin') DEFAULT 'user',
75
+ email_verified BOOLEAN DEFAULT FALSE,
76
+ verification_token VARCHAR(255),
77
+ reset_token VARCHAR(255),
78
+ reset_expires DATETIME,
79
+ last_login DATETIME,
80
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
81
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
82
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
83
+ ");
84
+
85
+ // Page visits table
86
+ $this->db->exec("
87
+ CREATE TABLE IF NOT EXISTS page_visits (
88
+ id INT AUTO_INCREMENT PRIMARY KEY,
89
+ user_id INT,
90
+ page VARCHAR(255) NOT NULL,
91
+ ip_address VARCHAR(45),
92
+ user_agent TEXT,
93
+ referrer VARCHAR(500),
94
+ session_id VARCHAR(255),
95
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
97
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
98
+ ");
99
+
100
+ // User sessions table
101
+ $this->db->exec("
102
+ CREATE TABLE IF NOT EXISTS user_sessions (
103
+ id INT AUTO_INCREMENT PRIMARY KEY,
104
+ user_id INT NOT NULL,
105
+ session_token VARCHAR(255) UNIQUE NOT NULL,
106
+ ip_address VARCHAR(45),
107
+ user_agent TEXT,
108
+ expires_at DATETIME NOT NULL,
109
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
111
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
112
+ ");
113
+
114
+ } catch (PDOException $e) {
115
+ error_log("Table creation failed: " . $e->getMessage());
116
+ throw new \RuntimeException('Failed to create database tables');
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Register a new user
122
+ */
123
+ public function register(array $data): array
124
+ {
125
+ $this->validateRegistrationData($data);
126
+
127
+ try {
128
+ $this->db->beginTransaction();
129
+
130
+ // Check if email already exists
131
+ $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
132
+ $stmt->execute([$data['email']]);
133
+ if ($stmt->fetch()) {
134
+ throw new \InvalidArgumentException('Email já cadastrado');
135
+ }
136
+
137
+ // Hash password
138
+ $hashedPassword = password_hash($data['password'], PASSWORD_ARGON2ID);
139
+
140
+ // Generate verification token
141
+ $verificationToken = bin2hex(random_bytes(32));
142
+
143
+ // Insert user
144
+ $stmt = $this->db->prepare("
145
+ INSERT INTO users (name, email, password, verification_token, created_at)
146
+ VALUES (?, ?, ?, ?, NOW())
147
+ ");
148
+ $stmt->execute([
149
+ $data['name'],
150
+ $data['email'],
151
+ $hashedPassword,
152
+ $verificationToken
153
+ ]);
154
+
155
+ $userId = $this->db->lastInsertId();
156
+
157
+ $this->db->commit();
158
+
159
+ return [
160
+ 'success' => true,
161
+ 'user_id' => $userId,
162
+ 'verification_token' => $verificationToken,
163
+ 'message' => 'Usuário registrado com sucesso. Verifique seu email.'
164
+ ];
165
+
166
+ } catch (PDOException $e) {
167
+ $this->db->rollBack();
168
+ error_log("Registration failed: " . $e->getMessage());
169
+ throw new \RuntimeException('Erro ao registrar usuário');
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Authenticate user
175
+ */
176
+ public function login(string $email, string $password): array
177
+ {
178
+ try {
179
+ $stmt = $this->db->prepare("
180
+ SELECT id, name, email, password, role, email_verified
181
+ FROM users
182
+ WHERE email = ? AND provider = 'local'
183
+ ");
184
+ $stmt->execute([$email]);
185
+ $user = $stmt->fetch();
186
+
187
+ if (!$user || !password_verify($password, $user['password'])) {
188
+ throw new \InvalidArgumentException('Email ou senha incorretos');
189
+ }
190
+
191
+ if (!$user['email_verified']) {
192
+ throw new \InvalidArgumentException('Email não verificado. Verifique sua caixa de entrada.');
193
+ }
194
+
195
+ // Update last login
196
+ $stmt = $this->db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
197
+ $stmt->execute([$user['id']]);
198
+
199
+ // Create session
200
+ $sessionToken = $this->createSession($user['id']);
201
+
202
+ // Log page visit
203
+ $this->logPageVisit($user['id'], 'login', $_SERVER['HTTP_USER_AGENT'] ?? '');
204
+
205
+ return [
206
+ 'success' => true,
207
+ 'user' => [
208
+ 'id' => $user['id'],
209
+ 'name' => $user['name'],
210
+ 'email' => $user['email'],
211
+ 'role' => $user['role']
212
+ ],
213
+ 'session_token' => $sessionToken
214
+ ];
215
+
216
+ } catch (PDOException $e) {
217
+ error_log("Login failed: " . $e->getMessage());
218
+ throw new \RuntimeException('Erro ao fazer login');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Social login (Google, GitHub)
224
+ */
225
+ public function socialLogin(string $provider, array $profile): array
226
+ {
227
+ try {
228
+ $this->db->beginTransaction();
229
+
230
+ // Check if user exists
231
+ $stmt = $this->db->prepare("
232
+ SELECT id, name, email, role, email_verified
233
+ FROM users
234
+ WHERE provider = ? AND provider_id = ?
235
+ ");
236
+ $stmt->execute([$provider, $profile['id']]);
237
+ $user = $stmt->fetch();
238
+
239
+ if (!$user) {
240
+ // Create new user
241
+ $stmt = $this->db->prepare("
242
+ INSERT INTO users (name, email, avatar, provider, provider_id, email_verified, created_at)
243
+ VALUES (?, ?, ?, ?, ?, TRUE, NOW())
244
+ ");
245
+ $stmt->execute([
246
+ $profile['name'],
247
+ $profile['email'],
248
+ $profile['avatar'] ?? null,
249
+ $provider,
250
+ $profile['id']
251
+ ]);
252
+
253
+ $userId = $this->db->lastInsertId();
254
+
255
+ $user = [
256
+ 'id' => $userId,
257
+ 'name' => $profile['name'],
258
+ 'email' => $profile['email'],
259
+ 'role' => 'user',
260
+ 'email_verified' => true
261
+ ];
262
+ }
263
+
264
+ // Update last login
265
+ $stmt = $this->db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
266
+ $stmt->execute([$user['id']]);
267
+
268
+ // Create session
269
+ $sessionToken = $this->createSession($user['id']);
270
+
271
+ // Log page visit
272
+ $this->logPageVisit($user['id'], 'social_login', $_SERVER['HTTP_USER_AGENT'] ?? '');
273
+
274
+ $this->db->commit();
275
+
276
+ return [
277
+ 'success' => true,
278
+ 'user' => $user,
279
+ 'session_token' => $sessionToken
280
+ ];
281
+
282
+ } catch (PDOException $e) {
283
+ $this->db->rollBack();
284
+ error_log("Social login failed: " . $e->getMessage());
285
+ throw new \RuntimeException('Erro ao fazer login social');
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Create user session
291
+ */
292
+ private function createSession(int $userId): string
293
+ {
294
+ $sessionToken = bin2hex(random_bytes(32));
295
+ $expiresAt = date('Y-m-d H:i:s', strtotime('+24 hours'));
296
+
297
+ $stmt = $this->db->prepare("
298
+ INSERT INTO user_sessions (user_id, session_token, ip_address, user_agent, expires_at, created_at)
299
+ VALUES (?, ?, ?, ?, ?, NOW())
300
+ ");
301
+ $stmt->execute([
302
+ $userId,
303
+ $sessionToken,
304
+ $_SERVER['REMOTE_ADDR'] ?? '',
305
+ $_SERVER['HTTP_USER_AGENT'] ?? '',
306
+ $expiresAt
307
+ ]);
308
+
309
+ return $sessionToken;
310
+ }
311
+
312
+ /**
313
+ * Validate session
314
+ */
315
+ public function validateSession(string $sessionToken): ?array
316
+ {
317
+ try {
318
+ $stmt = $this->db->prepare("
319
+ SELECT u.id, u.name, u.email, u.role, u.email_verified, s.expires_at
320
+ FROM user_sessions s
321
+ JOIN users u ON s.user_id = u.id
322
+ WHERE s.session_token = ? AND s.expires_at > NOW()
323
+ ");
324
+ $stmt->execute([$sessionToken]);
325
+ $result = $stmt->fetch();
326
+
327
+ return $result ?: null;
328
+
329
+ } catch (PDOException $e) {
330
+ error_log("Session validation failed: " . $e->getMessage());
331
+ return null;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Log page visit
337
+ */
338
+ public function logPageVisit(?int $userId, string $page, string $userAgent = ''): void
339
+ {
340
+ try {
341
+ $stmt = $this->db->prepare("
342
+ INSERT INTO page_visits (user_id, page, ip_address, user_agent, referrer, session_id, created_at)
343
+ VALUES (?, ?, ?, ?, ?, ?, NOW())
344
+ ");
345
+ $stmt->execute([
346
+ $userId,
347
+ $page,
348
+ $_SERVER['REMOTE_ADDR'] ?? '',
349
+ $userAgent,
350
+ $_SERVER['HTTP_REFERER'] ?? '',
351
+ session_id()
352
+ ]);
353
+ } catch (PDOException $e) {
354
+ error_log("Page visit logging failed: " . $e->getMessage());
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Get admin statistics
360
+ */
361
+ public function getAdminStats(): array
362
+ {
363
+ try {
364
+ // Total users
365
+ $stmt = $this->db->query("SELECT COUNT(*) as total FROM users");
366
+ $totalUsers = $stmt->fetch()['total'];
367
+
368
+ // Total page visits
369
+ $stmt = $this->db->query("SELECT COUNT(*) as total FROM page_visits");
370
+ $totalVisits = $stmt->fetch()['total'];
371
+
372
+ // Recent visits (last 30 days)
373
+ $stmt = $this->db->prepare("
374
+ SELECT COUNT(*) as total
375
+ FROM page_visits
376
+ WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
377
+ ");
378
+ $stmt->execute();
379
+ $recentVisits = $stmt->fetch()['total'];
380
+
381
+ // Top pages
382
+ $stmt = $this->db->prepare("
383
+ SELECT page, COUNT(*) as visits
384
+ FROM page_visits
385
+ GROUP BY page
386
+ ORDER BY visits DESC
387
+ LIMIT 10
388
+ ");
389
+ $stmt->execute();
390
+ $topPages = $stmt->fetchAll();
391
+
392
+ // Recent users
393
+ $stmt = $this->db->prepare("
394
+ SELECT id, name, email, created_at
395
+ FROM users
396
+ ORDER BY created_at DESC
397
+ LIMIT 10
398
+ ");
399
+ $stmt->execute();
400
+ $recentUsers = $stmt->fetchAll();
401
+
402
+ return [
403
+ 'total_users' => $totalUsers,
404
+ 'total_visits' => $totalVisits,
405
+ 'recent_visits' => $recentVisits,
406
+ 'top_pages' => $topPages,
407
+ 'recent_users' => $recentUsers
408
+ ];
409
+
410
+ } catch (PDOException $e) {
411
+ error_log("Admin stats failed: " . $e->getMessage());
412
+ return [];
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Validate registration data
418
+ */
419
+ private function validateRegistrationData(array $data): void
420
+ {
421
+ $required = ['name', 'email', 'password'];
422
+ foreach ($required as $field) {
423
+ if (empty($data[$field])) {
424
+ throw new \InvalidArgumentException("Campo {$field} é obrigatório");
425
+ }
426
+ }
427
+
428
+ if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
429
+ throw new \InvalidArgumentException('Email inválido');
430
+ }
431
+
432
+ if (strlen($data['name']) < 2) {
433
+ throw new \InvalidArgumentException('Nome deve ter pelo menos 2 caracteres');
434
+ }
435
+
436
+ if (strlen($data['password']) < 8) {
437
+ throw new \InvalidArgumentException('Senha deve ter pelo menos 8 caracteres');
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Check if user is admin
443
+ */
444
+ public function isAdmin(int $userId): bool
445
+ {
446
+ try {
447
+ $stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?");
448
+ $stmt->execute([$userId]);
449
+ $user = $stmt->fetch();
450
+
451
+ return $user && $user['role'] === 'admin';
452
+
453
+ } catch (PDOException $e) {
454
+ error_log("Admin check failed: " . $e->getMessage());
455
+ return false;
456
+ }
457
+ }
458
+ }
src/_init_.php ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SoftEdge Corporation - Initialization File
5
+ * Sets up the application environment and autoloading
6
+ */
7
+
8
+ // Start session if not already started
9
+ if (session_status() === PHP_SESSION_NONE) {
10
+ session_start();
11
+ }
12
+
13
+ // Set timezone
14
+ date_default_timezone_set('Africa/Luanda');
15
+
16
+ // Error handling for production
17
+ if (getenv('APP_ENV') === 'production') {
18
+ ini_set('display_errors', 0);
19
+ ini_set('display_startup_errors', 0);
20
+ error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
21
+ } else {
22
+ ini_set('display_errors', 1);
23
+ ini_set('display_startup_errors', 1);
24
+ error_reporting(E_ALL);
25
+ }
26
+
27
+ // Load environment variables if .env exists
28
+ if (file_exists(__DIR__ . '/../.env')) {
29
+ $dotenv = \Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
30
+ $dotenv->load();
31
+ }
32
+
33
+ // Set default environment variables
34
+ $_ENV['APP_NAME'] = $_ENV['APP_NAME'] ?? 'SoftEdge Corporation';
35
+ $_ENV['APP_URL'] = $_ENV['APP_URL'] ?? 'https://softedge-corporation.up.railway.app';
36
+ $_ENV['APP_ENV'] = $_ENV['APP_ENV'] ?? 'production';
37
+
38
+ // Security headers
39
+ header('X-Content-Type-Options: nosniff');
40
+ header('X-Frame-Options: DENY');
41
+ header('X-XSS-Protection: 1; mode=block');
42
+ header('Referrer-Policy: strict-origin-when-cross-origin');
43
+
44
+ // Content Security Policy
45
+ header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'");
46
+
47
+ // Create logs directory if it doesn't exist
48
+ $logsDir = __DIR__ . '/../logs';
49
+ if (!is_dir($logsDir)) {
50
+ mkdir($logsDir, 0755, true);
51
+ }
52
+
53
+ // Set custom error log
54
+ ini_set('error_log', $logsDir . '/php_errors.log');
55
+
56
+ // Function to get base URL
57
+ function getBaseUrl(): string
58
+ {
59
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
60
+ $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
61
+ $port = $_SERVER['SERVER_PORT'] ?? 80;
62
+
63
+ // Don't include port if it's the default for the protocol
64
+ if (($protocol === 'http' && $port == 80) || ($protocol === 'https' && $port == 443)) {
65
+ return $protocol . '://' . $host;
66
+ }
67
+
68
+ return $protocol . '://' . $host . ':' . $port;
69
+ }
70
+
71
+ // Function to get current URL
72
+ function getCurrentUrl(): string
73
+ {
74
+ return getBaseUrl() . $_SERVER['REQUEST_URI'];
75
+ }
76
+
77
+ // Function to redirect
78
+ function redirect(string $url, int $statusCode = 302): void
79
+ {
80
+ header('Location: ' . $url, true, $statusCode);
81
+ exit;
82
+ }
83
+
84
+ // Function to sanitize input
85
+ function sanitizeInput(string $input): string
86
+ {
87
+ return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
88
+ }
89
+
90
+ // Function to generate CSRF token
91
+ function generateCsrfToken(): string
92
+ {
93
+ if (empty($_SESSION['csrf_token'])) {
94
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
95
+ }
96
+ return $_SESSION['csrf_token'];
97
+ }
98
+
99
+ // Function to validate CSRF token
100
+ function validateCsrfToken(string $token): bool
101
+ {
102
+ return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
103
+ }
104
+
105
+ // Function to log activity
106
+ function logActivity(string $message, string $level = 'INFO'): void
107
+ {
108
+ $logFile = __DIR__ . '/../logs/activity.log';
109
+ $timestamp = date('Y-m-d H:i:s');
110
+ $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
111
+ $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
112
+
113
+ $logEntry = "[$timestamp] [$level] [$ip] [$userAgent] $message" . PHP_EOL;
114
+
115
+ file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
116
+ }
117
+
118
+ // Function to check if user is rate limited
119
+ function isRateLimited(string $identifier, int $maxRequests = 5, int $timeWindow = 300): bool
120
+ {
121
+ $rateLimitFile = __DIR__ . "/../logs/rate_limit_{$identifier}.txt";
122
+ $currentTime = time();
123
+
124
+ // Read existing requests
125
+ $requests = [];
126
+ if (file_exists($rateLimitFile)) {
127
+ $requests = json_decode(file_get_contents($rateLimitFile), true) ?? [];
128
+ }
129
+
130
+ // Filter out old requests
131
+ $requests = array_filter($requests, function($timestamp) use ($currentTime, $timeWindow) {
132
+ return ($currentTime - $timestamp) < $timeWindow;
133
+ });
134
+
135
+ // Check if rate limit exceeded
136
+ if (count($requests) >= $maxRequests) {
137
+ return true;
138
+ }
139
+
140
+ // Add current request
141
+ $requests[] = $currentTime;
142
+
143
+ // Save updated requests
144
+ file_put_contents($rateLimitFile, json_encode($requests));
145
+
146
+ return false;
147
+ }
148
+
149
+ // Initialize application
150
+ logActivity("Application initialized");
151
+
152
+ // Autoload classes (Composer will handle this)
153
+ require_once __DIR__ . '/../vendor/autoload.php';