2026-09-01 18:54:47 +02:00
<? php
declare ( strict_types = 1 );
2026-09-01 20:07:20 +02:00
require_once dirname ( __DIR__ ) . '/app/Domain/User/PasswordPolicy.php' ;
2026-09-01 19:22:48 +02:00
function load_dotenv ( string $path ) : void
{
if ( ! is_file ( $path ) || ! is_readable ( $path )) return ;
foreach ( file ( $path , FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ) as $line ) {
$line = trim ( $line );
if ( $line === '' || str_starts_with ( $line , '#' )) continue ;
$separator = strpos ( $line , '=' );
if ( $separator === false ) continue ;
$name = trim ( substr ( $line , 0 , $separator ));
$value = trim ( substr ( $line , $separator + 1 ));
2026-09-01 19:26:10 +02:00
if ( ! preg_match ( '/^[A-Za-z_][A-Za-z0-9_]*$/' , $name ) || getenv ( $name ) !== false ) continue ;
2026-09-01 19:22:48 +02:00
if ( strlen ( $value ) >= 2 && (( $value [ 0 ] === '"' && $value [ - 1 ] === '"' ) || ( $value [ 0 ] === "'" && $value [ - 1 ] === "'" ))) {
$value = substr ( $value , 1 , - 1 );
}
putenv ( " { $name } = { $value } " );
}
}
load_dotenv ( dirname ( __DIR__ ) . '/.env' );
2026-09-01 18:54:47 +02:00
function env_required ( string $name ) : string
{
$value = getenv ( $name );
if ( $value === false || trim ( $value ) === '' ) {
throw new RuntimeException ( "Missing required environment variable: { $name } " );
}
return $value ;
}
function db () : PDO
{
static $pdo = null ;
2026-09-01 19:22:48 +02:00
if ( $pdo instanceof PDO ) return $pdo ;
2026-09-01 18:54:47 +02:00
$dsn = sprintf ( 'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4' ,
env_required ( 'DB_HOST' ), getenv ( 'DB_PORT' ) ?: '3306' , env_required ( 'DB_DATABASE' ));
$pdo = new PDO ( $dsn , env_required ( 'DB_USERNAME' ), env_required ( 'DB_PASSWORD' ), [
PDO :: ATTR_ERRMODE => PDO :: ERRMODE_EXCEPTION ,
PDO :: ATTR_DEFAULT_FETCH_MODE => PDO :: FETCH_ASSOC ,
PDO :: ATTR_EMULATE_PREPARES => false ,
]);
return $pdo ;
}
2026-09-01 23:16:34 +02:00
function app_setting ( string $key , ? string $default = null ) : ? string
{
static $cache = [];
if ( array_key_exists ( $key , $cache )) return $cache [ $key ];
try { $stmt = db () -> prepare ( 'SELECT setting_value FROM app_settings WHERE setting_key = :key' ); $stmt -> execute ([ 'key' => $key ]); $value = $stmt -> fetchColumn (); }
catch ( Throwable ) { $value = false ; }
return $cache [ $key ] = $value === false || $value === null ? $default : ( string ) $value ;
}
2026-09-01 18:54:47 +02:00
function csrf_token () : string
{
2026-09-01 19:22:48 +02:00
if ( empty ( $_SESSION [ 'csrf' ])) $_SESSION [ 'csrf' ] = bin2hex ( random_bytes ( 32 ));
2026-09-01 18:54:47 +02:00
return $_SESSION [ 'csrf' ];
}
2026-09-01 18:58:18 +02:00
function scalar_input ( mixed $value , string $default = '' ) : string
{
return is_scalar ( $value ) ? ( string ) $value : $default ;
}
2026-09-01 18:54:47 +02:00
function verify_csrf () : void
{
2026-09-01 18:58:18 +02:00
$provided = scalar_input ( $_POST [ '_csrf' ] ?? null );
2026-09-01 18:54:47 +02:00
if ( ! hash_equals (( string )( $_SESSION [ 'csrf' ] ?? '' ), $provided )) {
http_response_code ( 419 );
exit ( 'Invalid CSRF token' );
}
}
function ensure_initial_administrator () : void
{
static $checked = false ;
2026-09-01 19:22:48 +02:00
if ( $checked ) return ;
2026-09-01 18:54:47 +02:00
$checked = true ;
2026-09-01 19:22:48 +02:00
if (( int ) db () -> query ( 'SELECT COUNT(*) FROM users' ) -> fetchColumn () !== 0 ) return ;
2026-09-01 18:54:47 +02:00
$email = strtolower ( trim ( env_required ( 'ADMIN_EMAIL' )));
$password = env_required ( 'ADMIN_PASSWORD' );
2026-09-01 20:07:20 +02:00
$passwordResult = ( new \App\Domain\User\PasswordPolicy ()) -> validateInitial ( $password );
if ( ! $passwordResult [ 'valid' ]) throw new RuntimeException ( 'ADMIN_PASSWORD does not meet the password policy' );
2026-09-01 18:54:47 +02:00
$roleId = ( int ) db () -> query ( "SELECT id FROM roles WHERE name = 'Administrator'" ) -> fetchColumn ();
2026-09-01 19:22:48 +02:00
if ( $roleId < 1 ) throw new RuntimeException ( 'Administrator role is missing from the database' );
2026-09-01 18:54:47 +02:00
$stmt = db () -> prepare ( 'INSERT INTO users (role_id, email, name, password_hash) VALUES (:role, :email, :name, :hash)' );
2026-09-01 19:22:48 +02:00
$stmt -> execute ([ 'role' => $roleId , 'email' => $email , 'name' => 'System Administrator' , 'hash' => password_hash ( $password , PASSWORD_DEFAULT )]);
2026-09-01 18:54:47 +02:00
}
function current_user () : ? array
{
static $user = false ;
2026-09-01 19:22:48 +02:00
if ( $user !== false ) return $user ;
2026-09-01 18:54:47 +02:00
$id = $_SESSION [ 'user_id' ] ?? null ;
2026-09-01 19:22:48 +02:00
if ( ! $id ) return $user = null ;
2026-09-01 18:54:47 +02:00
$stmt = db () -> prepare ( 'SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1' );
$stmt -> execute ([ 'id' => $id ]);
return $user = ( $stmt -> fetch () ?: null );
}
function require_login () : array
{
$user = current_user ();
2026-09-01 19:22:48 +02:00
if ( ! $user ) { header ( 'Location: /?route=login' ); exit ; }
2026-09-01 18:54:47 +02:00
return $user ;
}
function can ( string $permission ) : bool
{
$user = current_user ();
if ( ! $user ) return false ;
static $permissions = null ;
if ( $permissions === null ) {
$stmt = db () -> prepare ( 'SELECT p.name FROM permissions p JOIN role_permissions rp ON rp.permission_id = p.id WHERE rp.role_id = :role' );
$stmt -> execute ([ 'role' => $user [ 'role_id' ]]);
$permissions = array_column ( $stmt -> fetchAll (), 'name' );
}
return in_array ( $permission , $permissions , true );
}
function require_permission ( string $permission ) : void
{
2026-09-01 19:22:48 +02:00
if ( ! can ( $permission )) { http_response_code ( 403 ); exit ( 'Forbidden' ); }
2026-09-01 18:54:47 +02:00
}
2026-09-01 20:07:20 +02:00
function can_access_jobcard ( int $jobcardId ) : bool
{
$user = current_user ();
if ( ! $user ) return false ;
if ( $user [ 'role_name' ] !== 'Technician' ) return can ( 'jobcards.view' );
$stmt = db () -> prepare ( 'SELECT 1 FROM jobcard_assignments WHERE jobcard_id = :jobcard AND user_id = :user LIMIT 1' );
$stmt -> execute ([ 'jobcard' => $jobcardId , 'user' => $user [ 'id' ]]);
return ( bool ) $stmt -> fetchColumn ();
}
2026-09-01 20:43:24 +02:00
function can_access_client ( int $clientId ) : bool
{
$user = current_user ();
if ( ! $user ) return false ;
if ( $user [ 'role_name' ] !== 'Technician' ) return can ( 'clients.view' );
$stmt = db () -> prepare ( 'SELECT 1 FROM jobcard_assignments ja JOIN jobcards j ON j.id = ja.jobcard_id WHERE ja.user_id = :user AND j.client_id = :client LIMIT 1' );
$stmt -> execute ([ 'user' => $user [ 'id' ], 'client' => $clientId ]);
return ( bool ) $stmt -> fetchColumn ();
}
2026-09-01 18:54:47 +02:00
function audit ( string $action , string $entityType , ? int $entityId = null , array $metadata = []) : void
{
$user = current_user ();
$stmt = db () -> prepare ( 'INSERT INTO audit_events (user_id, action, entity_type, entity_id, metadata, ip_address) VALUES (:user_id, :action, :entity_type, :entity_id, :metadata, :ip)' );
2026-09-01 19:22:48 +02:00
$stmt -> execute ([ 'user_id' => $user [ 'id' ] ?? null , 'action' => $action , 'entity_type' => $entityType , 'entity_id' => $entityId , 'metadata' => $metadata ? json_encode ( $metadata , JSON_THROW_ON_ERROR ) : null , 'ip' => $_SERVER [ 'REMOTE_ADDR' ] ?? null ]);
2026-09-01 18:54:47 +02:00
}
function e ( string $value ) : string
{
return htmlspecialchars ( $value , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8' );
}