Update to use Auth.Mistox.Com
Docker Build and Release Upload / build (push) Successful in 1m25s

This commit is contained in:
2025-08-07 23:22:53 -07:00
parent 8ce7df46c2
commit 10868018fb
35 changed files with 216 additions and 1151 deletions
@@ -1,284 +1,48 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using MistoxWebsite.Server.Services;
using Microsoft.AspNetCore.Mvc;
using MistoxWebsite.Server.Services.DatabaseService;
using MistoxWebsite.Server.Entities;
using System.Text.Json;
using System.Text;
namespace MistoxWebsite.Server.Controllers {
[ApiController]
[Route("api/account/")]
public class AuthenticationController : MistoxControllerBase {
EmailService _emailContext;
public AuthenticationController(DatabaseService db) : base(db) { }
public AuthenticationController(DatabaseService db, EmailService emailContext) : base(db) {
_emailContext = emailContext;
[HttpPost("loginstate")]
public ActionResult<Account> LoginState() {
if (isLoggedIn()) {
return Ok(getLoggedInUser());
}
return NotFound("Not logged in");
}
[Route("login")]
[HttpPost]
public async Task<ActionResult<Account>> Login([FromForm] string UserName, [FromForm] string PasswordHash, [FromForm] bool StayLoggedIn) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (test.EmailVerified == true) {
if (test.FailedPasswordLock) {
if (test.CurrentPasswordAttempts >= test.PasswordAttempts) {
return new Account() { Error = "Too many failed password attempts. Please reset your password" };
}
}
if (BCrypt.Net.BCrypt.Verify(PasswordHash, test.PasswordHash)) {
test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test);
List<Claim> claims = new List<Claim>() {
new Claim("ID", test.ID.ToString())
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(new ClaimsIdentity(claims, "Auth")),
new AuthenticationProperties {
ExpiresUtc = DateTime.UtcNow.AddYears(30), // Add 30 years with sliding on
IsPersistent = StayLoggedIn, // Is set from the StayLoggedIn
}
);
return test;
}
else {
test.CurrentPasswordAttempts += 1;
await _databaseService.SetAccount(test);
return new Account() { Error = "Wrong password" };
}
}
else {
await SendVerify(test.UserName);
return new Account() { Error = "A new verify email has been sent. \n Note only 1 email send every 5 mintes" };
}
[HttpPost("loginticket")]
public async Task<ActionResult> LoginTicket([FromBody] string LoginToken) {
using (HttpClient client = new HttpClient()) {
var payload = new { ticket = LoginToken };
StringContent jsonPayload = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
HttpResponseMessage JWTResponse = await client.PostAsync("https://auth.mistox.com/api/auth/token", jsonPayload);
if (JWTResponse.IsSuccessStatusCode) {
string JWT = await JWTResponse.Content.ReadAsStringAsync();
signIn(JWT);
return Ok();
} else {
string error = await JWTResponse.Content.ReadAsStringAsync();
return NotFound(error);
}
return new Account() { Error = "User doesn't exist" };
} catch (Exception ex) {
return new Account() { Error = ex.Message };
}
}
[Route("register")]
[HttpPost]
public async Task<ActionResult<Account>> Register([FromForm] string Email, [FromForm] string UserName, [FromForm] string PasswordHash) {
try {
if (await _databaseService.GetAccount(UserName.ToLower()) == null) {
if (await _databaseService.GetAccount(Email.ToLower()) == null) {
Account? created = new Account() {
UserName = UserName.ToLower(),
Email = Email.ToLower(),
EmailVerified = false,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(PasswordHash),
};
await _databaseService.SetAccount(created);
created = await _databaseService.GetAccount(Email.ToLower());
if (created != null) {
await SendVerify(created.UserName);
return created;
}
return new Account() { Error = "Unknown Error" };
}
else {
return new Account() { Error = "Email is already in use" };
}
}
else {
return new Account() { Error = "UserName is taken" };
}
} catch (Exception ex) {
Console.WriteLine("Error: " + ex.Message);
return new Account() { Error = ex.Message };
}
}
[Route("changepassword")]
[HttpPost]
public async Task<ActionResult<bool>> ChangePassword([FromForm] string OldPassword, [FromForm] string NewPassword) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(OldPassword, user.PasswordHash)) {
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return true;
}
}
return false;
} catch {
return false;
}
}
[Route("toggleaccountlock")]
[HttpPost]
public async Task<ActionResult<string>> ToggleAccountLock([FromForm] bool AccountLock) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
user.FailedPasswordLock = AccountLock;
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return "Account Lock Status Updated";
}
return "Unknown Error Occurred";
} catch (Exception ex) {
return ex.Message;
}
}
[Route("get")]
[HttpPost]
public async Task<ActionResult<Account?>> Get() {
try {
if (isLoggedIn()) {
return await getLoggedInUser();
}
return Ok();
} catch {
return Ok();
}
}
[Route("logout")]
[HttpPost]
public async Task Logout() {
await HttpContext.SignOutAsync();
}
[Route("sendverifyemail")]
[HttpPost]
public async Task<ActionResult<string>> SendVerify([FromForm] string UserName) {
try {
string key = "v" + UserName;
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return "Cannot sent another verify email until 5 minutes has elapsed ";
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.VerifyEmailEmail;
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@VerifyPassword", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return result;
}
return "Account not found";
} catch (Exception) {
return "The connection couldn't be established to the email server";
}
}
[Route("verifyemail")]
[HttpPost]
public async Task<ActionResult<bool>> VerifyEmail([FromForm] string UserName, [FromForm] string EmailToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == EmailToken) {
test.EmailToken = "";
test.EmailVerified = true;
await _databaseService.SetAccount(test);
return true;
}
}
return false;
} catch {
return false;
}
}
[Route("sendresetpassword")]
[HttpPost]
public async Task<ActionResult<string>> ResetPassword([FromForm] string Email) {
try {
string key = "p" + Email.ToLower();
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return "Cannot sent another reset requests until 5 minutes has elapsed";
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(Email.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.ResetPasswordEmail;
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@ResetPassWord", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return result;
}
return "Account Not Found";
} catch (Exception e) {
Console.WriteLine("EmailService Error: " + e.ToString());
return "The connection couldn't be established to the email server";
}
}
[Route("resetpassword")]
[HttpPost]
public async Task<ActionResult<bool>> ResetPwdVerify([FromForm] string UserName, [FromForm] string NewPassword, [FromForm] string ResetToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null && !string.IsNullOrEmpty(test.EmailToken)) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == ResetToken) {
test.CurrentPasswordAttempts = 0;
test.EmailToken = "";
test.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
await _databaseService.SetAccount(test);
return true;
}
}
return false;
} catch {
return false;
}
}
[Route("delete")]
[HttpPost]
public async Task<ActionResult<bool>> delete([FromForm] string Password) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) {
await _databaseService.DeleteAccount(user.ID);
return true;
}
}
return false;
} catch {
return false;
[HttpGet("logout")]
public ActionResult Logout() {
if (isLoggedIn()) {
signOut();
return Redirect("/");
}
return NotFound("Not logged in");
}
}
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using MistoxWebsite.Server.Entities;
using MistoxWebsite.Server.Services.DatabaseService;
@@ -12,6 +13,19 @@ namespace MistoxWebsite.Server.Controllers {
_databaseService = databaseService;
}
public void signIn(string JWT) {
Response.Cookies.Append("mistox_session", JWT, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
}
public void signOut() {
Response.Cookies.Delete("mistox_session");
}
public bool isLoggedIn() {
if (User.Identity != null && User.Identity.IsAuthenticated) {
return true;
@@ -20,16 +34,19 @@ namespace MistoxWebsite.Server.Controllers {
}
public int getLoggedInUserID() {
return Convert.ToInt32(User.FindFirst("ID")?.Value);
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public async Task<Account> getLoggedInUser() {
public Account getLoggedInUser() {
try {
Account? test = await _databaseService.GetAccount(getLoggedInUserID());
if (test != null) {
return test;
}
return new Account();
Account building = new Account {
ID = Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier)),
UserName = User.FindFirstValue(ClaimTypes.Name)!.ToString(),
Email = User.FindFirstValue(ClaimTypes.Email)!.ToString(),
Role = User.FindFirstValue(ClaimTypes.Role)!.ToString(),
DataServer = User.FindFirstValue(ClaimTypes.UserData)!.ToString()
};
return building;
} catch {
return new Account();
}
@@ -14,7 +14,7 @@ namespace MistoxWebsite.Server.Controllers {
public async Task<ActionResult<bool>> CreateProduct([FromForm] Product obj, [FromForm] IFormFile[] images) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
Account user = getLoggedInUser();
if (user.Role == "Admin") {
List<ProductImage> building = new List<ProductImage>();
foreach (var file in images) {
@@ -70,7 +70,7 @@ namespace MistoxWebsite.Server.Controllers {
public async Task<ActionResult<bool>> DeleteProduct([FromForm] int productID) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
Account user = getLoggedInUser();
if (user.Role == "Admin") {
await _databaseService.DeleteProduct(productID);
return true;
@@ -3,17 +3,11 @@
namespace MistoxWebsite.Server.Entities {
public class Account {
public int ID { get; set; } // PK
public int? ID { get; set; } // PK
public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public bool EmailVerified { get; set; } = false;
public string PasswordHash { get; set; } = "";
public bool FailedPasswordLock { get; set; } = false;
public int PasswordAttempts { get; set; } = 5;
public int CurrentPasswordAttempts { get; set; } = 0;
public string Role { get; set; } = "Generic";
public string EmailToken { get; set; } = "";
public string Error { get; set; } = "";
public string DataServer { get; set; } = "";
}
public class Product {
@@ -11,6 +11,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+95 -15
View File
@@ -1,4 +1,10 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using MistoxWebsite.Server.Controllers.Payment;
using MistoxWebsite.Server.Services;
using MistoxWebsite.Server.Services.DatabaseService;
@@ -78,25 +84,97 @@ if (IPayment._PaymentType == PaymentType.StripeIntent) {
IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey;
}
// Authentication Service
builder.Services.AddAuthentication( options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
} ).AddCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.LoginPath = "/account/login";
options.LogoutPath = "/account/logout";
options.SlidingExpiration = true;
////////////////////////////////
/////// Auth Service ////////
////////////////////////////////
RsaSecurityKey? PublicKey = null;
using (HttpClient client = new HttpClient()) {
while (PublicKey == null) {
HttpResponseMessage PublicKeyResponse = await client.GetAsync("https://auth.mistox.com/api/auth/publickey");
if (PublicKeyResponse.IsSuccessStatusCode) {
string publicKey = await PublicKeyResponse.Content.ReadAsStringAsync();
RSA rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
PublicKey = new RsaSecurityKey(rsa);
} else {
await Task.Delay(2000); // sleep the main thread for 2 seconds before sending another request. Prevent DDOS of my own equiptment
}
}
}
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://auth.mistox.com",
ValidAudience = "mistox-llc-auth-token",
IssuerSigningKey = PublicKey,
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
context.Token = context.Request.Cookies["mistox_session"];
return Task.CompletedTask;
},
OnTokenValidated = context => {
var jwtToken = context.SecurityToken as JwtSecurityToken;
if (jwtToken != null) {
var exp = jwtToken.ValidTo;
var now = DateTime.UtcNow;
if ((exp - now) < TimeSpan.FromDays(3)) {
// Impliment token refresh
}
}
return Task.CompletedTask;
}
};
});
builder.Services.AddCors( o => o.AddDefaultPolicy( builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS
} ) );
////////////////////////////////
/// Rate Limiting Service ////
////////////////////////////////
List<string> allowedOrigins = new List<string>{ "https://mistox.com", "https://www.mistox.com" };
if (builder.Environment.IsDevelopment()) {
allowedOrigins.Add("http://localhost:5000");
}
builder.Services.AddCors(options => {
options.AddDefaultPolicy(policy => {
policy.WithOrigins(allowedOrigins.ToArray())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddRateLimiter(options => {
options.AddPolicy("PerUserPolicy", httpContext => {
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? $"ip:{httpContext.Connection.RemoteIpAddress}";
return RateLimitPartition.GetTokenBucketLimiter(userId, key => new TokenBucketRateLimiterOptions {
TokenLimit = 10, // max 10 requests
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0,
ReplenishmentPeriod = TimeSpan.FromSeconds(15),
TokensPerPeriod = 2,
AutoReplenishment = true
});
});
});
////////////////////////////////
///// ASPNET Core Function /////
////////////////////////////////
// Pages Service
builder.Services.AddControllers();
builder.Services.AddRazorPages();
var app = builder.Build();
@@ -108,6 +186,8 @@ if( !app.Environment.IsDevelopment() ) {
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRateLimiter();
app.UseCors();
app.UseRouting();
@@ -1,160 +0,0 @@
using MistoxWebsite.Server.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace MistoxWebsite.Server.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Account?> GetAccount( string UserNameOrEmail ) {
Account? account = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT *
FROM Account
WHERE UserName = @UorE OR Email = @UorE;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@UorE", UserNameOrEmail);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _id = reader.GetInt32("ID");
string _username = reader.GetString("UserName");
string _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified");
string _passwordhash = reader.GetString("PasswordHash");
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
string _role = reader.GetString( "Role" );
string _emailtoken = reader.GetString( "EmailToken" );
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _curpasswordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
};
}
}
}
return account;
}
public async Task<Account?> GetAccount( int ID ) {
Account? account = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT *
FROM Account
WHERE ID = @ID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", ID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _id = reader.GetInt32("ID");
string _username = reader.GetString("UserName");
string _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified");
string _passwordhash = reader.GetString("PasswordHash");
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
string _role = reader.GetString( "Role" );
string _emailtoken = reader.GetString( "EmailToken" );
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _passwordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
};
}
}
}
return account;
}
public async Task SetAccount( Account Profile ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
INSERT INTO Account
(ID,UserName,Email,EmailVerified,PasswordHash,FailedPasswordLock,PasswordAttempts,CurrentPasswordAttempts,Role,EmailToken)
VALUES
(@ID,@UserName,@Email,@EmailVerified,@PasswordHash,@FailedPasswordLock,@PasswordAttempts,@CurrentPasswordAttempts,@Role,@EmailToken)
ON DUPLICATE KEY UPDATE
UserName = @UserName,
Email = @Email,
EmailVerified = @EmailVerified,
PasswordHash = @PasswordHash,
FailedPasswordLock = @FailedPasswordLock,
PasswordAttempts = @PasswordAttempts,
CurrentPasswordAttempts = @CurrentPasswordAttempts,
Role = @Role,
EmailToken = @EmailToken;
";
MySqlCommand cmd = new MySqlCommand( command , connection);
cmd.Parameters.AddWithValue("@ID", Profile.ID);
cmd.Parameters.AddWithValue("@UserName", Profile.UserName);
cmd.Parameters.AddWithValue("@Email", Profile.Email);
cmd.Parameters.AddWithValue("@EmailVerified", Profile.EmailVerified);
cmd.Parameters.AddWithValue("@PasswordHash", Profile.PasswordHash);
cmd.Parameters.AddWithValue("@FailedPasswordLock", Profile.FailedPasswordLock);
cmd.Parameters.AddWithValue("@PasswordAttempts", Profile.PasswordAttempts);
cmd.Parameters.AddWithValue("@CurrentPasswordAttempts", Profile.CurrentPasswordAttempts);
cmd.Parameters.AddWithValue("@Role", Profile.Role);
cmd.Parameters.AddWithValue("@EmailToken", Profile.EmailToken);
await cmd.ExecuteNonQueryAsync();
}
}
public async Task DeleteAccount( int ID ) {
using( MySqlConnection connection = GetConnection() ) {
MySqlCommand cmd;
connection.Open();
string command = @"
DELETE FROM Account WHERE ID = @ID;
DELETE FROM AccountInventory WHERE AccountID = @ID;
DELETE FROM ProjectMistData WHERE AccountID = @ID;
DELETE FROM Cart WHERE AccountID = @ID;
";
cmd = new MySqlCommand( command, connection );
cmd.Parameters.AddWithValue("@ID", ID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}