This commit is contained in:
2026-02-04 23:31:53 +01:00
commit cd59ccd99b
2401 changed files with 56808 additions and 0 deletions

31
pages/auth/forgot().php Normal file
View File

@@ -0,0 +1,31 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Session;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\I18n;
use MintyPHP\Service\PasswordResetService;
$errors = [];
$email = trim((string) ($_POST['email'] ?? ''));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!Session::checkCsrfToken()) {
$errors[] = t('Form expired, please try again');
} elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = t('Please enter a valid email address');
} else {
PasswordResetService::requestReset($email);
$_SESSION['password_reset_email'] = $email;
Flash::success(
t('If the email exists, a verification code has been sent.'),
'password/verify',
'reset_requested'
);
Router::redirect('password/verify');
}
}
Buffer::set('title', t('Forgot password'));

View File

@@ -0,0 +1,43 @@
<?php
/**
* @var array<int, string> $errors
* @var string $email
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
Buffer::set('title', t('Forgot password'));
?>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Forgot password')); ?></h1>
<p><?php e(t('Enter your email address and we will send you a verification code.')); ?></p>
</hgroup>
</header>
<form method="post">
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
</label>
<?php if (!empty($errors)): ?>
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<button type="submit"><?php e(t('Send code')); ?></button>
<?php Session::getCsrfInput(); ?>
</form>
<hr>
<p class="text-align-center">
<a href="<?php e(lurl('login')); ?>"><?php e(t('Back to Login')); ?></a>
</p>
</article>

19
pages/auth/login().php Normal file
View File

@@ -0,0 +1,19 @@
<?php
use MintyPHP\Service\AuthService;
use MintyPHP\Support\Flash;
use MintyPHP\Router;
if (!empty($_SESSION['user']['id'])) {
if (Flash::has()) {
Flash::keep();
}
Router::redirect("admin");
}
if (isset($_POST['email'])) {
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
$remember = !empty($_POST['remember']);
AuthService::loginAndRedirect($email, $password, 'admin', 'login', $remember);
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @var mixed|null $error
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
Buffer::set('title', t('Login'));
?>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Login')); ?></h1>
<p><?php e(t('Please enter your credentials')); ?></p>
</hgroup>
</header>
<form method="post">
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" />
</label>
<label for="password">
<span><?php e(t('Password')); ?></span>
<input required type="password" name="password" id="password" />
</label>
<p>
<label class="app-field inline">
<input type="checkbox" name="remember" value="1">
<span><?php e(t('Remember me')); ?></span>
</label>
</p>
<p>
<button type="submit"><?php e(t('Login')); ?></button>
</p>
<p>
<a href="<?php e(lurl('password/forgot')); ?>"><?php e(t('Forgot password')); ?>?</a>
</p>
<?php Session::getCsrfInput(); ?>
</form>
<hr>
<p class="text-align-center"><?php e(t('No account yet')); ?>? <a href="register"><?php e(t('Register')); ?></a></p>
</article>

5
pages/auth/logout().php Normal file
View File

@@ -0,0 +1,5 @@
<?php
use MintyPHP\Service\AuthService;
AuthService::logoutAndRedirect('login');

31
pages/auth/register().php Normal file
View File

@@ -0,0 +1,31 @@
<?php
use MintyPHP\Service\AuthService;
use MintyPHP\Support\Flash;
use MintyPHP\Router;
$error = false;
if (isset($_POST['email'])) {
$firstName = trim((string) ($_POST['first_name'] ?? ''));
$lastName = trim((string) ($_POST['last_name'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
$password2 = (string) ($_POST['password2'] ?? '');
$result = AuthService::register([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'password' => $password,
'password2' => $password2,
]);
if (!($result['ok'] ?? false)) {
$error = $result['error'] ?? t('User can not be registered');
} else {
// Store email in session for verify-email page
$_SESSION['email_verification_email'] = $result['email'] ?? $email;
Flash::success(t('Registration successful! Please check your email for the verification code.'), 'verify-email', 'registration_success');
Router::redirect('verify-email');
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* @var mixed|null $error
* @var mixed|null $firstName
* @var mixed|null $lastName
* @var mixed|null $email
* @var mixed|null $password
* @var mixed|null $password2
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
use MintyPHP\Service\UserService;
Buffer::set('title', t('Register'));
?>
<a role="button" class="back_to_login small secondary outline" href="/login"><i
class="bi bi-arrow-left"></i> <?php e(t('Back to Login')); ?></a>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Register')); ?></h1>
<p><?php e(t('Create your account')); ?></p>
</hgroup>
</header>
<form method="post">
<label for="first_name">
<span><?php e(t('First name')); ?></span>
<input required type="text" name="first_name" id="first_name" />
</label>
<label for="last_name">
<span><?php e(t('Last name')); ?></span>
<input required type="text" name="last_name" id="last_name" />
</label>
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" />
</label>
<fieldset>
<legend>
<small>
<?php e(t('Password')); ?>
</small>
</legend>
<label for="password">
<span><?php e(t('Password')); ?></span>
<input required type="password" name="password" id="password"
minlength="<?php e(UserService::passwordMinLength()); ?>" autocomplete="new-password" />
</label>
<label for="password2">
<span><?php e(t('Password (again)')); ?></span>
<input required type="password" name="password2" id="password2"
minlength="<?php e(UserService::passwordMinLength()); ?>" autocomplete="new-password" />
</label>
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
data-email-input="#email" data-min-length="<?php e(UserService::passwordMinLength()); ?>">
<strong><?php e(t('Password requirements')); ?></strong>
<ul class="form-hint-list">
<?php foreach (UserService::passwordHints() as $hint): ?>
<li data-rule="<?php e($hint['rule']); ?>"><?php e($hint['text']); ?></li>
<?php endforeach; ?>
<li data-rule="match"><?php e(t('Passwords must match')); ?></li>
</ul>
</div>
</fieldset>
<button type="submit"><?php e(t('Register')); ?></button>
<?php if (!empty($error)): ?>
<div class="notice" data-variant="error"><?php e($error); ?></div>
<?php endif; ?>
<?php Session::getCsrfInput(); ?>
</form>
</article>

40
pages/auth/reset().php Normal file
View File

@@ -0,0 +1,40 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Session;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\I18n;
use MintyPHP\Service\PasswordResetService;
use MintyPHP\Service\UserService;
$errors = [];
$password = (string) ($_POST['password'] ?? '');
$password2 = (string) ($_POST['password2'] ?? '');
$resetId = (int) ($_SESSION['password_reset_id'] ?? 0);
if ($resetId <= 0) {
Router::redirect('password/forgot');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!Session::checkCsrfToken()) {
$errors = [t('Form expired, please try again')];
} else {
$result = PasswordResetService::resetPassword($resetId, $password, $password2);
if ($result['ok'] ?? false) {
unset($_SESSION['password_reset_id']);
unset($_SESSION['password_reset_email']);
Flash::success(t('Password updated'), 'login', 'password_updated');
Router::redirect('login');
} else {
$errors = $result['errors'] ?? [t('Password can not be updated')];
}
}
}
$passwordHints = UserService::passwordHints();
$passwordMinLength = UserService::passwordMinLength();
Buffer::set('title', t('Reset password'));

View File

@@ -0,0 +1,69 @@
<?php
/**
* @var array<int, string> $errors
* @var array<int, array{label:string,key:string}> $passwordHints
* @var int $passwordMinLength
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
Buffer::set('title', t('Reset password'));
?>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Reset password')); ?></h1>
<p><?php e(t('Choose a new password for your account.')); ?></p>
</hgroup>
</header>
<form method="post">
<fieldset>
<legend>
<small>
<?php e(t('Password')); ?>
</small>
</legend>
<label for="password">
<span><?php e(t('Password')); ?></span>
<input required type="password" name="password" id="password"
minlength="<?php e($passwordMinLength ?? 8); ?>" />
</label>
<label for="password2">
<span><?php e(t('Password (again)')); ?></span>
<input required type="password" name="password2" id="password2"
minlength="<?php e($passwordMinLength ?? 8); ?>" />
</label>
<?php if (!empty($passwordHints)): ?>
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
data-min-length="<?php e($passwordMinLength ?? 8); ?>">
<strong><?php e(t('Password requirements')); ?></strong>
<ul class="form-hint-list">
<?php foreach ($passwordHints as $hint): ?>
<li data-rule="<?php e($hint['rule']); ?>"><?php e($hint['text']); ?></li>
<?php endforeach; ?>
<li data-rule="match"><?php e(t('Passwords must match')); ?></li>
</ul>
</div>
<?php endif; ?>
</fieldset>
<?php if (!empty($errors)): ?>
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<button type="submit"><?php e(t('Reset password')); ?></button>
<?php Session::getCsrfInput(); ?>
</form>
<hr>
<p class="text-align-center">
<a href="<?php e(lurl('login')); ?>"><?php e(t('Back to Login')); ?></a>
</p>
</article>

35
pages/auth/verify().php Normal file
View File

@@ -0,0 +1,35 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Session;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\I18n;
use MintyPHP\Service\PasswordResetService;
$errors = [];
$email = trim((string) ($_POST['email'] ?? ($_SESSION['password_reset_email'] ?? '')));
$code = trim((string) ($_POST['code'] ?? ''));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!Session::checkCsrfToken()) {
$errors[] = t('Form expired, please try again');
} elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = t('Please enter a valid email address');
} elseif ($code === '' || !preg_match('/^\d{6}$/', $code)) {
$errors[] = t('Please enter the 6-digit code');
} else {
$result = PasswordResetService::verifyCode($email, $code);
if ($result['ok'] ?? false) {
$_SESSION['password_reset_id'] = (int) $result['reset_id'];
unset($_SESSION['password_reset_email']);
Flash::success(t('Code verified'), 'password/reset', 'reset_verified');
Router::redirect('password/reset');
} else {
$errors[] = t('Invalid or expired code');
}
}
}
Buffer::set('title', t('Verify code'));

View File

@@ -0,0 +1,51 @@
<?php
/**
* @var array<int, string> $errors
* @var string $email
* @var string $code
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
Buffer::set('title', t('Verify code'));
?>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Verify code')); ?></h1>
<p><?php e(t('Enter the verification code we sent to your email.')); ?></p>
</hgroup>
</header>
<form method="post">
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
</label>
<label for="code">
<span><?php e(t('Verification code')); ?></span>
<input required type="text" name="code" id="code" inputmode="numeric" pattern="\d{6}" maxlength="6"
value="<?php e($code ?? ''); ?>" />
</label>
<?php if (!empty($errors)): ?>
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<button type="submit"><?php e(t('Verify')); ?></button>
<?php Session::getCsrfInput(); ?>
</form>
<hr>
<p class="text-align-center">
<a href="<?php e(lurl('password/forgot')); ?>"><?php e(t('Send code again')); ?></a>
&nbsp;·&nbsp;
<a href="<?php e(lurl('login')); ?>"><?php e(t('Back to Login')); ?></a>
</p>
</article>

View File

@@ -0,0 +1,60 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Session;
use MintyPHP\Router;
use MintyPHP\Service\EmailVerificationService;
$errors = [];
$email = trim((string) ($_POST['email'] ?? ($_SESSION['email_verification_email'] ?? '')));
$code = trim((string) ($_POST['code'] ?? ''));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'verify';
if (!Session::checkCsrfToken()) {
$errors[] = t('Form expired, please try again');
} elseif ($action === 'resend') {
// Resend verification code
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = t('Please enter a valid email address');
} else {
$result = EmailVerificationService::resendVerification($email);
if ($result['ok'] ?? false) {
Flash::success(t('A new verification code has been sent.'), 'verify-email', 'code_resent');
Router::redirect('verify-email');
} elseif (($result['error'] ?? '') === 'already_verified') {
Flash::success(t('Email already verified. Please login.'), 'login', 'already_verified');
Router::redirect('login');
} else {
Flash::success(t('If the email exists, a new verification code has been sent.'), 'verify-email', 'code_resent');
Router::redirect('verify-email');
}
}
} elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = t('Please enter a valid email address');
} elseif ($code === '' || !preg_match('/^\d{6}$/', $code)) {
$errors[] = t('Please enter the 6-digit code');
} else {
$result = EmailVerificationService::verifyCode($email, $code);
if ($result['ok'] ?? false) {
unset($_SESSION['email_verification_email']);
Flash::success(t('Email verified successfully! Please login.'), 'login', 'email_verified');
Router::redirect('login');
} else {
$errorCode = $result['error'] ?? 'invalid';
if ($errorCode === 'already_verified') {
Flash::success(t('Email already verified. Please login.'), 'login', 'already_verified');
Router::redirect('login');
} elseif ($errorCode === 'too_many_attempts') {
$errors[] = t('Too many failed attempts. Please request a new code.');
} else {
$errors[] = t('Invalid or expired code');
}
}
}
}
Buffer::set('title', t('Verify email'));

View File

@@ -0,0 +1,55 @@
<?php
/**
* @var array<int, string> $errors
* @var string $email
* @var string $code
*/
use MintyPHP\Buffer;
use MintyPHP\Session;
Buffer::set('title', t('Verify email'));
?>
<article>
<header>
<img src="<?php e(appLogoUrl(128)); ?>" alt="<?php e(appTitle()); ?>" class="brand-logo" style="margin-bottom:1rem;">
<hgroup>
<h1><?php e(t('Verify email')); ?></h1>
<p><?php e(t('Enter the verification code we sent to your email to complete your registration.')); ?></p>
</hgroup>
</header>
<form method="post">
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
</label>
<label for="code">
<span><?php e(t('Verification code')); ?></span>
<input required type="text" name="code" id="code" inputmode="numeric" pattern="\d{6}" maxlength="6"
value="<?php e($code ?? ''); ?>" />
</label>
<?php if (!empty($errors)): ?>
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<button type="submit"><?php e(t('Verify')); ?></button>
<?php Session::getCsrfInput(); ?>
</form>
<hr>
<form method="post" style="text-align: center;">
<input type="hidden" name="email" value="<?php e($email ?? ''); ?>">
<input type="hidden" name="action" value="resend">
<?php Session::getCsrfInput(); ?>
<button type="submit" class="secondary outline"><?php e(t('Send code again')); ?></button>
</form>
<p class="text-align-center" style="margin-top: 1rem;">
<a href="<?php e(lurl('login')); ?>"><?php e(t('Back to Login')); ?></a>
</p>
</article>