update login scurity
This commit is contained in:
@@ -24,6 +24,8 @@ class Filters extends BaseConfig
|
|||||||
'honeypot' => Honeypot::class,
|
'honeypot' => Honeypot::class,
|
||||||
'invalidchars' => InvalidChars::class,
|
'invalidchars' => InvalidChars::class,
|
||||||
'secureheaders' => SecureHeaders::class,
|
'secureheaders' => SecureHeaders::class,
|
||||||
|
'sanitizeinput' => \App\Filters\SanitizeInputFilter::class,
|
||||||
|
'apiratelimit' => \App\Filters\ApiRateLimitFilter::class,
|
||||||
'auth' => \App\Filters\AuthFilter::class, // Define the alias for your auth filter
|
'auth' => \App\Filters\AuthFilter::class, // Define the alias for your auth filter
|
||||||
'apiAuth' => \App\Filters\ApiAuthFilter::class, // JWT-based API authentication
|
'apiAuth' => \App\Filters\ApiAuthFilter::class, // JWT-based API authentication
|
||||||
'cleanupScheduler' => \App\Filters\CleanupScheduler::class,
|
'cleanupScheduler' => \App\Filters\CleanupScheduler::class,
|
||||||
@@ -42,6 +44,8 @@ class Filters extends BaseConfig
|
|||||||
public array $globals = [
|
public array $globals = [
|
||||||
'before' => [
|
'before' => [
|
||||||
'timezone',
|
'timezone',
|
||||||
|
'sanitizeinput',
|
||||||
|
'invalidchars',
|
||||||
'csrf' => ['except' => [
|
'csrf' => ['except' => [
|
||||||
// Webhooks / integrations
|
// Webhooks / integrations
|
||||||
'api/paypal-webhook',
|
'api/paypal-webhook',
|
||||||
@@ -80,6 +84,7 @@ class Filters extends BaseConfig
|
|||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
'after' => [
|
'after' => [
|
||||||
|
'secureheaders',
|
||||||
'toolbar',
|
'toolbar',
|
||||||
'cleanupScheduler' => ['except' => ['cleanup/*']],
|
'cleanupScheduler' => ['except' => ['cleanup/*']],
|
||||||
],
|
],
|
||||||
@@ -112,7 +117,7 @@ class Filters extends BaseConfig
|
|||||||
* @var array<string, array<string, list<string>>>
|
* @var array<string, array<string, list<string>>>
|
||||||
*/
|
*/
|
||||||
public array $filters = [
|
public array $filters = [
|
||||||
|
'apiratelimit' => ['before' => ['api/*', 'index.php/api/*']],
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Controllers;
|
|||||||
use App\Models\LoginActivityModel;
|
use App\Models\LoginActivityModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use App\Models\UserRoleModel;
|
use App\Models\UserRoleModel;
|
||||||
use CodeIgniter\Controller;
|
|
||||||
use CodeIgniter\Events\Events;
|
use CodeIgniter\Events\Events;
|
||||||
use App\Models\IpAttemptModel;
|
use App\Models\IpAttemptModel;
|
||||||
use App\Models\PasswordResetModel;
|
use App\Models\PasswordResetModel;
|
||||||
@@ -19,7 +18,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
|||||||
require_once APPPATH . 'Helpers/jwt_helper.php';
|
require_once APPPATH . 'Helpers/jwt_helper.php';
|
||||||
|
|
||||||
|
|
||||||
class AuthController extends Controller
|
class AuthController extends BaseController
|
||||||
{
|
{
|
||||||
protected $configModel;
|
protected $configModel;
|
||||||
protected $userModel;
|
protected $userModel;
|
||||||
@@ -83,9 +82,23 @@ class AuthController extends Controller
|
|||||||
log_message('info', 'Processing login form submission.');
|
log_message('info', 'Processing login form submission.');
|
||||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
||||||
|
|
||||||
|
$validator = \Config\Services::validation();
|
||||||
|
$requestData = sanitize_request_value($this->request->getPost(['email', 'password']));
|
||||||
|
$validator->setRules([
|
||||||
|
'email' => 'required|valid_email|max_length[254]',
|
||||||
|
'password' => 'required|max_length[255]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $validator->run($requestData)) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('error', 'Please enter a valid email and password.')
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: Get email, password, and IP from the request
|
// Step 1: Get email, password, and IP from the request
|
||||||
$email = $this->request->getPost('email');
|
$email = strtolower((string) $requestData['email']);
|
||||||
$password = $this->request->getPost('password');
|
$password = (string) $requestData['password'];
|
||||||
$ip = $this->request->getIPAddress();
|
$ip = $this->request->getIPAddress();
|
||||||
|
|
||||||
log_message('info', 'Login attempt from IP: ' . $ip . ' for email: ' . $email);
|
log_message('info', 'Login attempt from IP: ' . $ip . ' for email: ' . $email);
|
||||||
@@ -139,26 +152,29 @@ class AuthController extends Controller
|
|||||||
// JSON API: POST /api/login
|
// JSON API: POST /api/login
|
||||||
public function apiLogin()
|
public function apiLogin()
|
||||||
{
|
{
|
||||||
$requestData = $this->request->getJSON(true);
|
$requestData = sanitize_request_value($this->request->getJSON(true) ?: [
|
||||||
if (!$requestData) {
|
|
||||||
// fallback to form vars
|
|
||||||
$requestData = [
|
|
||||||
'email' => $this->request->getPost('email'),
|
'email' => $this->request->getPost('email'),
|
||||||
'password' => $this->request->getPost('password'),
|
'password' => $this->request->getPost('password'),
|
||||||
];
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
$email = $requestData['email'] ?? '';
|
$validation = \Config\Services::validation();
|
||||||
$password = $requestData['password'] ?? '';
|
$validation->setRules([
|
||||||
$ip = $this->request->getIPAddress();
|
'email' => 'required|valid_email|max_length[254]',
|
||||||
|
'password' => 'required|max_length[255]',
|
||||||
|
]);
|
||||||
|
|
||||||
if (!$email || !$password) {
|
if (! $validation->run($requestData)) {
|
||||||
return $this->response->setStatusCode(400)->setJSON([
|
return $this->response->setStatusCode(422)->setJSON([
|
||||||
'status' => false,
|
'status' => false,
|
||||||
'message' => 'Email and password are required.'
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => $validation->getErrors(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$email = strtolower((string) ($requestData['email'] ?? ''));
|
||||||
|
$password = (string) ($requestData['password'] ?? '');
|
||||||
|
$ip = $this->request->getIPAddress();
|
||||||
|
|
||||||
if ($this->isIpBlocked($ip)) {
|
if ($this->isIpBlocked($ip)) {
|
||||||
return $this->response->setStatusCode(429)->setJSON([
|
return $this->response->setStatusCode(429)->setJSON([
|
||||||
'status' => false,
|
'status' => false,
|
||||||
@@ -220,7 +236,15 @@ class AuthController extends Controller
|
|||||||
'exp' => $exp,
|
'exp' => $exp,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secret = env('JWT_SECRET', 'change-me-in-env');
|
try {
|
||||||
|
$secret = require_env('JWT_SECRET');
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'JWT configuration is missing.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$token = jwt_encode($payload, $secret, 'HS256');
|
$token = jwt_encode($payload, $secret, 'HS256');
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
@@ -240,18 +264,14 @@ class AuthController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function apiRegister()
|
public function apiRegister()
|
||||||
{
|
{
|
||||||
$requestData = $this->request->getJSON(true);
|
$requestData = sanitize_request_value($this->request->getJSON(true) ?: $this->request->getPost());
|
||||||
if (!$requestData) {
|
|
||||||
// fallback to form vars
|
|
||||||
$requestData = $this->request->getPost();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
$rules = [
|
$rules = [
|
||||||
'firstname' => 'required|min_length[2]|max_length[30]',
|
'firstname' => "required|regex_match[/^[\\p{L}\\s'\\-]+$/u]|min_length[2]|max_length[30]",
|
||||||
'lastname' => 'required|min_length[2]|max_length[30]',
|
'lastname' => "required|regex_match[/^[\\p{L}\\s'\\-]+$/u]|min_length[2]|max_length[30]",
|
||||||
'email' => 'required|valid_email|is_unique[users.email]',
|
'email' => 'required|valid_email|max_length[254]|is_unique[users.email]',
|
||||||
'password' => 'required|min_length[8]',
|
'password' => 'required|min_length[8]|max_length[255]',
|
||||||
'cellphone' => 'required|min_length[10]|max_length[20]',
|
'cellphone' => 'required|min_length[10]|max_length[20]',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -332,7 +352,7 @@ class AuthController extends Controller
|
|||||||
'exp' => $exp,
|
'exp' => $exp,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secret = env('JWT_SECRET', 'change-me-in-env');
|
$secret = require_env('JWT_SECRET');
|
||||||
$token = jwt_encode($payload, $secret, 'HS256');
|
$token = jwt_encode($payload, $secret, 'HS256');
|
||||||
|
|
||||||
return $this->response->setStatusCode(201)->setJSON([
|
return $this->response->setStatusCode(201)->setJSON([
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ abstract class BaseController extends Controller
|
|||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $helpers = [];
|
protected $helpers = ['security'];
|
||||||
|
|
||||||
/** @var ApiClient */
|
/** @var ApiClient */
|
||||||
protected ApiClient $api;
|
protected ApiClient $api;
|
||||||
@@ -73,6 +73,21 @@ abstract class BaseController extends Controller
|
|||||||
// Assuming the user role is stored in the session
|
// Assuming the user role is stored in the session
|
||||||
return session()->get('role');
|
return session()->get('role');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function validated(array $rules, ?array $data = null): array
|
||||||
|
{
|
||||||
|
$validation = service('validation');
|
||||||
|
$payload = $data ?? ($this->request->getJSON(true) ?: $this->request->getPost());
|
||||||
|
$payload = sanitize_request_value($payload);
|
||||||
|
|
||||||
|
$validation->setRules($rules);
|
||||||
|
|
||||||
|
if (! $validation->run($payload)) {
|
||||||
|
throw new \InvalidArgumentException(json_encode($validation->getErrors(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'Validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -20,15 +20,22 @@ class ProofreadController extends ResourceController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Accept form-urlencoded payload to play nicely with CSRF protection
|
// Accept form-urlencoded payload to play nicely with CSRF protection
|
||||||
$text = (string) ($this->request->getPost('text') ?? '');
|
$validation = service('validation');
|
||||||
if ($text === '' || mb_strlen($text) > 20000) {
|
$payload = sanitize_request_value($this->request->getPost(['text']));
|
||||||
|
$validation->setRules([
|
||||||
|
'text' => 'required|min_length[1]|max_length[20000]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $validation->run($payload)) {
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'error' => 'Invalid text (empty or too long).',
|
'error' => 'Invalid text payload.',
|
||||||
'csrfHash' => csrf_hash(),
|
'csrfHash' => csrf_hash(),
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$text = (string) $payload['text'];
|
||||||
|
|
||||||
$client = \Config\Services::curlrequest(['timeout' => 10]);
|
$client = \Config\Services::curlrequest(['timeout' => 10]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class ContactController extends BaseController
|
|||||||
|
|
||||||
// Define validation rules
|
// Define validation rules
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
'name' => 'required|min_length[3]',
|
'name' => "required|regex_match[/^[\\p{L}\\s'\\-]+$/u]|min_length[3]|max_length[100]",
|
||||||
'email' => 'required|valid_email',
|
'email' => 'required|valid_email|max_length[254]',
|
||||||
'subject' => 'required|min_length[3]',
|
'subject' => 'required|min_length[3]|max_length[150]',
|
||||||
'message' => 'required|min_length[10]'
|
'message' => 'required|min_length[10]|max_length[5000]'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$validation->withRequest($this->request)->run()) {
|
if (!$validation->withRequest($this->request)->run()) {
|
||||||
@@ -37,10 +37,10 @@ class ContactController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process form data
|
// Process form data
|
||||||
$name = $this->request->getPost('name');
|
$name = esc((string) $this->request->getPost('name'));
|
||||||
$email = strtolower($this->request->getPost('email'));
|
$email = esc(strtolower((string) $this->request->getPost('email')));
|
||||||
$subject = $this->request->getPost('subject');
|
$subject = esc((string) $this->request->getPost('subject'));
|
||||||
$message = $this->request->getPost('message');
|
$message = nl2br(esc((string) $this->request->getPost('message')));
|
||||||
|
|
||||||
// Initialize the EmailController
|
// Initialize the EmailController
|
||||||
$emailController = new \App\Controllers\View\EmailController();
|
$emailController = new \App\Controllers\View\EmailController();
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ class EventController extends ResourceController
|
|||||||
|
|
||||||
$chargesBuilder = $this->eventChargesModel
|
$chargesBuilder = $this->eventChargesModel
|
||||||
->select('event_charges.*,
|
->select('event_charges.*,
|
||||||
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
users.firstname AS parent_firstname, users.lastname AS parent_lastname, users.cellphone AS parent_cellphone,
|
||||||
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
||||||
events.event_name, events.description AS event_description, events.amount AS event_amount')
|
events.event_name, events.description AS event_description, events.amount AS event_amount')
|
||||||
->join('users', 'users.id = event_charges.parent_id', 'left')
|
->join('users', 'users.id = event_charges.parent_id', 'left')
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class ApiAuthFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
public function before(RequestInterface $request, $arguments = null)
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
{
|
{
|
||||||
|
helper('security');
|
||||||
|
|
||||||
$authorization = $request->getHeaderLine('Authorization');
|
$authorization = $request->getHeaderLine('Authorization');
|
||||||
if (!$authorization || stripos($authorization, 'Bearer ') !== 0) {
|
if (!$authorization || stripos($authorization, 'Bearer ') !== 0) {
|
||||||
return $this->unauthorized('Missing or invalid Authorization header');
|
return $this->unauthorized('Missing or invalid Authorization header');
|
||||||
@@ -21,7 +23,12 @@ class ApiAuthFilter implements FilterInterface
|
|||||||
return $this->unauthorized('Bearer token is required');
|
return $this->unauthorized('Bearer token is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
$secret = env('JWT_SECRET', 'change-me-in-env');
|
try {
|
||||||
|
$secret = require_env('JWT_SECRET');
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
return $this->unauthorized('JWT configuration is missing');
|
||||||
|
}
|
||||||
|
|
||||||
$payload = jwt_decode($token, $secret);
|
$payload = jwt_decode($token, $secret);
|
||||||
|
|
||||||
if (!$payload || empty($payload['sub'])) {
|
if (!$payload || empty($payload['sub'])) {
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ class ApiDocsAuthFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
public function before(RequestInterface $request, $arguments = null)
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
{
|
{
|
||||||
|
helper('security');
|
||||||
|
|
||||||
$authHeader = $request->getHeaderLine('Authorization');
|
$authHeader = $request->getHeaderLine('Authorization');
|
||||||
|
|
||||||
if ($authHeader && str_starts_with($authHeader, 'Bearer ')) {
|
if ($authHeader && str_starts_with($authHeader, 'Bearer ')) {
|
||||||
$token = trim(substr($authHeader, 7));
|
$token = trim(substr($authHeader, 7));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$key = getenv('JWT_SECRET') ?: 'your_default_secret';
|
$key = require_env('JWT_SECRET');
|
||||||
$decoded = JWT::decode($token, new Key($key, 'HS256'));
|
$decoded = JWT::decode($token, new Key($key, 'HS256'));
|
||||||
|
|
||||||
if (!isset($decoded->roles) || empty($decoded->roles->admin)) {
|
if (!isset($decoded->roles) || empty($decoded->roles->admin)) {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use Config\Services;
|
||||||
|
|
||||||
|
class ApiRateLimitFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
|
{
|
||||||
|
$throttler = service('throttler');
|
||||||
|
$response = Services::response();
|
||||||
|
|
||||||
|
$maxRequests = isset($arguments[0]) ? max(1, (int) $arguments[0]) : 60;
|
||||||
|
$windowSeconds = isset($arguments[1]) ? max(1, (int) $arguments[1]) : MINUTE;
|
||||||
|
|
||||||
|
$userId = session()->get('user_id');
|
||||||
|
$identifier = $userId ? 'user:' . $userId : 'ip:' . $request->getIPAddress();
|
||||||
|
$route = trim($request->getUri()->getPath(), '/');
|
||||||
|
$key = 'api-rate-' . sha1($request->getMethod() . '|' . $route . '|' . $identifier);
|
||||||
|
|
||||||
|
if (! $throttler->check($key, $maxRequests, $windowSeconds)) {
|
||||||
|
return $response
|
||||||
|
->setStatusCode(429)
|
||||||
|
->setHeader('Retry-After', (string) $windowSeconds)
|
||||||
|
->setJSON([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'Too many requests. Please try again later.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
{
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use Config\Services;
|
||||||
|
|
||||||
|
class SanitizeInputFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
|
{
|
||||||
|
helper('security');
|
||||||
|
|
||||||
|
$sanitizedGet = sanitize_request_value($request->getGet() ?? []);
|
||||||
|
$sanitizedPost = sanitize_request_value($request->getPost() ?? []);
|
||||||
|
$request->setGlobal('get', $sanitizedGet);
|
||||||
|
$request->setGlobal('post', $sanitizedPost);
|
||||||
|
$request->setGlobal('request', array_merge(is_array($sanitizedGet) ? $sanitizedGet : [], is_array($sanitizedPost) ? $sanitizedPost : []));
|
||||||
|
$request->setGlobal('cookie', sanitize_request_value($request->getCookie() ?? []));
|
||||||
|
|
||||||
|
$contentType = strtolower($request->getHeaderLine('Content-Type'));
|
||||||
|
$body = $request->getBody();
|
||||||
|
|
||||||
|
if (str_contains($contentType, 'application/json') && trim($body) !== '') {
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) {
|
||||||
|
return Services::response()
|
||||||
|
->setStatusCode(400)
|
||||||
|
->setJSON([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'Malformed JSON payload.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->setBody((string) json_encode(sanitize_request_value($decoded), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
{
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (! function_exists('sanitize_request_value')) {
|
||||||
|
/**
|
||||||
|
* Recursively normalize request values without applying output encoding.
|
||||||
|
*/
|
||||||
|
function sanitize_request_value($value)
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
foreach ($value as $key => $item) {
|
||||||
|
$value[$key] = sanitize_request_value($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value) ?? $value;
|
||||||
|
|
||||||
|
return trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('require_env')) {
|
||||||
|
/**
|
||||||
|
* Fetch an environment variable and fail closed when it is missing.
|
||||||
|
*/
|
||||||
|
function require_env(string $key): string
|
||||||
|
{
|
||||||
|
$value = env($key);
|
||||||
|
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
throw new RuntimeException(sprintf('Missing required environment variable: %s', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,9 @@ class StaffTimeOffLinkService
|
|||||||
public function __construct(?string $secret = null, ?int $ttlSeconds = null)
|
public function __construct(?string $secret = null, ?int $ttlSeconds = null)
|
||||||
{
|
{
|
||||||
helper('jwt');
|
helper('jwt');
|
||||||
|
helper('security');
|
||||||
|
|
||||||
$this->secret = $secret ?: (string)env('JWT_SECRET', 'change-me-in-env');
|
$this->secret = $secret ?: require_env('JWT_SECRET');
|
||||||
$this->ttl = ($ttlSeconds !== null && $ttlSeconds > 0) ? $ttlSeconds : self::DEFAULT_TTL;
|
$this->ttl = ($ttlSeconds !== null && $ttlSeconds > 0) ? $ttlSeconds : self::DEFAULT_TTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -290,11 +290,15 @@
|
|||||||
);
|
);
|
||||||
$externalParentPhone = $charge['external_parent_phone'] ?? '';
|
$externalParentPhone = $charge['external_parent_phone'] ?? '';
|
||||||
$externalParentEmail = $charge['external_parent_email'] ?? '';
|
$externalParentEmail = $charge['external_parent_email'] ?? '';
|
||||||
|
$parentPhone = $charge['parent_cellphone'] ?? '';
|
||||||
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
|
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
|
||||||
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
|
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
|
||||||
|
$isExternalParticipant = $externalName !== '';
|
||||||
|
$infoPhone = $isExternalParticipant ? $externalParentPhone : $parentPhone;
|
||||||
|
$infoEmail = $isExternalParticipant ? $externalParentEmail : '';
|
||||||
$externalInfoValue = trim(implode(' ', array_filter([
|
$externalInfoValue = trim(implode(' ', array_filter([
|
||||||
$externalParentPhone,
|
$infoPhone,
|
||||||
$externalParentEmail,
|
$infoEmail,
|
||||||
$externalNote,
|
$externalNote,
|
||||||
])));
|
])));
|
||||||
?>
|
?>
|
||||||
@@ -306,13 +310,13 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td data-sort-value="<?= esc(strtolower($externalInfoValue)) ?>">
|
<td data-sort-value="<?= esc(strtolower($externalInfoValue)) ?>">
|
||||||
<?php if ($externalParentPhone): ?>
|
<?php if ($infoPhone): ?>
|
||||||
<div class="text-muted small"><?= esc($externalParentPhone) ?></div>
|
<div class="text-muted small"><?= esc($infoPhone) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($externalParentEmail): ?>
|
<?php if ($infoEmail): ?>
|
||||||
<div class="text-muted small"><?= esc($externalParentEmail) ?></div>
|
<div class="text-muted small"><?= esc($infoEmail) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!$externalParentPhone && !$externalParentEmail): ?>
|
<?php if (!$infoPhone && !$infoEmail): ?>
|
||||||
<span class="text-muted small">—</span>
|
<span class="text-muted small">—</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<label for="email" class="form-label"></label>
|
<label for="email" class="form-label"></label>
|
||||||
<input type="email" class="form-control item" id="email" name="email"
|
<input type="email" class="form-control item" id="email" name="email"
|
||||||
placeholder="Enter your email" maxlength="50" value="<?= old('email') ?>" required>
|
placeholder="Enter your email" maxlength="254" autocomplete="username"
|
||||||
|
inputmode="email" value="<?= old('email') ?>" required>
|
||||||
<div id="email-error" class="text-danger small mt-1"></div>
|
<div id="email-error" class="text-danger small mt-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@
|
|||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
maxlength="30"
|
maxlength="255"
|
||||||
|
autocomplete="current-password"
|
||||||
required>
|
required>
|
||||||
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
|
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
|
||||||
onclick="togglePasswordVisibility('password', this)"
|
onclick="togglePasswordVisibility('password', this)"
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
helper('security');
|
||||||
|
|
||||||
// Database configuration
|
// Database configuration
|
||||||
$host = 'localhost';
|
$host = env('database.default.hostname', 'localhost');
|
||||||
$dbname = 'u280815660_school';
|
$dbname = require_env('database.default.database');
|
||||||
$username = 'u280815660_melabidi';
|
$username = require_env('database.default.username');
|
||||||
$password = '>tNxlRzP/W8';
|
$password = require_env('database.default.password');
|
||||||
|
$port = (int) env('database.default.port', 3306);
|
||||||
|
|
||||||
// Create connection
|
// Create connection
|
||||||
$conn = @new mysqli($host, $username, $password, $dbname);
|
$conn = @new mysqli($host, $username, $password, $dbname, $port);
|
||||||
|
|
||||||
// Check connection
|
// Check connection
|
||||||
if ($conn->connect_error) {
|
if ($conn->connect_error) {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\App\Filters;
|
||||||
|
|
||||||
|
use App\Filters\ApiRateLimitFilter;
|
||||||
|
use CodeIgniter\HTTP\IncomingRequest;
|
||||||
|
use CodeIgniter\HTTP\Response;
|
||||||
|
use CodeIgniter\HTTP\URI;
|
||||||
|
use CodeIgniter\HTTP\UserAgent;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Config\App;
|
||||||
|
use Config\Services;
|
||||||
|
|
||||||
|
class ApiRateLimitFilterTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Services::resetSingle('throttler');
|
||||||
|
Services::resetSingle('session');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBlocksRequestWhenThresholdIsExceeded(): void
|
||||||
|
{
|
||||||
|
$request = new IncomingRequest(config(App::class), new URI('https://example.test/api/health'), 'php://input', new UserAgent());
|
||||||
|
$filter = new ApiRateLimitFilter();
|
||||||
|
|
||||||
|
$first = $filter->before($request, [1, 60]);
|
||||||
|
$second = $filter->before($request, [1, 60]);
|
||||||
|
|
||||||
|
$this->assertNull($first);
|
||||||
|
$this->assertInstanceOf(Response::class, $second);
|
||||||
|
$this->assertSame(429, $second->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\App\Filters;
|
||||||
|
|
||||||
|
use App\Filters\SanitizeInputFilter;
|
||||||
|
use CodeIgniter\HTTP\IncomingRequest;
|
||||||
|
use CodeIgniter\HTTP\Response;
|
||||||
|
use CodeIgniter\HTTP\URI;
|
||||||
|
use CodeIgniter\HTTP\UserAgent;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Config\App;
|
||||||
|
use Config\Services;
|
||||||
|
|
||||||
|
class SanitizeInputFilterTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
public function testSanitizesGetPostAndJsonPayloads(): void
|
||||||
|
{
|
||||||
|
$request = new IncomingRequest(config(App::class), new URI('https://example.test/api/login'), 'php://input', new UserAgent());
|
||||||
|
$request->setHeader('Content-Type', 'application/json');
|
||||||
|
$request->setGlobal('get', ['q' => " hello\x00 "]);
|
||||||
|
$request->setGlobal('post', ['email' => " user@example.com \n"]);
|
||||||
|
$request->setGlobal('request', ['email' => " user@example.com \n"]);
|
||||||
|
$request->setGlobal('cookie', ['token' => " abc\t"]);
|
||||||
|
$request->setBody("{\"email\":\" user@example.com \",\"name\":\" Bob\\u0000 \"}");
|
||||||
|
|
||||||
|
$filter = new SanitizeInputFilter();
|
||||||
|
$result = $filter->before($request);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
$this->assertSame('hello', $request->getGet('q'));
|
||||||
|
$this->assertSame('user@example.com', $request->getPost('email'));
|
||||||
|
$this->assertSame('abc', $request->getCookie('token'));
|
||||||
|
$this->assertSame(['email' => 'user@example.com', 'name' => 'Bob'], $request->getJSON(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRejectsMalformedJsonPayload(): void
|
||||||
|
{
|
||||||
|
$request = new IncomingRequest(config(App::class), new URI('https://example.test/api/login'), 'php://input', new UserAgent());
|
||||||
|
$request->setHeader('Content-Type', 'application/json');
|
||||||
|
$request->setBody('{invalid');
|
||||||
|
|
||||||
|
$filter = new SanitizeInputFilter();
|
||||||
|
$result = $filter->before($request);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Response::class, $result);
|
||||||
|
$this->assertSame(400, $result->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user