diff --git a/JOBot.Backend/DTOs/HeadHunterHook/HeadHunterTokenResponseDto.cs b/JOBot.Backend/DTOs/HeadHunterApi/HeadHunterTokenResponseDto.cs similarity index 89% rename from JOBot.Backend/DTOs/HeadHunterHook/HeadHunterTokenResponseDto.cs rename to JOBot.Backend/DTOs/HeadHunterApi/HeadHunterTokenResponseDto.cs index a970955..3b10e1f 100644 --- a/JOBot.Backend/DTOs/HeadHunterHook/HeadHunterTokenResponseDto.cs +++ b/JOBot.Backend/DTOs/HeadHunterApi/HeadHunterTokenResponseDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace JOBot.Backend.DTOs.HeadHunterHook; +namespace JOBot.Backend.DTOs.HeadHunterApi; public record HeadHunterTokenResponseDto( [property: JsonPropertyName("access_token")] diff --git a/JOBot.Backend/DTOs/HeadHunterApi/ListOfResumeResponseDto.cs b/JOBot.Backend/DTOs/HeadHunterApi/ListOfResumeResponseDto.cs new file mode 100644 index 0000000..f606a8f --- /dev/null +++ b/JOBot.Backend/DTOs/HeadHunterApi/ListOfResumeResponseDto.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace JOBot.Backend.DTOs.HeadHunterApi; + +public class ListOfResumeResponseDto +{ + [JsonPropertyName("found")] + public bool Found { get; set; } + [JsonPropertyName("items")] + public List Items { get; set; } = []; +} + +public class Resume +{ + [JsonPropertyName("can_publish_or_update")] + public bool CanPublishOrUpdate { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } = null!; + [JsonPropertyName("url")] + public string Url { get; set; } = null!; +} \ No newline at end of file diff --git a/JOBot.Backend/JOBot.Backend.csproj b/JOBot.Backend/JOBot.Backend.csproj index f814b31..34ffb1e 100644 --- a/JOBot.Backend/JOBot.Backend.csproj +++ b/JOBot.Backend/JOBot.Backend.csproj @@ -25,6 +25,9 @@ + + Proto\resume.proto + diff --git a/JOBot.Backend/Services/HeadHunterService.cs b/JOBot.Backend/Services/HeadHunterService.cs index c155837..8df11f0 100644 --- a/JOBot.Backend/Services/HeadHunterService.cs +++ b/JOBot.Backend/Services/HeadHunterService.cs @@ -1,7 +1,8 @@ using System.Text; using System.Text.Json; using JOBot.Backend.DAL.Context; -using JOBot.Backend.DTOs.HeadHunterHook; +using JOBot.Backend.DAL.Models; +using JOBot.Backend.DTOs.HeadHunterApi; using JOBot.Backend.Infrastructure.Config; using JOBot.Infrastructure.Config; using Microsoft.EntityFrameworkCore; @@ -38,6 +39,13 @@ public class HeadHunterService( return string.Format(_config.Links.AuthLink, [_config.ClientId, GetRedirectUrl(userId)]); } + /// + /// Auth user on HeadHunter API + /// + /// + /// + /// + /// public async Task AuthUser(int userId, string authorizationCode, string? error) //TODO: Разбить этот метод { logger.LogInformation($"Authorization for user {userId} in process..."); @@ -123,6 +131,49 @@ public class HeadHunterService( return Status.Success; } + public async Task UpdateUserAccessToken(User user) + { + if(user.RefreshToken == null) + return Status.UserAuthRejectedError; + + using var client = new HttpClient(); //TODO: Написать wrapper для работы с HH API + var form = new Dictionary + { + { "refresh_token", user.RefreshToken }, + { "grant_type", "refresh_token" } + }; + client.BaseAddress = new UriBuilder(_config.Links.HeadHunterApiDomain) + { + Port = -1, + Scheme = "https" + }.Uri; + client.DefaultRequestHeaders.UserAgent.ParseAdd("Jobot BackEnd Service"); + + using var res = await client.SendAsync( + new HttpRequestMessage( + HttpMethod.Post, + _config.Links.HeadHunterTokenRoute) + { + Content = new FormUrlEncodedContent(form) + }); + + var responseDto = + JsonSerializer.Deserialize(await res.Content.ReadAsStringAsync()); + + if (responseDto == null) + { + logger.LogWarning($"User {user.UserId} access token accept completed with error " + + $"{nameof(Status.HeadHunterResponseDeserializationFailedError)}"); + return Status.HeadHunterResponseDeserializationFailedError; + } + + user.RefreshToken = responseDto.RefreshToken; + user.AccessToken = responseDto.AccessToken; + await dbContext.SaveChangesAsync(); + + return Status.Success; + } + private string GetRedirectUrl(long userId) { return new UriBuilder(_config.Links.HookDomain) diff --git a/JOBot.Backend/Services/ResumeService.cs b/JOBot.Backend/Services/ResumeService.cs new file mode 100644 index 0000000..4d970f8 --- /dev/null +++ b/JOBot.Backend/Services/ResumeService.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using JOBot.Backend.DAL.Context; +using JOBot.Backend.DTOs.HeadHunterApi; +using JOBot.Backend.Infrastructure.Config; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace JOBot.Backend.Services; + +public class ResumeService( + AppDbContext dbContext, + IOptions config) +{ + public async Task<(Status Status, List? Resume)> GetAvailableResumes(long userId) + { + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserId == userId); + if (user == null) + { + return new(Status.UserNotFound, null); + } + + using var client = new HttpClient(); //TODO: Написать wrapper для работы с HH API + client.BaseAddress = new UriBuilder(config.Value.Links.HeadHunterApiDomain) + { + Port = -1, + Scheme = "https" + }.Uri; + client.DefaultRequestHeaders.UserAgent.ParseAdd("Jobot BackEnd Service"); + + using var res = await client.GetAsync("/resumes/mine"); + + var responseDto = JsonSerializer.Deserialize(await res.Content.ReadAsStringAsync()); + if (responseDto == null) + return new(Status.Error, null); + + return new(Status.Success, responseDto.Items); + } + + public enum Status + { + Success, + UserNotFound, + Error + } +} \ No newline at end of file diff --git a/JOBot.Backend/Services/gRPC/ResumeService.cs b/JOBot.Backend/Services/gRPC/ResumeService.cs new file mode 100644 index 0000000..ffee7df --- /dev/null +++ b/JOBot.Backend/Services/gRPC/ResumeService.cs @@ -0,0 +1,46 @@ +using Grpc.Core; +using JOBot.Backend.DAL.Context; +using JOBot.Proto; +using Microsoft.EntityFrameworkCore; + +namespace JOBot.Backend.Services.gRPC; + +public class ResumeService(AppDbContext dbContext, Backend.Services.ResumeService resumeService) + : Proto.Resume.ResumeBase +{ + public override async Task GetResume(GetResumeRequest request, ServerCallContext context) + { + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserId == request.UserId); + if (user == null) + throw new RpcException(new Status(StatusCode.NotFound, "User not found")); + + return new GetResumeResponse { ResumeUrl = user.CvUrl }; + } + + public override async Task GetAvailable(GetAvailableRequest request, + ServerCallContext context) + { + var response = await resumeService.GetAvailableResumes(request.UserId); + + switch (response.Status) + { + case Services.ResumeService.Status.UserNotFound: + throw new RpcException(new Status(StatusCode.NotFound, "User not found")); + case Services.ResumeService.Status.Error: + throw new RpcException(new Status(StatusCode.Internal, "Unknown error")); + case Services.ResumeService.Status.Success: + { + var resp = new GetAvailableResponse(); + resp.Resumes.AddRange(response.Resume!.ConvertAll(x => new ResumeDesc + { + Name = x.Title, + ResumeUrl = x.Url + })); + + return resp; + } + default: + throw new RpcException(new Status(StatusCode.Unimplemented, "Unimplemented statement")); + } + } +} \ No newline at end of file diff --git a/JOBot.TClient/JOBot.TClient.csproj b/JOBot.TClient/JOBot.TClient.csproj index 9bce854..286fd53 100644 --- a/JOBot.TClient/JOBot.TClient.csproj +++ b/JOBot.TClient/JOBot.TClient.csproj @@ -32,6 +32,9 @@ Always + + Always + @@ -58,12 +61,6 @@ - - - PreserveNewest - - - diff --git a/Proto/resume.proto b/Proto/resume.proto new file mode 100644 index 0000000..bff0a30 --- /dev/null +++ b/Proto/resume.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +option csharp_namespace = "JOBot.Proto"; + +import "google/protobuf/wrappers.proto"; + +service Resume{ + rpc GetResume(GetResumeRequest) returns (GetResumeResponse); + rpc GetAvailable(GetAvailableRequest) returns (GetAvailableResponse); +} + +message GetResumeRequest{ + int64 user_id = 1; +} + +message GetResumeResponse{ + google.protobuf.StringValue resumeUrl = 1; +} + +message GetAvailableRequest{ + int64 user_id = 1; +} + +message GetAvailableResponse{ + repeated ResumeDesc resumes = 2; +} + +message ResumeDesc{ + string resumeUrl = 1; + string name = 2; +} \ No newline at end of file