initial commit of mobile app
This commit is contained in:
451
application/MobileApp/MobileAppController.php
Normal file
451
application/MobileApp/MobileAppController.php
Normal file
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MobileApp Controller
|
||||
*
|
||||
* Main dispatcher for the Mobile PWA application.
|
||||
*
|
||||
* URL Structure:
|
||||
* - /MobileApp → Main app (Vue SPA)
|
||||
* - /MobileApp/auth/{action} → Auth endpoints (login/logout/check)
|
||||
* - /MobileApp/{module}/{submodule} → Module view (handled by Vue SPA)
|
||||
* - /MobileApp/{module}/{submodule}/{action} → API endpoints
|
||||
*
|
||||
* Example:
|
||||
* - /MobileApp → Shows main menu
|
||||
* - /MobileApp/Lager/Inventur → Shows stocktake (handled by Vue)
|
||||
* - /MobileApp/Lager/Inventur/getActiveStocktakes → API call
|
||||
*/
|
||||
class MobileAppController extends mfBaseController {
|
||||
|
||||
protected $user;
|
||||
|
||||
protected function init() {
|
||||
// We handle auth ourselves
|
||||
$this->needlogin = false;
|
||||
|
||||
// Try to load user if session exists
|
||||
$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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user