Compare commits

..

2 Commits
main ... 8

Author SHA1 Message Date
dd1364f744 fix: added Token reaccessing 2025-07-27 21:36:26 +03:00
171757705a feat: implemented available resumes on Back-end 2025-07-27 21:18:31 +03:00
9 changed files with 232 additions and 12 deletions

View File

@ -15,11 +15,13 @@ public class User
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[MaxLength(255)] public string? AccessToken { get; set; } = null;
[MaxLength(255)] public string? RefreshToken { get; set; } = null;
[MaxLength(255)] public string? AccessToken { get; set; }
[MaxLength(255)] public string? RefreshToken { get; set; }
public bool Eula { get; set; } = false;
[MaxLength(255)] public string? CvUrl { get; set; } = null;
public long? ExpiresIn { get; set; } = null;
public bool Eula { get; set; }
[MaxLength(255)] public string? CvUrl { get; set; }
}
//TODO: Негоже это маппинги в DAL ложить

View File

@ -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")]

View File

@ -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<Resume> 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!;
}

View File

@ -25,6 +25,9 @@
<ItemGroup>
<Protobuf Include="..\Proto\*" GrpcServices="Server"></Protobuf>
<Protobuf Update="..\Proto\resume.proto">
<Link>Proto\resume.proto</Link>
</Protobuf>
</ItemGroup>
<ItemGroup>

View File

@ -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)]);
}
/// <summary>
/// Auth user on HeadHunter API
/// </summary>
/// <param name="userId"></param>
/// <param name="authorizationCode"></param>
/// <param name="error"></param>
/// <returns></returns>
public async Task<Status> AuthUser(int userId, string authorizationCode, string? error) //TODO: Разбить этот метод
{
logger.LogInformation($"Authorization for user {userId} in process...");
@ -108,6 +116,7 @@ public class HeadHunterService(
user.AccessToken = responseDto.AccessToken;
user.RefreshToken = responseDto.RefreshToken;
user.ExpiresIn = responseDto.ExpiresIn;
await dbContext.SaveChangesAsync();
@ -123,6 +132,50 @@ public class HeadHunterService(
return Status.Success;
}
public async Task<Status> UpdateUserAccessToken(User user)
{
if (user.RefreshToken == null)
return Status.UserAuthRejectedError;
using var client = new HttpClient(); //TODO: Написать wrapper для работы с HH API
var form = new Dictionary<string, string>
{
{ "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<HeadHunterTokenResponseDto>(await res.Content.ReadAsStringAsync());
if (responseDto == null)
{
logger.LogWarning(
$"User {user.Username ?? user.UserId.ToString()} 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)

View File

@ -0,0 +1,68 @@
using System.Net.Http.Headers;
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,
ILogger<ResumeService> logger,
IOptions<HeadHunterConfig> config,
HeadHunterService headHunterService)
{
public async Task<(Status Status, List<Resume>? Resume)> GetAvailableResumes(long userId)
{
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserId == userId);
if (user == null)
{
return new(Status.UserNotFound, null);
}
if (!user.ExpiresIn.HasValue || new DateTime().AddSeconds(user.ExpiresIn.Value) > DateTime.Now)
{
var status = await headHunterService.UpdateUserAccessToken(user);
if (status != HeadHunterService.Status.Success)
{
logger.LogError($"User {userId} has expired access and update of it was unsuccessful: {status}");
return (Status.TokenExpired, 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");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.AccessToken);
using var res = await client.GetAsync("/resumes/mine");
if (!res.IsSuccessStatusCode)
{
logger.LogWarning(
$"User {user.Username ?? user.UserId.ToString()} resume list is unavailable by unsuccessful status code: {res.StatusCode}");
return new(Status.RequestError, null);
}
var responseDto = JsonSerializer.Deserialize<ListOfResumeResponseDto>(await res.Content.ReadAsStringAsync());
if (responseDto == null)
return new(Status.Error, null);
return new(Status.Success, responseDto.Items);
}
public enum Status
{
Success,
UserNotFound,
Error,
RequestError,
TokenExpired
}
}

View File

@ -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<GetResumeResponse> 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<GetAvailableResponse> 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"));
}
}
}

View File

@ -32,6 +32,9 @@
<None Update="appsettings.Example.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
@ -58,12 +61,6 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\JOBot.Infrastructure\JOBot.Infrastructure.csproj" />
</ItemGroup>

30
Proto/resume.proto Normal file
View File

@ -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;
}