needlogin = false; $me = mfValuecache::singleton()->get("me"); if (!$me) { if (mfLoginController::isLoggedIn()) { $me = new User(); $me->loadMe(); mfValuecache::singleton()->set("me", $me); } } $this->user = $me; } /** * Main dispatcher */ public function indexAction() { $module = $this->request->module ?? null; $submodule = $this->request->submodule ?? null; $endpoint = $this->request->endpoint ?? null; // Auth endpoints: /MobileApp/auth/{action} if (strtolower($module) === 'auth') { return $this->handleAuth($submodule ?? 'check'); } // API call: /MobileApp/{module}/{submodule}/{endpoint} if ($module && $submodule && $endpoint) { return $this->handleApiCall($module, $submodule, $endpoint); } // Everything else: render the main Vue SPA // The Vue app handles internal routing for /MobileApp, /MobileApp/Lager, /MobileApp/Lager/Inventur, etc. return $this->renderApp(); } /** * Render the main Vue SPA */ protected function renderApp() { $this->layout()->setTemplate("MobileApp/App"); $this->layout()->set("JSGlobals", [ 'BASE_PATH' => '/MobileApp', 'USER' => $this->user ? [ 'id' => $this->user->id, 'name' => $this->user->name, 'username' => $this->user->username, ] : null, 'INITIAL_PATH' => $_SERVER['REQUEST_URI'] ?? '/MobileApp', ]); } /** * Handle authentication endpoints */ protected function handleAuth($action) { switch (strtolower($action)) { case 'login': return $this->authLogin(); case 'verify2fa': return $this->authVerify2FA(); case 'resend2fa': return $this->authResend2FA(); case 'logout': return $this->authLogout(); case 'check': return $this->authCheck(); default: self::returnJson(['success' => false, 'error' => 'Unknown auth endpoint'], 404); } } /** * POST /MobileApp/auth/login * * Step 1 of authentication. If 2FA is required, returns requires2FA: true * and the frontend should proceed to verify2fa endpoint. */ protected function authLogin() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { self::returnJson(['success' => false, 'error' => 'Method not allowed'], 405); return; } $postData = json_decode(file_get_contents('php://input'), true) ?? []; $username = $postData['username'] ?? ''; $password = $postData['password'] ?? ''; $rememberMe = $postData['rememberMe'] ?? false; if (!$username || !$password) { self::returnJson(['success' => false, 'message' => 'Benutzername und Passwort erforderlich']); return; } $db = FronkDB::singleton(); $escapedUsername = $db->escape($username); $res = $db->select(MFUSERTABLE, "*", "username='$escapedUsername'"); if (!$db->num_rows($res)) { sleep(1); self::returnJson(['success' => false, 'message' => 'Ungültige Anmeldedaten']); return; } $userRow = $db->fetch_object($res); if ($userRow->active == 0) { self::returnJson(['success' => false, 'message' => 'Benutzer ist deaktiviert']); return; } $hash = $userRow->password; $salt = substr($hash, 0, 16); $passhash = mfLoginController::generatePasswordHash($password, $salt); if ($passhash !== $hash) { sleep(1); self::returnJson(['success' => false, 'message' => 'Ungültige Anmeldedaten']); return; } // Check if 2FA is required if ($userRow->twofactor !== "0") { // Generate and send 2FA code $twoFactor = new UserTwofactor($userRow->id); $twoFactor->sendCode(); // Store pending auth in session for 2FA verification $_SESSION['mobileapp_2fa_pending'] = [ 'user_id' => $userRow->id, 'username' => $userRow->username, 'remember_me' => $rememberMe, 'timestamp' => time() ]; // Determine delivery method for UI feedback $deliveryMethod = $userRow->twofactor == 1 ? 'email' : 'sms'; $maskedTarget = $deliveryMethod === 'email' ? $this->maskEmail($userRow->email) : $this->maskPhone($userRow->mobile); self::returnJson([ 'success' => false, 'requires2FA' => true, 'deliveryMethod' => $deliveryMethod, 'maskedTarget' => $maskedTarget, 'message' => 'Verifizierungscode wurde gesendet' ]); return; } // No 2FA - complete login directly $this->completeLogin($userRow, $rememberMe); } /** * POST /MobileApp/auth/verify2fa * * Step 2 of authentication - verify the 2FA code */ protected function authVerify2FA() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { self::returnJson(['success' => false, 'error' => 'Method not allowed'], 405); return; } $postData = json_decode(file_get_contents('php://input'), true) ?? []; $code = $postData['code'] ?? ''; // Check for pending 2FA session if (!isset($_SESSION['mobileapp_2fa_pending'])) { self::returnJson(['success' => false, 'message' => 'Keine ausstehende Verifizierung']); return; } $pending = $_SESSION['mobileapp_2fa_pending']; // Check if pending session is expired (10 minutes max) if (time() - $pending['timestamp'] > 600) { unset($_SESSION['mobileapp_2fa_pending']); self::returnJson(['success' => false, 'message' => 'Sitzung abgelaufen. Bitte erneut anmelden.', 'expired' => true]); return; } if (!$code || strlen($code) !== 5) { self::returnJson(['success' => false, 'message' => 'Bitte gib den 5-stelligen Code ein']); return; } $db = FronkDB::singleton(); $userId = intval($pending['user_id']); // Get user's 2FA code and timestamp $res = $db->select(MFUSERTABLE, "twofactorcode, twofactortimestamp, username", "id = {$userId}"); if (!$db->num_rows($res)) { unset($_SESSION['mobileapp_2fa_pending']); self::returnJson(['success' => false, 'message' => 'Benutzer nicht gefunden']); return; } $userRow = $db->fetch_object($res); $storedCode = $userRow->twofactorcode; $codeTimestamp = intval($userRow->twofactortimestamp); // Check if code is expired (5 minutes) if (time() - $codeTimestamp > 300) { self::returnJson(['success' => false, 'message' => 'Code abgelaufen. Bitte neuen Code anfordern.', 'codeExpired' => true]); return; } // Verify code if ($code !== $storedCode) { sleep(1); // Rate limiting self::returnJson(['success' => false, 'message' => 'Ungültiger Code']); return; } // Clear the 2FA code $twoFactor = new UserTwofactor($userId); $twoFactor->removeCode(); // Clear pending session unset($_SESSION['mobileapp_2fa_pending']); // Get full user row for login completion $res = $db->select(MFUSERTABLE, "*", "id = {$userId}"); $userRow = $db->fetch_object($res); // Complete login $this->completeLogin($userRow, $pending['remember_me']); } /** * POST /MobileApp/auth/resend2fa * * Resend the 2FA code */ protected function authResend2FA() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { self::returnJson(['success' => false, 'error' => 'Method not allowed'], 405); return; } // Check for pending 2FA session if (!isset($_SESSION['mobileapp_2fa_pending'])) { self::returnJson(['success' => false, 'message' => 'Keine ausstehende Verifizierung']); return; } $pending = $_SESSION['mobileapp_2fa_pending']; // Check if pending session is expired (10 minutes max) if (time() - $pending['timestamp'] > 600) { unset($_SESSION['mobileapp_2fa_pending']); self::returnJson(['success' => false, 'message' => 'Sitzung abgelaufen. Bitte erneut anmelden.', 'expired' => true]); return; } // Resend 2FA code $twoFactor = new UserTwofactor($pending['user_id']); $twoFactor->sendCode(); self::returnJson([ 'success' => true, 'message' => 'Neuer Code wurde gesendet' ]); } /** * Complete the login process after password (and optionally 2FA) verification */ protected function completeLogin($userRow, $rememberMe) { $db = FronkDB::singleton(); $db->update(MFUSERTABLE, [ 'ip' => $_SERVER['REMOTE_ADDR'], 'sessionid' => session_id() ], "id = {$userRow->id}"); $_SESSION[MFAPPNAME . '_username'] = $userRow->username; $_SESSION[MFAPPNAME . '_ip'] = $_SERVER['REMOTE_ADDR']; if ($rememberMe) { UserToken::generateToken($userRow->id); } $user = new User(); $user->loadMe(); self::returnJson([ 'success' => true, 'user' => [ 'id' => $user->id, 'name' => $user->name, 'username' => $user->username, ] ]); } /** * Mask email address for privacy (e.g., j***@example.com) */ protected function maskEmail($email) { if (!$email) return '***'; $parts = explode('@', $email); if (count($parts) !== 2) return '***'; $local = $parts[0]; $domain = $parts[1]; $masked = strlen($local) > 1 ? $local[0] . str_repeat('*', min(5, strlen($local) - 1)) : '*'; return $masked . '@' . $domain; } /** * Mask phone number for privacy (e.g., +43***123) */ protected function maskPhone($phone) { if (!$phone) return '***'; $phone = preg_replace('/\s+/', '', $phone); if (strlen($phone) < 6) return '***'; return substr($phone, 0, 3) . str_repeat('*', strlen($phone) - 6) . substr($phone, -3); } /** * POST /MobileApp/auth/logout */ protected function authLogout() { mfLoginController::staticLogout(); self::returnJson(['success' => true]); } /** * GET /MobileApp/auth/check */ protected function authCheck() { if (mfLoginController::isLoggedIn()) { $user = new User(); $user->loadMe(); if ($user->id) { self::returnJson([ 'authenticated' => true, 'user' => [ 'id' => $user->id, 'name' => $user->name, 'username' => $user->username, ] ]); return; } } UserToken::checkToken(); if (isset($_SESSION[MFAPPNAME . '_username']) && $_SESSION[MFAPPNAME . '_username']) { $user = new User(); $user->loadMe(); if ($user->id) { self::returnJson([ 'authenticated' => true, 'user' => [ 'id' => $user->id, 'name' => $user->name, 'username' => $user->username, ] ]); return; } } self::returnJson(['authenticated' => false]); } /** * Handle API calls to module endpoints * /MobileApp/{module}/{submodule}/{endpoint} */ protected function handleApiCall($module, $submodule, $endpoint) { // Normalize names $moduleName = ucfirst(strtolower($module)); $submoduleName = ucfirst(strtolower($submodule)); // Check authentication for API calls if (!$this->user || !$this->user->id) { self::returnJson(['success' => false, 'error' => 'Not authenticated'], 401); return; } // Build handler path $handlerFile = APPDIR . "MobileApp/Modules/{$moduleName}/{$submoduleName}/{$submoduleName}Handler.php"; if (!file_exists($handlerFile)) { self::returnJson(['success' => false, 'error' => "Module not found: {$moduleName}/{$submoduleName}"], 404); return; } require_once $handlerFile; $handlerClass = "{$submoduleName}Handler"; if (!class_exists($handlerClass)) { self::returnJson(['success' => false, 'error' => "Handler class not found"], 500); return; } $handler = new $handlerClass($this->request, $this->user, $this); // Check permissions if (!$handler->checkPermission()) { self::returnJson(['success' => false, 'error' => 'Permission denied'], 403); return; } // Route to method $method = $endpoint . 'Action'; if (method_exists($handler, $method)) { return $handler->$method(); } if (method_exists($handler, $endpoint)) { return $handler->$endpoint(); } self::returnJson(['success' => false, 'error' => "Endpoint not found: {$endpoint}"], 404); } }