validate([ 'email' => 'required|email', 'password' => 'required', ]); $config = $this->mobileConfig->all(); $maxAttempts = $config['security_auth']['login_max_attempts'] ?? 5; $throttleKey = 'login_attempt_'.$request->ip().'_'.$request->email; if (RateLimiter::tooManyAttempts($throttleKey, $maxAttempts)) { $seconds = RateLimiter::availableIn($throttleKey); return ApiResponse::error("Too many login attempts. Please try again in {$seconds} seconds.", 429); } $user = User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { RateLimiter::hit($throttleKey, 60); // Lock for 60 seconds if limit reached return ApiResponse::unauthorized('The provided credentials are incorrect.'); } RateLimiter::clear($throttleKey); if (isset($user->is_active) && ! $user->is_active) { return ApiResponse::forbidden('Your account is currently inactive. Please contact support.'); } if (class_exists(Role::class)) { $allowedRoles = ['User', 'Administrator', 'Developer']; if (! $user->hasAnyRole($allowedRoles)) { return ApiResponse::forbidden('Access denied. Your role does not have permission to access the mobile application.'); } } $token = $user->createToken('mobile-app')->plainTextToken; return ApiResponse::success(['user' => $user, 'token' => $token], 'Login successful'); } #[OA\Post( path: '/v1/register', operationId: 'register', tags: ['Auth'], summary: 'Register a new user', requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['name', 'email', 'password'], properties: [ new OA\Property(property: 'name', type: 'string', example: 'John Doe'), new OA\Property(property: 'email', type: 'string', format: 'email'), new OA\Property(property: 'password', type: 'string', minLength: 8), ] ) ), responses: [ new OA\Response(response: 201, description: 'Registration successful'), new OA\Response(response: 422, description: 'Validation error'), ] )] public function register(Request $request) { $config = $this->mobileConfig->all(); // 1. Check if registration is enabled in Global Mobile Settings if (! ($config['features']['enable_registration'] ?? true)) { return ApiResponse::error('Registration is currently disabled by administrator.', 403); } // 2. Check if OTP is required if (($config['features']['require_otp_registration'] ?? false) && ! $request->has('otp_token')) { return ApiResponse::error('OTP verification is required for registration.', 403); } $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => ['required', 'string', PasswordPolicyService::getRules()], ]); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), 'is_active' => true, ]); if (class_exists(Role::class)) { $user->assignRole('User'); } $token = $user->createToken('mobile-app')->plainTextToken; return ApiResponse::created(['user' => $user, 'token' => $token], 'Registration successful'); } #[OA\Post( path: '/v1/logout', operationId: 'logout', tags: ['Auth'], summary: 'Revoke current access token', security: [['sanctum' => []]], responses: [ new OA\Response(response: 200, description: 'Logged out successfully'), new OA\Response(response: 401, description: 'Unauthenticated'), ] )] public function logout(Request $request) { $request->user()->currentAccessToken()->delete(); return ApiResponse::success(null, 'Logged out successfully'); } #[OA\Post( path: '/v1/forgot-password', operationId: 'forgotPassword', tags: ['Auth'], summary: 'Send password reset link', requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['email'], properties: [new OA\Property(property: 'email', type: 'string', format: 'email')] ) ), responses: [ new OA\Response(response: 200, description: 'Reset link sent'), new OA\Response(response: 422, description: 'Email not found or throttled'), ] )] public function forgotPassword(Request $request) { $request->validate([ 'email' => 'required|email', ]); $status = Password::sendResetLink($request->only('email')); if ($status === Password::RESET_LINK_SENT) { return ApiResponse::success(null, __($status)); } return ApiResponse::error(__($status), 422); } #[OA\Post( path: '/v1/profile/update', operationId: 'updateProfile', tags: ['Profile'], summary: 'Update authenticated user profile', security: [['sanctum' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['name', 'email'], properties: [ new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'email', type: 'string', format: 'email'), ] ) ), responses: [ new OA\Response(response: 200, description: 'Profile updated'), new OA\Response(response: 422, description: 'Validation error'), ] )] public function updateProfile(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users,email,'.Auth::id(), ]); $user = Auth::user(); $user->update([ 'name' => $request->name, 'email' => $request->email, ]); return ApiResponse::success(['user' => $user->fresh()], 'Profile updated successfully'); } #[OA\Post( path: '/v1/profile/avatar', operationId: 'updateAvatar', tags: ['Profile'], summary: 'Upload user avatar', security: [['sanctum' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\MediaType( mediaType: 'multipart/form-data', schema: new OA\Schema(properties: [ new OA\Property(property: 'avatar', type: 'string', format: 'binary'), ]) ) ), responses: [ new OA\Response(response: 200, description: 'Avatar updated'), new OA\Response(response: 422, description: 'Validation error'), ] )] public function updateAvatar(Request $request) { $request->validate([ 'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:10240', ]); $user = Auth::user(); if ($request->hasFile('avatar')) { $user->clearMediaCollection('avatar'); $media = $user->addMediaFromRequest('avatar')->toMediaCollection('avatar'); return ApiResponse::success([ 'avatar_url' => $media->getFullUrl(), 'user' => $user->fresh(), ], 'Avatar updated successfully'); } return ApiResponse::error('No file uploaded', 400); } #[OA\Get( path: '/v1/dashboard', operationId: 'getDashboard', tags: ['Dashboard'], summary: 'Get dashboard summary data', security: [['sanctum' => []]], responses: [ new OA\Response(response: 200, description: 'Dashboard data'), new OA\Response(response: 401, description: 'Unauthenticated'), ] )] public function getDashboardData(Request $request) { $user = Auth::user(); $cacheKey = "user_dashboard_v2_{$user->id}"; $data = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($user) { $transactions = Transaction::where('user_id', $user->id)->latest()->take(10)->get(); $totalBalance = Transaction::where('user_id', $user->id) ->selectRaw("SUM(CASE WHEN type = 'income' THEN amount ELSE -amount END) as balance") ->value('balance') ?? 0; return [ 'balance' => number_format($totalBalance, 2), 'transactions' => $transactions, 'stats' => [ 'income' => Transaction::where('user_id', $user->id)->where('type', 'income')->sum('amount'), 'expense' => Transaction::where('user_id', $user->id)->where('type', 'expense')->sum('amount'), ], ]; }); return ApiResponse::success($data); } #[OA\Post( path: '/v1/profile/password', operationId: 'updatePassword', tags: ['Profile'], summary: 'Change authenticated user password', security: [['sanctum' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['current_password', 'password', 'password_confirmation'], properties: [ new OA\Property(property: 'current_password', type: 'string'), new OA\Property(property: 'password', type: 'string', minLength: 8), new OA\Property(property: 'password_confirmation', type: 'string'), ] ) ), responses: [ new OA\Response(response: 200, description: 'Password updated'), new OA\Response(response: 422, description: 'Current password incorrect'), ] )] public function updatePassword(Request $request) { $request->validate([ 'current_password' => 'required', 'password' => 'required|string|min:8|confirmed', ]); $user = Auth::user(); if (! Hash::check($request->current_password, $user->password)) { return ApiResponse::error('The current password you entered is incorrect.', 422); } try { PasswordPolicyService::checkHistory($user, $request->password); } catch (ValidationException $e) { return ApiResponse::error($e->errors()['password'][0] ?? 'Password does not meet policy requirements.', 422); } $passwordHash = Hash::make($request->password); $user->update(['password' => $passwordHash]); PasswordPolicyService::recordPasswordChange($user, $passwordHash); return ApiResponse::success(null, 'Password updated successfully.'); } #[OA\Delete( path: '/v1/profile/delete', operationId: 'deleteAccount', tags: ['Profile'], summary: 'Permanently delete authenticated user account', security: [['sanctum' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['password'], properties: [ new OA\Property(property: 'password', type: 'string', description: 'Current password to confirm deletion'), ] ) ), responses: [ new OA\Response(response: 200, description: 'Account deleted'), new OA\Response(response: 422, description: 'Password incorrect or missing'), ] )] public function deleteAccount(Request $request) { $request->validate([ 'password' => 'required|string', ]); $user = Auth::user(); if (! Hash::check($request->password, $user->password)) { return ApiResponse::error('The password you entered is incorrect.', 422); } $user->tokens()->delete(); $user->delete(); return ApiResponse::success(null, 'Your account has been deleted permanently.'); } #[OA\Get( path: '/v1/user', operationId: 'getUser', tags: ['Auth'], summary: 'Get authenticated user', security: [['sanctum' => []]], responses: [ new OA\Response(response: 200, description: 'User object'), new OA\Response(response: 401, description: 'Unauthenticated'), ] )] public function user(Request $request) { return ApiResponse::success(['user' => $request->user()]); } #[OA\Get( path: '/v1/app-config', operationId: 'getAppConfig', tags: ['Config'], summary: 'Get public app configuration (branding, taglines)', responses: [ new OA\Response(response: 200, description: 'App config'), ] )] public function getAppConfig() { return ApiResponse::success([ 'logo' => asset(get_setting('app_logo', 'assets/img/logo.png')), 'tagline1' => get_setting('app_tagline1', 'Welcome'), 'tagline2' => strip_tags(get_setting('app_tagline2', 'Manage your assets efficiently')), 'footer' => get_setting('footer_text', '© 2026 Your App'), ]); } }