diff --git a/backend/CookifyAPI/CookifyAPI/Controllers/AuthController.cs b/backend/CookifyAPI/CookifyAPI/Controllers/AuthController.cs index 99fe462..6b65c61 100644 --- a/backend/CookifyAPI/CookifyAPI/Controllers/AuthController.cs +++ b/backend/CookifyAPI/CookifyAPI/Controllers/AuthController.cs @@ -77,4 +77,17 @@ public async Task Confirm([FromBody] ConfirmOtpRequest request) return Ok(response); } + [HttpPost("google")] + public async Task GoogleAuth([FromBody] GoogleAuthRequest request) + { + var response = await authService.GoogleAuthAsync(request); + + if (response == null) + { + return BadRequest(new { message = "Invalid Google token" }); + } + + return Ok(response); + } + } \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/CookifyAPI.csproj b/backend/CookifyAPI/CookifyAPI/CookifyAPI.csproj index 153941d..f77689b 100644 --- a/backend/CookifyAPI/CookifyAPI/CookifyAPI.csproj +++ b/backend/CookifyAPI/CookifyAPI/CookifyAPI.csproj @@ -13,6 +13,7 @@ + diff --git a/backend/CookifyAPI/CookifyAPI/Extensions/AuthSettings.cs b/backend/CookifyAPI/CookifyAPI/Extensions/AuthSettings.cs index 01b206c..2900310 100644 --- a/backend/CookifyAPI/CookifyAPI/Extensions/AuthSettings.cs +++ b/backend/CookifyAPI/CookifyAPI/Extensions/AuthSettings.cs @@ -8,4 +8,6 @@ public record AuthSettings public int AccessTokenExpirationMinutes { get; init; } public int RefreshTokenExpirationDays { get; init; } public bool SkipVerification { get; init; } + + public string GoogleClientId { get; init; } = string.Empty; } \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Models/DTOs/Requests/GoogleAuthRequest.cs b/backend/CookifyAPI/CookifyAPI/Models/DTOs/Requests/GoogleAuthRequest.cs new file mode 100644 index 0000000..1e014a6 --- /dev/null +++ b/backend/CookifyAPI/CookifyAPI/Models/DTOs/Requests/GoogleAuthRequest.cs @@ -0,0 +1,7 @@ +using System.ComponentModel.DataAnnotations; + +namespace CookifyAPI.Models.DTOs.Requests; + +public record GoogleAuthRequest( + [Required] string IdToken +); \ No newline at end of file diff --git a/backend/CookifyAPI/CookifyAPI/Services/Implementations/AuthService.cs b/backend/CookifyAPI/CookifyAPI/Services/Implementations/AuthService.cs index 27e7064..fa632f3 100644 --- a/backend/CookifyAPI/CookifyAPI/Services/Implementations/AuthService.cs +++ b/backend/CookifyAPI/CookifyAPI/Services/Implementations/AuthService.cs @@ -2,6 +2,7 @@ using CookifyAPI.Models.DTOs.Requests; using CookifyAPI.Models.DTOs.Responses; using CookifyAPI.Models.Entities; +using Google.Apis.Auth; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -88,7 +89,55 @@ public async Task SignUpAsync(RegisterRequest request) return null; } - + + public async Task GoogleAuthAsync(GoogleAuthRequest request) + { + GoogleJsonWebSignature.Payload payload; + try + { + // 1. Проверка токена от Google + var settings = new GoogleJsonWebSignature.ValidationSettings + { + Audience = new[] { _settings.GoogleClientId } + }; + payload = await GoogleJsonWebSignature.ValidateAsync(request.IdToken, settings); + } + catch (InvalidJwtException ex) + { + // Токен подделан или просрочен + Console.WriteLine(ex.Message); + return null; + } + + // 2. Ищем пользователя по Email + var user = await userManager.FindByEmailAsync(payload.Email); + + if (user == null) + { + // 3. Регистрация нового пользователя "на лету" + user = new User + { + UserName = payload.Email, // В качестве логина используем email + Email = payload.Email, + EmailConfirmed = true, // Почта от Google уже подтверждена! + AvatarUrl = payload.Picture, // Берем аватарку из Google + CreatedAt = DateTime.UtcNow + }; + + // Создаем пользователя БЕЗ пароля + var result = await userManager.CreateAsync(user); + + if (!result.Succeeded) + { + // Логируем ошибку, если что-то пошло не так (например, БД недоступна) + throw new Exception("Failed to create user via Google Auth"); + } + } + + // 4. Генерируем НАШИ токены и возвращаем их + return await UpdateTokens(user); + } + public async Task VerifyCodeAsync(ConfirmOtpRequest request) { var user = await userManager.FindByEmailAsync(request.Login) diff --git a/backend/CookifyAPI/CookifyAPI/Services/Implementations/RecipeService.cs b/backend/CookifyAPI/CookifyAPI/Services/Implementations/RecipeService.cs index 1487f26..e6ca004 100644 --- a/backend/CookifyAPI/CookifyAPI/Services/Implementations/RecipeService.cs +++ b/backend/CookifyAPI/CookifyAPI/Services/Implementations/RecipeService.cs @@ -84,7 +84,7 @@ public async Task> GetRecipesKeysetAsync(int? l IQueryable query = context.Recipes .AsNoTracking() .AsSplitQuery() - .OrderBy(r => r.Id); + .OrderByDescending(r => r.Id); if (lastId.HasValue) query = query.Where(r => r.Id > lastId.Value); diff --git a/backend/CookifyAPI/CookifyAPI/Services/Interfaces/IAuthService.cs b/backend/CookifyAPI/CookifyAPI/Services/Interfaces/IAuthService.cs index fb0eadc..27aca57 100644 --- a/backend/CookifyAPI/CookifyAPI/Services/Interfaces/IAuthService.cs +++ b/backend/CookifyAPI/CookifyAPI/Services/Interfaces/IAuthService.cs @@ -11,4 +11,5 @@ public interface IAuthService { Task VerifyCodeAsync(ConfirmOtpRequest request); Task SendOtpCodeAsync(string login); Task ResetPasswordAsync(ResetPasswordRequest request); + Task GoogleAuthAsync(GoogleAuthRequest request); } \ No newline at end of file diff --git a/infrastructure/.env.example b/infrastructure/.env.example index 7afe545..21ba7ca 100644 --- a/infrastructure/.env.example +++ b/infrastructure/.env.example @@ -8,6 +8,9 @@ JWT_KEY=GenerateSomeLongRandomStringHereForDevelopment SMTP_PASSWORD=get_app_password_from_google SENDER_EMAIL=your_dev_email@gmail.com +# Google Authorization +GOOGLE_CLIENT_ID=your_id.apps.googleusercontent.com + # Meilisearch MEILI_MASTER_KEY=YourSuperSecretMasterKey123! diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml index e1f99eb..8c9c174 100644 --- a/infrastructure/docker-compose.yml +++ b/infrastructure/docker-compose.yml @@ -59,6 +59,8 @@ services: - EmailSettings__Password=${SMTP_PASSWORD} - EmailSettings__SenderEmail=${SENDER_EMAIL} + + - AuthSettings__GoogleClientId=${GOOGLE_CLIENT_ID} - Cloudinary__CloudName=${CLOUDINARY_CLOUD_NAME} - Cloudinary__ApiKey=${CLOUDINARY_API_KEY}