feat(ops): 배포 알림과 텔레그램 리포트 추가
This commit is contained in:
@@ -3,6 +3,8 @@ using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
@@ -80,6 +82,85 @@ builder.Services.AddAuthentication(opts =>
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
})
|
||||
.AddCookie(PortalAuthDefaults.Scheme, opts =>
|
||||
{
|
||||
opts.Cookie.Name = PortalAuthDefaults.CookieName;
|
||||
opts.Cookie.HttpOnly = true;
|
||||
opts.Cookie.SameSite = SameSiteMode.Lax;
|
||||
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
opts.LoginPath = "/taxbaik/portal/login";
|
||||
opts.AccessDeniedPath = "/taxbaik/portal/login";
|
||||
opts.SlidingExpiration = true;
|
||||
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
})
|
||||
.AddCookie(PortalOAuthDefaults.ExternalScheme, opts =>
|
||||
{
|
||||
opts.Cookie.Name = "TaxBaik.Portal.External";
|
||||
opts.Cookie.HttpOnly = true;
|
||||
opts.Cookie.SameSite = SameSiteMode.Lax;
|
||||
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
})
|
||||
.AddGoogle(PortalOAuthDefaults.GoogleScheme, opts =>
|
||||
{
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = builder.Configuration["Authentication:Google:ClientId"] ?? "";
|
||||
opts.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"] ?? "";
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-google";
|
||||
})
|
||||
.AddOAuth(PortalOAuthDefaults.NaverScheme, opts =>
|
||||
{
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = builder.Configuration["Authentication:Naver:ClientId"] ?? "";
|
||||
opts.ClientSecret = builder.Configuration["Authentication:Naver:ClientSecret"] ?? "";
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-naver";
|
||||
opts.AuthorizationEndpoint = "https://nid.naver.com/oauth2.0/authorize";
|
||||
opts.TokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
||||
opts.UserInformationEndpoint = "https://openapi.naver.com/v1/nid/me";
|
||||
opts.SaveTokens = true;
|
||||
opts.Events = new OAuthEvents
|
||||
{
|
||||
OnCreatingTicket = async context =>
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, opts.UserInformationEndpoint);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var payload = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted));
|
||||
var responseRoot = payload.RootElement.GetProperty("response");
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, responseRoot.GetProperty("id").GetString() ?? ""));
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, responseRoot.GetProperty("name").GetString() ?? ""));
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Email, responseRoot.GetProperty("email").GetString() ?? ""));
|
||||
}
|
||||
};
|
||||
})
|
||||
.AddOAuth(PortalOAuthDefaults.KakaoScheme, opts =>
|
||||
{
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = builder.Configuration["Authentication:Kakao:ClientId"] ?? "";
|
||||
opts.ClientSecret = builder.Configuration["Authentication:Kakao:ClientSecret"] ?? "";
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-kakao";
|
||||
opts.AuthorizationEndpoint = "https://kauth.kakao.com/oauth/authorize";
|
||||
opts.TokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
||||
opts.UserInformationEndpoint = "https://kapi.kakao.com/v2/user/me";
|
||||
opts.SaveTokens = true;
|
||||
opts.Events = new OAuthEvents
|
||||
{
|
||||
OnCreatingTicket = async context =>
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, opts.UserInformationEndpoint);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var payload = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted));
|
||||
var kakaoAccount = payload.RootElement.GetProperty("kakao_account");
|
||||
var profile = kakaoAccount.GetProperty("profile");
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, payload.RootElement.GetProperty("id").GetInt64().ToString()));
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, profile.GetProperty("nickname").GetString() ?? ""));
|
||||
if (kakaoAccount.TryGetProperty("email", out var emailProp))
|
||||
context.Identity?.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Email, emailProp.GetString() ?? ""));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Blazor 인증
|
||||
@@ -178,6 +259,11 @@ builder.Services.AddResponseCompression(opts => {
|
||||
opts.Providers.Add<GzipCompressionProvider>();
|
||||
});
|
||||
builder.Services.AddScoped<IInquiryNotificationService, TelegramInquiryNotificationService>();
|
||||
builder.Services.AddHostedService<TelegramReportBackgroundService>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<PortalAuthService>();
|
||||
|
||||
builder.Services.Configure<PortalAuthOptions>(builder.Configuration.GetSection("Authentication"));
|
||||
|
||||
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
|
||||
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
|
||||
|
||||
@@ -33,8 +33,8 @@ public class TelegramNotificationService : ITelegramNotificationService
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_botToken = config["Telegram:BotToken"] ?? "";
|
||||
_defaultChatId = config["Telegram:ChatId"] ?? "";
|
||||
_inquiryChatId = config["Telegram:InquiryChatId"] ?? "-5434691215";
|
||||
_defaultChatId = config["Telegram:ChatId"] ?? "-5434691215";
|
||||
_inquiryChatId = config["Telegram:InquiryChatId"] ?? _defaultChatId;
|
||||
_systemChatId = config["Telegram:SystemChatId"] ?? "-5585148480";
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class TelegramNotificationService : ITelegramNotificationService
|
||||
public async Task SendErrorAsync(string title, string details, CancellationToken ct = default)
|
||||
{
|
||||
var message = $"<b>❌ {title}</b>\n\n{details}\n\n<i>{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</i>";
|
||||
await SendMessageAsync(message, ct);
|
||||
await SendToChat(_systemChatId, message, ct);
|
||||
}
|
||||
|
||||
public async Task SendInfoAsync(string title, string message, CancellationToken ct = default)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using TaxBaik.Application.Services;
|
||||
|
||||
public class TelegramReportBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<TelegramReportBackgroundService> logger) : BackgroundService
|
||||
{
|
||||
private static readonly TimeZoneInfo KoreaTimeZone = GetKoreaTimeZone();
|
||||
private DateOnly? _lastDailyReportDate;
|
||||
private DateOnly? _lastWeeklyReportWeekStart;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(30));
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, KoreaTimeZone);
|
||||
await TrySendReportsAsync(now, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Telegram report background loop failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TrySendReportsAsync(DateTimeOffset nowKst, CancellationToken ct)
|
||||
{
|
||||
if (nowKst.Hour is 9 or 10)
|
||||
await SendDailyIfNeededAsync(DateOnly.FromDateTime(nowKst.DateTime), ct);
|
||||
|
||||
if (nowKst.DayOfWeek == DayOfWeek.Monday && nowKst.Hour is 9 or 10)
|
||||
await SendWeeklyIfNeededAsync(DateOnly.FromDateTime(nowKst.DateTime).AddDays(-7), ct);
|
||||
}
|
||||
|
||||
private async Task SendDailyIfNeededAsync(DateOnly date, CancellationToken ct)
|
||||
{
|
||||
if (_lastDailyReportDate == date)
|
||||
return;
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var reportService = scope.ServiceProvider.GetRequiredService<TelegramReportService>();
|
||||
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
||||
|
||||
var report = await reportService.BuildDailyReportAsync(date, ct);
|
||||
await telegram.SendSystemNotificationAsync(TelegramReportService.FormatDailyMessage(report), ct);
|
||||
_lastDailyReportDate = date;
|
||||
logger.LogInformation("Daily telegram report sent for {Date}", date);
|
||||
}
|
||||
|
||||
private async Task SendWeeklyIfNeededAsync(DateOnly weekStart, CancellationToken ct)
|
||||
{
|
||||
if (_lastWeeklyReportWeekStart == weekStart)
|
||||
return;
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var reportService = scope.ServiceProvider.GetRequiredService<TelegramReportService>();
|
||||
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
||||
|
||||
var report = await reportService.BuildWeeklyReportAsync(weekStart, ct);
|
||||
await telegram.SendSystemNotificationAsync(TelegramReportService.FormatWeeklyMessage(report), ct);
|
||||
_lastWeeklyReportWeekStart = weekStart;
|
||||
logger.LogInformation("Weekly telegram report sent for {WeekStart}", weekStart);
|
||||
}
|
||||
|
||||
private static TimeZoneInfo GetKoreaTimeZone()
|
||||
{
|
||||
try
|
||||
{
|
||||
return TimeZoneInfo.FindSystemTimeZoneById("Korea Standard Time");
|
||||
}
|
||||
catch (TimeZoneNotFoundException)
|
||||
{
|
||||
return TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user