Test of HeadHunter hook #20

Merged
Lisoveliy merged 1 commits from 1 into dev 2025-07-12 18:52:31 +02:00
7 changed files with 184 additions and 7 deletions

View File

@ -0,0 +1,23 @@
using JOBot.Backend.Services;
using Microsoft.AspNetCore.Mvc;
namespace JOBot.Backend.Controllers;
[ApiController]
[Route("auth")]
public class HeadHunterHookController(HeadHunterService hhService)
: ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get(int userId, string? error, string authorizationCode)
{
var res = await hhService.AuthUser(userId, authorizationCode, error);
return res switch
{
HeadHunterService.Status.Success => Ok("Авторизация завершена успешно. Вернитесь в Telegram для продолжения."),
HeadHunterService.Status.UserNotFoundError => NotFound("Пользователь не найден."),
_ => BadRequest("Авторизация завершена с ошибкой. Вернитесь в Telegram для продолжения.") //TODO: Add resource
};
}
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace JOBot.Backend.DTOs.HeadHunterHook;
public record HeadHunterTokenResponseDto(
[property:JsonPropertyName("access_token")]
string AccessToken,
[property:JsonPropertyName("expires_in")]
int ExpiresIn,
[property:JsonPropertyName("refresh_token")]
string RefreshToken,
[property:JsonPropertyName("token_type")]
string TokenType
);

View File

@ -0,0 +1,18 @@
namespace JOBot.Backend.Infrastructure.Config;
public class HeadHunterConfig
{
public const string SectionName = "HeadHunter";
public required HeadHunterLinksConfig Links { get; init; }
public required string ClientId { get; init; }
public required string Secret { get; init; }
public class HeadHunterLinksConfig
{
public required string AuthLink { get; init; }
public required string HookDomain { get; init; }
public required string HookRoute { get; init; }
public required string HeadHunterApiDomain { get; init; }
public required string HeadHunterTokenRoute { get; init; }
}
}

View File

@ -14,6 +14,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@ -0,0 +1,106 @@
using System.Text.Json;
using JOBot.Backend.DAL.Context;
using JOBot.Backend.DTOs.HeadHunterHook;
using JOBot.Backend.Infrastructure.Config;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace JOBot.Backend.Services;
public class HeadHunterService(ILogger<HeadHunterService> logger, IOptions<HeadHunterConfig> config, AppDbContext dbContext)
{
private readonly HeadHunterConfig _config = config.Value;
/// <summary>
/// Generate HeadHunter oauth authorization link
/// </summary>
/// <param name="userId">Telegram UserId</param>
/// <returns>Link for auth</returns>
public string GenerateAuthLink(int userId)
{
var redirectUri = new UriBuilder(_config.Links.HookDomain)
{
Port = -1,
Scheme = "https",
Path = _config.Links.HookRoute,
Query = $"?userId={userId}"
}.ToString();
return string.Format(_config.Links.AuthLink, [_config.ClientId, userId, redirectUri]);
}
public async Task<Status> AuthUser(int userId, string authorizationCode, string? error)
{
logger.LogInformation($"Authorization for user {userId} in process...");
if (!string.IsNullOrEmpty(error))
{
logger.LogWarning($"User {userId} auth completed with error {error}");
return Status.UserAuthRejectedError;
}
using var client = new HttpClient();
var form = new Dictionary<string, string>
{
{ "client-id", _config.ClientId },
{ "client_secret", _config.Secret },
{ "code", authorizationCode },
{ "grant_type", "authorization_code" }
};
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)
});
if (!res.IsSuccessStatusCode)
{
logger.LogWarning($"Response of HttpRequest ${_config.Links.HeadHunterApiDomain} " +
$"${_config.Links.HeadHunterTokenRoute} has unsuccessful status code {res.StatusCode}");
return Status.HeadHunterAuthRejectedError;
}
var responseDto = JsonSerializer.Deserialize<HeadHunterTokenResponseDto>(await res.Content.ReadAsStringAsync());
if (responseDto == null)
{
logger.LogWarning($"User {userId} auth completed with error " +
$"{nameof(Status.HeadHunterResponseDeserializationFailedError)}");
return Status.HeadHunterResponseDeserializationFailedError;
}
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserId == userId);
if (user == null)
{
logger.LogWarning($"User {userId} search completed with error {nameof(Status.UserNotFoundError)}");
return Status.UserNotFoundError;
}
user.AccessToken = responseDto.AccessToken;
user.RefreshToken = responseDto.RefreshToken;
await dbContext.SaveChangesAsync();
logger.LogInformation($"User {userId} auth completed!");
return Status.Success;
}
public enum Status
{
UserAuthRejectedError,
HeadHunterAuthRejectedError,
UserNotFoundError,
HeadHunterResponseDeserializationFailedError,
Success
}
}

View File

@ -1,30 +1,34 @@
using JOBot.Backend.DAL.Context;
using JOBot.Backend.Infrastructure.Config;
using JOBot.Backend.Services;
using JOBot.Backend.Services.gRPC;
using Microsoft.EntityFrameworkCore;
namespace JOBot.Backend;
public class Startup
public class Startup(IConfiguration configuration)
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private IConfiguration Configuration { get; } = configuration;
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
services.AddGrpcReflection();
services.AddControllers();
services.AddLogging();
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("PostgreSQL")));
services.Configure<HeadHunterConfig>(Configuration.GetSection(HeadHunterConfig.SectionName));
services.AddScoped<HeadHunterService>();
}
public void Configure(WebApplication app, IWebHostEnvironment env)
{
app.MapGrpcReflectionService().AllowAnonymous();
app.MapGrpcService<UserService>();
app.MapControllers();
}
}

View File

@ -9,6 +9,17 @@
"ConnectionStrings": {
"PostgreSQL": "Host=postgres;Port=5432;Database=jobot;Username=postgres;Password=LocalDbPass"
},
"HeadHunter": {
"Links": {
"AuthLink": "https://hh.ru/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}",
"HookDomain": "jobot.lisoveliy.su",
"HookRoute": "/auth",
"HeadHunterApiDomain": "api.hh.ru",
"HeadHunterTokenRoute": "/token"
},
"ClientId": "",
"Secret": ""
},
"Kestrel": {
"Endpoints": {
"gRPC": {