using DocumentFormat.OpenXml.Spreadsheet; using DocumentFormat.OpenXml.VariantTypes; using Ease.NetCore.DataAccess; using Ease.NetCore.DataAccess.SQL; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Options; using MySqlX.XDevAPI.Common; using OnlineSalesAutoCrop.CoreAPI.Models.Global; using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems; using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations; using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations; using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems; using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Auth; using System; using System.Data; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Auth; public class RefreshTokenService : IRefreshTokenService { private readonly AppSettings _settings; public RefreshTokenService(IOptions settings) { _settings = settings.Value; } public async Task AddAsync(InsertRefreshTokenRequest refreshToken) { bool returnValue = false; try { using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true); await AddAsync(tc, refreshToken); tc.End(); } catch (Exception e) { throw new InvalidOperationException(e.Message, e); } return returnValue; } private async Task AddAsync( TransactionContext tc, InsertRefreshTokenRequest refreshToken) { bool returnValue = false; try { SqlParameter[] p = [ SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.VarChar, pValue: refreshToken.UserId), SqlHelperExtension.CreateInParam(pName: "@TokenHash", pType: SqlDbType.VarChar, pValue: refreshToken.TokenHash), SqlHelperExtension.CreateInParam(pName: "@IpAddress", pType: SqlDbType.VarChar, pValue: refreshToken.IpAddress), SqlHelperExtension.CreateInParam(pName: "@CreatedAt", pType: SqlDbType.DateTime, pValue: DateTime.Now), SqlHelperExtension.CreateInParam(pName: "@ExpiredAt", pType: SqlDbType.DateTime, pValue: DateTime.Now.AddMinutes(_settings.RefreshTokenDuration)), ]; _ = await tc.ExecuteNonQuerySpAsync(spName: "dbo.InsertRefreshToken", parameterValues: p); returnValue = true; } catch (Exception e) { throw new InvalidOperationException(e.Message, e); } return returnValue; } public async Task GetByTokenHashAsync(string tokenHash) { RefreshTokenResponse response = new(); try { using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode); try { SqlParameter[] p = [ SqlHelperExtension.CreateInParam(pName: "@TokenHash", pType: SqlDbType.VarChar, pValue: tokenHash, size: 10) ]; using (IDataReader dr =await tc.ExecuteReaderSpAsync("dbo.GetRefreshTokenByTokenHash", parameterValues: p)) { if (dr.Read()) { response.UserId = dr.GetString(0); response.TokenHash = dr.GetString(1); response.IpAddress = dr.GetString(2); response.ExpiredAt = dr.GetDateTime(3); response.RevokedAt = dr.IsDBNull(4) ? null: dr.GetDateTime(4); } dr.Close(); } tc.End(); } catch (Exception ie) { tc?.HandleError(); throw DBCustomError.GenerateCustomError(ie); } } catch (Exception e) { throw new InvalidOperationException(e.Message, e); } return response; } public async Task RevokeAllForUserAsync(int userId) { bool returnValue = false; try { using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true); try { SqlParameter[] p = [ SqlHelperExtension.CreateInParam(pName: "@UserId", pType: SqlDbType.Int, pValue: userId) ]; _ = await tc.ExecuteNonQuerySpAsync(spName: "dbo.RevokedAllRefreshToken", parameterValues: p); returnValue = true; tc.End(); } catch (Exception ie) { tc?.HandleError(); throw DBCustomError.GenerateCustomError(ie); } } catch (Exception e) { throw new InvalidOperationException(e.Message, e); } return returnValue; } public async Task RevokeAsync(RevokedRefreshTokenRequest token) { bool returnValue = false; try { using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true); try { SqlParameter[] p = [ SqlHelperExtension.CreateInParam(pName: "@RefreshToken", pType: SqlDbType.NVarChar, pValue: token.RefreshToken) ]; _ = await tc.ExecuteNonQuerySpAsync(spName: "dbo.RevokedAllRefreshToken", parameterValues: p); returnValue = true; tc.End(); } catch (Exception ie) { tc?.HandleError(); throw DBCustomError.GenerateCustomError(ie); } } catch (Exception e) { throw new InvalidOperationException(e.Message, e); } return returnValue; } public async Task GenerateRefreshToken() { throw new NotImplementedException(); } // ----- private helpers ----- private async Task IssueTokensAsync(TransactionContext tc, User user, string deviceInfo, string ipAddress) { var refreshToken = new InsertRefreshTokenRequest { UserId = user.UserId, TokenHash = HashToken(GenerateRowToken()), IpAddress = ipAddress, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddDays(_settings.RefreshTokenDuration) }; await AddAsync(tc,refreshToken); return new LoginResponse { AccessToken = accessToken, RefreshToken = rawRefreshToken, AccessTokenExpiry = DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpiryMinutes) }; } private static string HashToken(string token) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); return Convert.ToBase64String(bytes); } private string GenerateRowToken() { var bytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(bytes); return Convert.ToBase64String(bytes); } }