feat: redesign Android app with animations, charts, profile, and registration

- Register screen with validation (username, email, password, confirm)
- Profile screen with email editing and password change
- User greeting on dashboard showing logged-in username
- 401 auto-logout via SessionManager + AuthInterceptor
- MetricsChart with Canvas line graphs (CPU, RAM, Disk)
- Animated StatCard with counters and icons
- ServerCard with progress bars, pulsating status dot, dynamic elevation
- Shimmer loading placeholders
- Dark mode support with Material3
- Navigation transitions (slide + fade)
- New server rack launcher icon
- Splash screen
- Backend API: register, profile (GET/PUT) endpoints
This commit is contained in:
2026-06-09 16:38:04 -04:00
parent 1741545318
commit b613e43ab3
58 changed files with 4217 additions and 1 deletions

View File

@@ -6,13 +6,13 @@ namespace ServerManager\Controllers;
use ServerManager\Core\App;
use ServerManager\Core\Session;
use ServerManager\Core\Validator;
use ServerManager\Models\Server;
use ServerManager\Models\User;
use ServerManager\Services\MonitoringService;
use ServerManager\Services\SSHService;
use ServerManager\Services\AuditService;
use ServerManager\Models\CommandHistory;
use ServerManager\Core\Validator;
class ApiController
{
@@ -305,4 +305,219 @@ class ApiController
'data' => $results,
]);
}
public function register(): void
{
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$validator = new Validator();
$rules = [
'username' => 'required|min:3|max:50',
'email' => 'required|email',
'password' => 'required|min:8',
'confirm_password' => 'required',
];
if (!$validator->validate($input, $rules)) {
$this->view->json([
'success' => false,
'error' => $validator->getFirstError(),
], 400);
return;
}
if ($input['password'] !== $input['confirm_password']) {
$this->view->json([
'success' => false,
'error' => 'Passwords do not match.',
], 400);
return;
}
$userModel = new User();
$existing = $userModel->findByUsername($input['username']);
if ($existing) {
$this->view->json([
'success' => false,
'error' => 'Username is already taken.',
], 409);
return;
}
$existingEmail = $userModel->findByEmail($input['email']);
if ($existingEmail) {
$this->view->json([
'success' => false,
'error' => 'Email is already registered.',
], 409);
return;
}
$userId = $userModel->create([
'username' => $input['username'],
'email' => $input['email'],
'password' => $input['password'],
'role' => 'admin',
'is_active' => 1,
]);
$user = $userModel->findById($userId);
$auditService = new AuditService();
$auditService->log('user_registered', 'User registered via API', $userId);
$this->view->json([
'success' => true,
'data' => [
'token' => $user['api_token'],
'user' => [
'id' => (int) $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'],
],
],
], 201);
}
public function profile(): void
{
$user = $this->authenticateRequest();
$this->view->json([
'success' => true,
'data' => [
'id' => (int) $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'],
'created_at' => $user['created_at'],
],
]);
}
public function updateProfile(): void
{
$user = $this->authenticateRequest();
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$validator = new Validator();
$rules = [
'email' => 'required|email',
];
if (!$validator->validate($input, $rules)) {
$this->view->json([
'success' => false,
'error' => $validator->getFirstError(),
], 400);
return;
}
$updateData = ['email' => $input['email']];
if (!empty($input['new_password'])) {
if (strlen($input['new_password']) < 8) {
$this->view->json([
'success' => false,
'error' => 'New password must be at least 8 characters.',
], 400);
return;
}
if (empty($input['current_password'])) {
$this->view->json([
'success' => false,
'error' => 'Current password is required to set a new password.',
], 400);
return;
}
if ($input['new_password'] !== ($input['confirm_password'] ?? '')) {
$this->view->json([
'success' => false,
'error' => 'New passwords do not match.',
], 400);
return;
}
$userModel = new User();
$fullUser = $userModel->findById((int) $user['id']);
if (!$fullUser || !(new \ServerManager\Core\Security())->verifyPassword($input['current_password'], $fullUser['password_hash'])) {
$this->view->json([
'success' => false,
'error' => 'Current password is incorrect.',
], 403);
return;
}
$updateData['password'] = $input['new_password'];
}
$userModel = new User();
$userModel->update((int) $user['id'], $updateData);
$auditService = new AuditService();
$auditService->log('profile_updated', 'Profile updated via API', (int) $user['id']);
$this->view->json([
'success' => true,
'message' => 'Profile updated successfully.',
]);
}
public function login(): void
{
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$validator = new Validator();
$rules = [
'username' => 'required',
'password' => 'required|min:6',
];
if (!$validator->validate($input, $rules)) {
$this->view->json([
'success' => false,
'error' => $validator->getFirstError(),
], 400);
return;
}
$userModel = new User();
$user = $userModel->authenticate($input['username'], $input['password']);
if (!$user) {
$auditService = new AuditService();
$auditService->logLogin($input['username'], false, 'Invalid credentials');
$this->view->json([
'success' => false,
'error' => 'Invalid username or password.',
], 401);
return;
}
$token = $user['api_token'];
if (empty($token)) {
$security = new \ServerManager\Core\Security();
$token = $security->generateApiToken();
$userModel->update((int) $user['id'], ['api_token' => $token]);
}
$auditService = new AuditService();
$auditService->logLogin($input['username'], true);
$this->view->json([
'success' => true,
'data' => [
'token' => $token,
'user' => [
'id' => (int) $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $user['role'],
],
],
]);
}
}