using Fitness_core_BE.DTOs.Auth; using Fitness_core_BE.Helpers; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using System.Net.Http; using System.Text; using System.Text.Json; namespace Fitness_core_BE.Services.Auth { public class AuthService(IHttpClientFactory httpClientFactory, ICustomerRepository customerRepository, IUnitOfWork unitOfWork, IConfiguration configuration) : IAuthService { private readonly HttpClient _httpClient = httpClientFactory.CreateClient(); private readonly ICustomerRepository _cusRepo = customerRepository; private readonly IUnitOfWork _UoW = unitOfWork; private readonly string _baseUrl = configuration["AuthService:BaseUrl"] ?? "http://127.0.0.1:5602"; private string AuthAltTargetUrl => _baseUrl + "/api/alt"; private string AuthUserTargetUrl => _baseUrl + "/api/user"; private string AuthRecoveryTargetUrl => _baseUrl + "/api/recovery"; public async Task SendOtp(object payload, HttpContext httpContext) { var targetUrl = AuthAltTargetUrl; var request = new { functionName = "sendOtp", payload, reference = "send-otp" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task LoginUser(object payload, HttpContext httpContext) { // Web clients explicitly opt in to a persistent 30-day cookie. Without // this, closing the browser ends the session even though Auth retains the // server-side refresh session for security/audit purposes. var loginData = DeserializePayload.Deserialize(payload); var rememberMe = loginData?.TryGetValue("rememberMe", out var rememberValue) == true && rememberValue.ValueKind is System.Text.Json.JsonValueKind.True; var targetUrl = AuthUserTargetUrl; var request = new { functionName = "loginUser", payload, reference = "LoginUser" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize( responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (authResponse == null) return new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; if (authResponse.Success && authResponse.Data is JsonElement dataElement) { SetAuthCookiesFromData(httpContext, dataElement, rememberMe); } return authResponse; } catch (Exception ex) { throw new Exception($"Auth service error: {ex.Message}"); } } /// Sets the access_token (and refresh_token if present) cookies from an auth-service data payload. private void SetAuthCookiesFromData(HttpContext httpContext, JsonElement dataElement, bool persistent = true) { string? accessToken = null; if (dataElement.TryGetProperty("accessToken", out var tokenProp) || dataElement.TryGetProperty("AccessToken", out tokenProp)) accessToken = tokenProp.GetString(); if (!string.IsNullOrEmpty(accessToken)) SetAccessTokenCookieInternal(httpContext, accessToken, persistent); string? refreshToken = null; if (dataElement.TryGetProperty("refreshToken", out var refreshProp) || dataElement.TryGetProperty("RefreshToken", out refreshProp)) refreshToken = refreshProp.GetString(); if (!string.IsNullOrEmpty(refreshToken)) SetRefreshTokenCookie(httpContext, refreshToken, persistent); } public void SetAccessTokenCookie(HttpContext httpContext, string token) => SetAccessTokenCookieInternal(httpContext, token, persistent: true); private void SetAccessTokenCookieInternal(HttpContext httpContext, string token, bool persistent) { var requestHost = httpContext.Request.Host.Host; var forwardedProto = httpContext.Request.Headers["X-Forwarded-Proto"].FirstOrDefault(); var isHttpsRequest = httpContext.Request.IsHttps || string.Equals(forwardedProto, "https", StringComparison.OrdinalIgnoreCase); httpContext.Response.Cookies.Append("access_token", token, new CookieOptions { HttpOnly = true, Secure = isHttpsRequest, SameSite = isHttpsRequest ? SameSiteMode.None : SameSiteMode.Lax, Expires = persistent ? DateTimeOffset.UtcNow.AddDays(30) : null, Path = "/", Domain = IsLocalHost(requestHost) ? null : "hexafit.hexdive.com" }); } public void SetRefreshTokenCookie(HttpContext httpContext, string refreshToken, bool persistent = true) { var requestHost = httpContext.Request.Host.Host; var forwardedProto = httpContext.Request.Headers["X-Forwarded-Proto"].FirstOrDefault(); var isHttpsRequest = httpContext.Request.IsHttps || string.Equals(forwardedProto, "https", StringComparison.OrdinalIgnoreCase); httpContext.Response.Cookies.Append("refresh_token", refreshToken, new CookieOptions { HttpOnly = true, Secure = isHttpsRequest, SameSite = isHttpsRequest ? SameSiteMode.None : SameSiteMode.Lax, Expires = persistent ? DateTimeOffset.UtcNow.AddDays(30) : null, Path = "/", Domain = IsLocalHost(requestHost) ? null : "hexafit.hexdive.com" }); } public void ClearAuthCookies(HttpContext httpContext) { var requestHost = httpContext.Request.Host.Host; var domain = IsLocalHost(requestHost) ? null : "hexafit.hexdive.com"; var options = new CookieOptions { Path = "/", Domain = domain }; httpContext.Response.Cookies.Delete("access_token", options); httpContext.Response.Cookies.Delete("refresh_token", options); } public async Task RefreshToken(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; // The refresh token lives in an httpOnly cookie so the browser can trigger a // silent refresh without ever holding it in JS. Fall back to the request payload // (e.g. mobile clients that pass it explicitly). string? refreshToken = null; try { var data = DeserializePayload.Deserialize(payload); if (data.TryGetValue("refreshToken", out var rt) && rt.ValueKind == JsonValueKind.String) refreshToken = rt.GetString(); } catch { /* payload may be empty on cookie-driven refresh */ } if (string.IsNullOrEmpty(refreshToken)) refreshToken = httpContext.Request.Cookies["refresh_token"]; if (string.IsNullOrEmpty(refreshToken)) { ClearAuthCookies(httpContext); return new AuthResponse { StatusCode = 401, Success = false, Message = "Missing refresh token", Data = null }; } var deviceName = httpContext.Request.Headers["User-Agent"].ToString(); var request = new { functionName = "refreshToken", payload = new { refreshToken, deviceName }, reference = "RefreshToken" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; if (!string.IsNullOrWhiteSpace(deviceName)) forwardRequest.Headers.TryAddWithoutValidation("User-Agent", deviceName); var forwardedFor = httpContext.Connection.RemoteIpAddress?.ToString(); if (!string.IsNullOrWhiteSpace(forwardedFor)) forwardRequest.Headers.TryAddWithoutValidation("X-Forwarded-For", forwardedFor); try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (authResponse == null) return new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; if (authResponse.Success && authResponse.Data is JsonElement dataElement) { // Core-Hex-Auth rotates the refresh token; persist both new cookies. SetAuthCookiesFromData(httpContext, dataElement); return authResponse; } // Refresh token invalid/expired — clear cookies and report 401 so the // frontend redirects to login rather than surfacing a 500. ClearAuthCookies(httpContext); return new AuthResponse { StatusCode = 401, Success = false, Message = "Session expired. Please log in again.", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } //customer ta only use? why public async Task VerifyOTPForCus(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); var targetUrl = AuthRecoveryTargetUrl; var request = new { functionName = "verifyOTP", payload, reference = "verify-otp" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (authResponse == null) throw new Exception("Empty response from auth service"); string identifireType = DataExtractor.GetRequiredString(data, "identifireType"); var identifier = DataExtractor.GetRequiredString(data, "identifier"); Guid userId = DataExtractor.GetUserIdFromContext(httpContext); await _UoW.BeginAsync(); try { bool result = await _cusRepo.VerifyCustomer(userId, identifireType, identifier); if (!result) throw new Exception("verfifying faled"); await _UoW.CommitAsync(); } catch(Exception ex) { await _UoW.RollbackAsync(); throw new Exception($"{ex.Message}"); } return authResponse; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task VerifyOtpForLogin(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "VerifyOtpForLogin", payload, reference = "verify-otp-for-login" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task IsAvailable(object payload, HttpContext httpContext) { var targetUrl = AuthAltTargetUrl; var request = new { functionName = "IsAvailable", payload, reference = "is-available" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task RegisterUser(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "registerUser", payload, reference = "register-user" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task GetUserDetails(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "getUserDetails", payload, reference = "user-details" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var authHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrEmpty(authHeader)) { forwardRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", authHeader.Replace("Bearer ", "") ); } try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task UpdateUser(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "UpdateUser", payload, reference = "user-details" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; // Web clients authenticate via the access_token cookie, not an Authorization // header — forward whichever is present so the auth service can read the userId. ApplyAuthHeader(forwardRequest, httpContext); try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public async Task AdminUpdateUser(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "adminUpdateUser", payload, reference = "admin-update-user" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } /// Forwards a recovery-manager function (forgotPassword / resetPassword) — no auth required. private async Task ForwardToRecovery(string functionName, object payload) { var request = new { functionName, payload, reference = functionName }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, AuthRecoveryTargetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } public Task ForgotPassword(object payload, HttpContext httpContext) => ForwardToRecovery("forgotPassword", payload); public Task ResetPassword(object payload, HttpContext httpContext) => ForwardToRecovery("resetPassword", payload); public Task ResetPasswordWithToken(object payload, HttpContext httpContext) => ForwardToRecovery("resetPasswordWithToken", payload); // Confirms the caller's own password (used to unlock sensitive views). Returns { valid }. public async Task VerifyPassword(object payload, HttpContext httpContext) { var request = new { functionName = "VerifyPassword", payload, reference = "verify-password" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, AuthUserTargetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; ApplyAuthHeader(forwardRequest, httpContext); try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 500, Success = false, Message = "Empty response from auth service", Data = null }; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } /// Sets the outgoing Bearer token from the Authorization header, the MobileAuthToken header, or the access_token cookie. private static void ApplyAuthHeader(HttpRequestMessage req, HttpContext ctx) { string? token = null; var authHeader = ctx.Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(authHeader)) token = authHeader.Replace("Bearer ", "").Trim(); if (string.IsNullOrWhiteSpace(token)) { var mobile = ctx.Request.Headers["MobileAuthToken"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(mobile) && mobile.StartsWith("Bearer ")) token = mobile.Substring("Bearer ".Length).Trim(); } if (string.IsNullOrWhiteSpace(token)) token = ctx.Request.Cookies["access_token"]; if (!string.IsNullOrWhiteSpace(token)) req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } // new otp verfy for client public async Task VerifyOTP(object payload, HttpContext httpContext) { var data = DeserializePayload.Deserialize(payload); var targetUrl = AuthRecoveryTargetUrl; var request = new { functionName = "verifyOTP", payload, reference = "verify-otp" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (authResponse == null) throw new Exception("Empty response from auth service"); return authResponse; } catch (Exception ex) { return new AuthResponse { StatusCode = 500, Success = false, Message = $"Error communicating with auth service: {ex.Message}", Data = null }; } } //Logout user by clearing cookie public async Task LogOut(object payload, HttpContext httpContext) { var targetUrl = AuthUserTargetUrl; var request = new { functionName = "LogoutUser", payload, reference = "log-out" }; var json = JsonSerializer.Serialize(request); var forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; // Always clear cookies locally, even if the remote revoke fails — the user // clicked logout and their browser session must end. ClearAuthCookies(httpContext); try { var response = await _httpClient.SendAsync(forwardRequest); var responseContent = await response.Content.ReadAsStringAsync(); var authResponse = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return authResponse ?? new AuthResponse { StatusCode = 200, Success = true, Message = "Logged out", Data = null }; } catch { return new AuthResponse { StatusCode = 200, Success = true, Message = "Logged out", Data = null }; } } private static bool IsLocalHost(string? host) { return string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || string.Equals(host, "127.0.0.1", StringComparison.OrdinalIgnoreCase); } } }