< Summary

Information
Class: OzonOrdersWeb.Areas.Identity.Pages.Account.LoginModel
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Identity/Pages/Account/Login.cshtml.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 156
Coverable lines: 156
Total lines: 345
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 54
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Input()100%210%
get_ExternalLogins()100%210%
get_ReturnUrl()100%210%
get_ErrorMessage()100%210%
get_Username()100%210%
get_Password()100%210%
get_RememberMe()100%210%
OnGetAsync()0%110100%
OnPostAsync()0%702260%
FindUserByLoginOrEmailAsync()0%2040%
SendFailedLoginNotificationAsync()0%4260%
LogAuthenticationWarning(...)0%620%
SendAccountLockedNotificationAsync()0%4260%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Identity/Pages/Account/Login.cshtml.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3#nullable disable
 4
 5using System;
 6using System.Collections.Generic;
 7using System.ComponentModel.DataAnnotations;
 8using System.Linq;
 9using System.Threading.Tasks;
 10using Microsoft.AspNetCore.Authorization;
 11using Microsoft.AspNetCore.Authentication;
 12using Microsoft.AspNetCore.Http;
 13using Microsoft.AspNetCore.Identity;
 14using Microsoft.AspNetCore.Identity.UI.Services;
 15using Microsoft.AspNetCore.Mvc;
 16using Microsoft.AspNetCore.Mvc.RazorPages;
 17using Microsoft.AspNetCore.RateLimiting;
 18using Microsoft.Extensions.Logging;
 19using OzonOrdersWeb.Extensions;
 20using OzonRepositories.Context.Identity;
 21using SlqStudio.Application.Services.EmailService;
 22
 23namespace OzonOrdersWeb.Areas.Identity.Pages.Account
 24{
 25    [EnableRateLimiting("auth")]
 26    public class LoginModel : PageModel
 27    {
 28        private readonly SignInManager<CustomIdentityUser> _signInManager;
 29        private readonly UserManager<CustomIdentityUser> _userManager;
 30        private readonly ILogger<LoginModel> _logger;
 31        private readonly IEmailService _emailService;
 32
 033        public LoginModel(
 034            SignInManager<CustomIdentityUser> signInManager,
 035            UserManager<CustomIdentityUser> userManager,
 036            ILogger<LoginModel> logger,
 037            IEmailService emailService = null)
 38        {
 039            _signInManager = signInManager;
 040            _userManager = userManager;
 041            _logger = logger;
 042            _emailService = emailService;
 043        }
 44
 45        /// <summary>
 46        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 47        ///     directly from your code. This API may change or be removed in future releases.
 48        /// </summary>
 49        [BindProperty]
 050        public InputModel Input { get; set; }
 51
 52        /// <summary>
 53        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 54        ///     directly from your code. This API may change or be removed in future releases.
 55        /// </summary>
 056        public IList<AuthenticationScheme> ExternalLogins { get; set; }
 57
 58        /// <summary>
 59        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 60        ///     directly from your code. This API may change or be removed in future releases.
 61        /// </summary>
 062        public string ReturnUrl { get; set; }
 63
 64        /// <summary>
 65        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 66        ///     directly from your code. This API may change or be removed in future releases.
 67        /// </summary>
 68        [TempData]
 069        public string ErrorMessage { get; set; }
 70
 71        /// <summary>
 72        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 73        ///     directly from your code. This API may change or be removed in future releases.
 74        /// </summary>
 75        public class InputModel
 76        {
 77            /// <summary>
 78            ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 79            ///     directly from your code. This API may change or be removed in future releases.
 80            /// </summary>
 81            [Required(ErrorMessage = "Логин или email обязателен для заполнения")]
 82            [Display(Name = "Логин или email")]
 083            public string Username { get; set; }
 84
 85            /// <summary>
 86            ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 87            ///     directly from your code. This API may change or be removed in future releases.
 88            /// </summary>
 89            [Required(ErrorMessage = "Пароль обязателен для заполнения")]
 90            [DataType(DataType.Password)]
 91            [Display(Name = "Пароль")]
 092            public string Password { get; set; }
 93
 94            /// <summary>
 95            ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
 96            ///     directly from your code. This API may change or be removed in future releases.
 97            /// </summary>
 98            [Display(Name = "Запомнить меня")]
 099            public bool RememberMe { get; set; }
 100        }
 101
 102        public async Task OnGetAsync(string returnUrl = null)
 103        {
 0104            if (!string.IsNullOrEmpty(ErrorMessage))
 105            {
 0106                ModelState.AddModelError(string.Empty, ErrorMessage);
 107            }
 108
 109            // Проверяем наличие сообщения об ошибке в query string (от Rate Limiting)
 0110            var errorParam = Request.Query["error"].ToString();
 0111            if (!string.IsNullOrEmpty(errorParam))
 112            {
 0113                ModelState.AddModelError(string.Empty, errorParam);
 114            }
 115
 116            // Проверяем, был ли пользователь разлогинен из-за блокировки
 0117            var lockedParam = Request.Query["locked"].ToString();
 0118            if (!string.IsNullOrEmpty(lockedParam) && lockedParam == "true")
 119            {
 0120                ModelState.AddModelError(string.Empty, "Ваш аккаунт был заблокирован. Обратитесь к администратору.");
 121            }
 122
 0123            returnUrl ??= "/Studio2/OrdersGrid";
 124
 125            // Clear the existing external cookie to ensure a clean login process
 0126            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
 127
 0128            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
 129
 0130            ReturnUrl = returnUrl;
 0131        }
 132
 133        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
 134        {
 0135            returnUrl = Url.IsLocalUrl(returnUrl)
 0136                ? LocalRedirectUrlEncoder.Encode(returnUrl)
 0137                : "/Studio2/OrdersGrid";
 138
 0139            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
 140
 0141            if (ModelState.IsValid)
 142            {
 0143                var user = await FindUserByLoginOrEmailAsync(Input.Username);
 0144                if (user == null)
 145                {
 0146                    LogAuthenticationWarning("LoginUserNotFound", Input.Username);
 0147                    ModelState.AddModelError(string.Empty, "Пользователь с таким логином/email не найден.");
 0148                    return Page();
 149                }
 150
 0151                if (!user.EmailConfirmed)
 152                {
 0153                    LogAuthenticationWarning("LoginEmailNotConfirmed", Input.Username, user.Id);
 0154                    ModelState.AddModelError(string.Empty, "Вход запрещен: email не подтвержден.");
 0155                    return Page();
 156                }
 157
 0158                bool isAdmin = false;
 0159                var roles = await _userManager.GetRolesAsync(user);
 0160                isAdmin = roles.Contains("Admin");
 161
 162                // Если пользователь - администратор и аккаунт заблокирован, разблокируем его
 0163                if (isAdmin && await _userManager.IsLockedOutAsync(user))
 164                {
 0165                    await _userManager.SetLockoutEndDateAsync(user, null);
 0166                    await _userManager.ResetAccessFailedCountAsync(user);
 0167                    _logger.LogInformation("Аккаунт администратора {Username} автоматически разблокирован при попытке вх
 168                }
 169
 0170                if (isAdmin)
 171                {
 0172                    _logger.LogInformation("Попытка входа администратора {Username} - блокировка аккаунта отключена.", I
 173                }
 174
 175                // Для администраторов отключаем блокировку аккаунта
 0176                bool lockoutOnFailure = !isAdmin;
 177
 0178                var result = await _signInManager.CheckPasswordSignInAsync(user, Input.Password, lockoutOnFailure);
 0179                if (result.Succeeded)
 180                {
 0181                    if (!user.IsAdminApproved)
 182                    {
 0183                        LogAuthenticationWarning("LoginNotApproved", Input.Username, user.Id);
 0184                        ModelState.AddModelError(string.Empty, "Вход запрещен: доступ еще не одобрен администратором.");
 0185                        return Page();
 186                    }
 187
 0188                    await _signInManager.SignInAsync(user, new AuthenticationProperties
 0189                    {
 0190                        IsPersistent = Input.RememberMe,
 0191                        RedirectUri = returnUrl
 0192                    });
 0193                    _logger.LogInformation("Пользователь {Username} успешно вошел в систему.", Input.Username);
 194
 195                    // Сохраняем время начала сессии в cookie
 0196                    var sessionStartTime = DateTime.Now.ToString("O");
 0197                    Response.Cookies.Append("SessionStartTime", sessionStartTime, new CookieOptions
 0198                    {
 0199                        HttpOnly = true,
 0200                        Secure = Request.IsHttps,
 0201                        SameSite = SameSiteMode.Lax,
 0202                        Expires = Input.RememberMe ? DateTimeOffset.Now.AddDays(30) : DateTimeOffset.Now.AddHours(24)
 0203                    });
 204
 0205                    return LocalRedirect(returnUrl);
 206                }
 0207                if (result.RequiresTwoFactor)
 208                {
 0209                    return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }
 210                }
 0211                if (result.IsLockedOut)
 212                {
 0213                    _logger.LogWarning("Аккаунт пользователя {Username} заблокирован из-за превышения лимита попыток вхо
 214
 215                    // Отправляем уведомление на email о блокировке аккаунта
 0216                    await SendAccountLockedNotificationAsync(Input.Username);
 217
 0218                    ModelState.AddModelError(string.Empty, "Аккаунт временно заблокирован из-за превышения лимита попыто
 0219                    return Page();
 220                }
 221                else
 222                {
 223                    // Отправляем уведомление на email о неудачной попытке входа
 0224                    LogAuthenticationWarning("LoginInvalidPassword", Input.Username, user.Id);
 0225                    await SendFailedLoginNotificationAsync(Input.Username);
 226
 0227                    ModelState.AddModelError(string.Empty, "Неверный пароль.");
 0228                    return Page();
 229                }
 230            }
 231
 232            // If we got this far, something failed, redisplay form
 0233            LogAuthenticationWarning("LoginFormValidationFailed", Input?.Username);
 0234            return Page();
 0235        }
 236
 237        private async Task<CustomIdentityUser> FindUserByLoginOrEmailAsync(string loginOrEmail)
 238        {
 0239            if (string.IsNullOrWhiteSpace(loginOrEmail))
 240            {
 0241                return null;
 242            }
 243
 0244            var trimmed = loginOrEmail.Trim();
 0245            var byUserName = await _userManager.FindByNameAsync(trimmed);
 0246            if (byUserName != null)
 247            {
 0248                return byUserName;
 249            }
 250
 0251            return await _userManager.FindByEmailAsync(trimmed);
 0252        }
 253
 254        /// <summary>
 255        /// Отправляет уведомление на email о неудачной попытке входа
 256        /// </summary>
 257        private async Task SendFailedLoginNotificationAsync(string username)
 258        {
 0259            if (_emailService == null)
 260            {
 0261                return; // Email сервис не настроен
 262            }
 263
 264            try
 265            {
 0266                var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Неизвестен";
 0267                var timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss");
 0268                var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
 269
 0270                var subject = "⚠️ Неудачная попытка входа в систему";
 0271                var body = $@"
 0272<h2>Обнаружена неудачная попытка входа</h2>
 0273<p><strong>Время:</strong> {timestamp}</p>
 0274<p><strong>Логин:</strong> {System.Net.WebUtility.HtmlEncode(username)}</p>
 0275<p><strong>IP адрес:</strong> {System.Net.WebUtility.HtmlEncode(clientIp)}</p>
 0276<p><strong>User-Agent:</strong> {System.Net.WebUtility.HtmlEncode(userAgent)}</p>
 0277<hr>
 0278<p style=""color: #666; font-size: 12px;"">Это автоматическое уведомление системы безопасности.</p>
 0279";
 280
 0281                await _emailService.SendEmailAsync(subject, body);
 0282                _logger.LogInformation("Отправлено уведомление на email о неудачной попытке входа для пользователя {User
 0283            }
 0284            catch (Exception ex)
 285            {
 0286                _logger.LogError(ex, "Ошибка при отправке уведомления на email о неудачной попытке входа");
 0287            }
 0288        }
 289
 290        private void LogAuthenticationWarning(string eventKind, string username, string userId = null)
 291        {
 0292            using (_logger.BeginScope(new Dictionary<string, object>
 0293                   {
 0294                       ["EventKind"] = eventKind,
 0295                       ["EventCategory"] = "auth",
 0296                       ["RequestPath"] = HttpContext.Request.Path.Value,
 0297                       ["TraceId"] = HttpContext.TraceIdentifier,
 0298                       ["UserId"] = userId,
 0299                       ["RemoteIp"] = HttpContext.Connection.RemoteIpAddress?.ToString()
 0300                   }))
 301            {
 0302                _logger.LogWarning("Неуспешная попытка входа для {Username}: {EventKind}", username, eventKind);
 0303            }
 0304        }
 305
 306        /// <summary>
 307        /// Отправляет уведомление на email о блокировке аккаунта
 308        /// </summary>
 309        private async Task SendAccountLockedNotificationAsync(string username)
 310        {
 0311            if (_emailService == null)
 312            {
 0313                return; // Email сервис не настроен
 314            }
 315
 316            try
 317            {
 0318                var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Неизвестен";
 0319                var timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss");
 0320                var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
 321
 0322                var subject = "🔒 Аккаунт заблокирован из-за превышения лимита попыток входа";
 0323                var body = $@"
 0324<h2>Аккаунт заблокирован</h2>
 0325<p><strong>Время блокировки:</strong> {timestamp}</p>
 0326<p><strong>Логин:</strong> {System.Net.WebUtility.HtmlEncode(username)}</p>
 0327<p><strong>IP адрес:</strong> {System.Net.WebUtility.HtmlEncode(clientIp)}</p>
 0328<p><strong>User-Agent:</strong> {System.Net.WebUtility.HtmlEncode(userAgent)}</p>
 0329<p><strong>Причина:</strong> Превышен лимит неудачных попыток входа (5 попыток за 15 минут)</p>
 0330<p><strong>Длительность блокировки:</strong> 15 минут</p>
 0331<hr>
 0332<p style=""color: #dc3545; font-weight: bold;"">⚠️ ВНИМАНИЕ: Возможна попытка несанкционированного доступа!</p>
 0333<p style=""color: #666; font-size: 12px;"">Это автоматическое уведомление системы безопасности.</p>
 0334";
 335
 0336                await _emailService.SendEmailAsync(subject, body);
 0337                _logger.LogInformation("Отправлено уведомление на email о блокировке аккаунта для пользователя {Username
 0338            }
 0339            catch (Exception ex)
 340            {
 0341                _logger.LogError(ex, "Ошибка при отправке уведомления на email о блокировке аккаунта");
 0342            }
 0343        }
 344    }
 345}