ow
This commit is contained in:
1 parent
7eb017d8ee
commit
873f65dbbe
48 files changed
+1389
-1234
No files matched your search
@@ -26,6 +26,7 @@ namespace CloudBuilder.Core.DatabaseAccessor
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly IHttpContextAccessor httpContextAccessor;
|
||||
private FileLog fileLog;
|
||||
|
||||
public MasterDbContext(DbContextOptions<MasterDbContext> options,
|
||||
IEnumerable<IMasterRegistEntityType<ModelBuilder>> entityManagers,
|
||||
@@ -38,6 +39,7 @@ namespace CloudBuilder.Core.DatabaseAccessor
|
||||
_serviceProvider = serviceProvider;
|
||||
this.configuration = configuration;
|
||||
this.httpContextAccessor = httpContextAccessor;
|
||||
fileLog = new FileLog(configuration);
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
@@ -132,7 +134,7 @@ namespace CloudBuilder.Core.DatabaseAccessor
|
||||
|
||||
// 拼接 WHERE 条件:Id=1 AND TenantId=2
|
||||
var whereClauses = pkProps
|
||||
.Select(p => $"{p.Metadata.GetColumnName()?? p.Metadata.Name} = '{p.CurrentValue?.ToString()?.Replace("'", "''")}'");
|
||||
.Select(p => $"{p.Metadata.GetColumnName() ?? p.Metadata.Name} = '{p.CurrentValue?.ToString()?.Replace("'", "''")}'");
|
||||
string whereSql = string.Join(" AND ", whereClauses);
|
||||
|
||||
string sqlLog = "";
|
||||
@@ -169,42 +171,10 @@ namespace CloudBuilder.Core.DatabaseAccessor
|
||||
sqlLog = $"[{uid}][{now.ToString("yyyy-MM-dd HH:mm:ss")}]DELETE FROM {entityName} WHERE {whereSql}";
|
||||
}
|
||||
|
||||
WriteToFile(sqlLog);
|
||||
fileLog.WriteToFile(sqlLog);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToFile(string log)
|
||||
{
|
||||
try
|
||||
{
|
||||
long MaxFileSizeBytes = (long)(configuration.GetValue<int>("Logging:MaxFileSizeMB") * 1024 * 1024);
|
||||
|
||||
string today = DateTime.Now.ToString("yyyyMMdd");
|
||||
string basePath = configuration["Logging:LogFileDirectory"];
|
||||
Directory.CreateDirectory(basePath);
|
||||
|
||||
string baseFileName = $"log-{today}";
|
||||
string ext = ".log";
|
||||
string finalPath = Path.Combine(basePath, $"{baseFileName}{ext}");
|
||||
int index = 1;
|
||||
|
||||
// 循环找没超过大小的文件
|
||||
while (File.Exists(finalPath) && new FileInfo(finalPath).Length >= MaxFileSizeBytes)
|
||||
{
|
||||
finalPath = Path.Combine(basePath, $"{baseFileName}-{index}{ext}");
|
||||
index++;
|
||||
}
|
||||
|
||||
// 写入(线程安全)
|
||||
lock (this)
|
||||
{
|
||||
File.AppendAllText(finalPath, log + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
foreach (IMasterRegistEntityType<ModelBuilder> entityManager in _entityManagers)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 文件日志写入封装类,自动按日期分文件、超大小自动切割
|
||||
/// </summary>
|
||||
public class FileLog
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly object _lockObj = new object(); // 独立锁,不依赖外部this
|
||||
|
||||
public FileLog(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入日志内容到文件
|
||||
/// </summary>
|
||||
/// <param name="log">日志文本</param>
|
||||
public void WriteToFile(string log)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取配置,转换最大文件字节数
|
||||
int maxMb = _configuration.GetValue<int>("Logging:MaxFileSizeMB");
|
||||
long maxFileSizeBytes = (long)maxMb * 1024 * 1024;
|
||||
|
||||
string today = DateTime.Now.ToString("yyyyMMdd");
|
||||
string basePath = _configuration["Logging:LogFileDirectory"];
|
||||
Directory.CreateDirectory(basePath);
|
||||
|
||||
string baseFileName = $"log-{today}";
|
||||
string ext = ".log";
|
||||
string finalPath = Path.Combine(basePath, $"{baseFileName}{ext}");
|
||||
int index = 1;
|
||||
|
||||
// 文件超限则递增后缀切割
|
||||
while (File.Exists(finalPath) && new FileInfo(finalPath).Length >= maxFileSizeBytes)
|
||||
{
|
||||
finalPath = Path.Combine(basePath, $"{baseFileName}-{index}{ext}");
|
||||
index++;
|
||||
}
|
||||
|
||||
// 线程安全写入
|
||||
lock (_lockObj)
|
||||
{
|
||||
File.AppendAllText(finalPath, log + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 原逻辑吞掉所有异常,如需日志可自行增加内部异常输出
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,6 +10,7 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
@@ -50,6 +52,30 @@ return Task.Run(() => serviceRequest.PostAsJsonAsync<JwtInfo,JwtInfo>("/Auth/R
|
||||
{
|
||||
Task.Run(() => serviceRequest.PostAsync("/Auth/Logout")).GetAwaiter().GetResult(); }
|
||||
|
||||
public bool ConfirmPassword(string password)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<bool>("/Auth/ConfirmPassword?password=" + password.ToString())).GetAwaiter().GetResult(); }
|
||||
|
||||
public string BeginBiometricRegistration(string deviceName)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<string>("/Auth/BeginBiometricRegistration?deviceName=" + deviceName.ToString())).GetAwaiter().GetResult(); }
|
||||
|
||||
public BiometricRegistrationResultDto FinishBiometricRegistration(FinishBiometricRegistrationData dto)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<FinishBiometricRegistrationData,BiometricRegistrationResultDto>("/Auth/FinishBiometricRegistration" , dto)).GetAwaiter().GetResult(); }
|
||||
|
||||
public BiometricLoginChallengeDto BeginBiometricLogin(string username)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<BiometricLoginChallengeDto>("/Auth/BeginBiometricLogin?username=" + username.ToString())).GetAwaiter().GetResult(); }
|
||||
|
||||
public LoginProfile FinishBiometricLogin(FinishBiometricLoginData dto)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<FinishBiometricLoginData,LoginProfile>("/Auth/FinishBiometricLogin" , dto)).GetAwaiter().GetResult(); }
|
||||
|
||||
public void RemoveBiometric()
|
||||
{
|
||||
Task.Run(() => serviceRequest.PostAsync("/Auth/RemoveBiometric")).GetAwaiter().GetResult(); }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,6 +10,7 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Core.Request;
|
||||
using CloudBuilder.Core.Service;
|
||||
@@ -9,10 +10,11 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CloudBuilder.Request.Security
|
||||
{
|
||||
|
||||
@@ -13,16 +13,22 @@ using CloudBuilder.Security.Publisher;
|
||||
using CloudBuilder.Security.Service.Authorization;
|
||||
using CloudBuilder.Security.Service.Publisher;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Runtime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using RSA = System.RSA;
|
||||
|
||||
namespace CloudBuilder.Security.Service
|
||||
{
|
||||
@@ -32,13 +38,15 @@ namespace CloudBuilder.Security.Service
|
||||
private readonly IHttpContextAccessor httpContextAccessor;
|
||||
private readonly ILocaleMessageManager localeMessageManager;
|
||||
private readonly IContext context;
|
||||
private readonly IWebAuthnService webAuthnService;
|
||||
private IRepository<SecurityUserEntity> repository;
|
||||
public AuthService(IApplicationService service, IHttpContextAccessor httpContextAccessor, ILocaleMessageManager localeMessageManager, IContext context)
|
||||
public AuthService(IApplicationService service, IHttpContextAccessor httpContextAccessor, ILocaleMessageManager localeMessageManager, IContext context, IWebAuthnService webAuthnService)
|
||||
{
|
||||
this.service = service;
|
||||
this.httpContextAccessor = httpContextAccessor;
|
||||
this.localeMessageManager = localeMessageManager;
|
||||
this.context = context;
|
||||
this.webAuthnService = webAuthnService;
|
||||
repository = service.GetRepository<IRepository<SecurityUserEntity>>();
|
||||
}
|
||||
|
||||
@@ -60,7 +68,7 @@ namespace CloudBuilder.Security.Service
|
||||
var descKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_DescKey];
|
||||
|
||||
var privateKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_RsaPrivateKey];
|
||||
var rsa = new RSA(privateKey, true);
|
||||
var rsa = new System.RSA(privateKey, true);
|
||||
|
||||
var psd = rsa.DecodeOrNull(data.RsaUserPassword);
|
||||
if (string.IsNullOrEmpty(psd)) throw new ValidatedException(certification);
|
||||
@@ -96,6 +104,19 @@ namespace CloudBuilder.Security.Service
|
||||
throw new ValidatedException(certification);
|
||||
}
|
||||
|
||||
securityUserEntity.ErrorLoginCount = 0;
|
||||
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.NO;
|
||||
securityUserEntity.AccountsLockedDatetime = null;
|
||||
//更新最后登录日期
|
||||
securityUserEntity.LastLoginDatetime = DateTime.Now;
|
||||
|
||||
repository.UpdateNow(securityUserEntity);
|
||||
|
||||
return PackLoginProfile(securityUserEntity, data.Culture);
|
||||
}
|
||||
|
||||
private LoginProfile PackLoginProfile(SecurityUserEntity securityUserEntity, string culture)
|
||||
{
|
||||
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
|
||||
|
||||
string clainmName = Guid.NewGuid().ToString();
|
||||
@@ -103,7 +124,7 @@ namespace CloudBuilder.Security.Service
|
||||
claims.Add(ClaimPolicy.CLAINM_NAME, clainmName);
|
||||
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, securityUserEntity.UserMobile!);
|
||||
claims.Add(ClaimPolicy.CLAINM_USERID, securityUserEntity.UserName);
|
||||
claims.Add(ClaimPolicy.CLAINM_LOCALE, data.Culture);
|
||||
claims.Add(ClaimPolicy.CLAINM_LOCALE, culture);
|
||||
|
||||
var accessToken = jwtGenerate.Generate(claims);
|
||||
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
|
||||
@@ -112,18 +133,10 @@ namespace CloudBuilder.Security.Service
|
||||
|
||||
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey(ClaimPolicy.HEADER_LOCALE))
|
||||
httpContextAccessor.HttpContext.Request.Headers.Remove(ClaimPolicy.HEADER_LOCALE);
|
||||
httpContextAccessor.HttpContext.Request.Headers.Add(ClaimPolicy.HEADER_LOCALE, data.Culture);
|
||||
httpContextAccessor.HttpContext.Request.Headers.Add(ClaimPolicy.HEADER_LOCALE, culture);
|
||||
|
||||
service.UserId = securityUserEntity.UserName;
|
||||
|
||||
securityUserEntity.ErrorLoginCount = 0;
|
||||
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.NO;
|
||||
securityUserEntity.AccountsLockedDatetime = null;
|
||||
//更新最后登录日期
|
||||
securityUserEntity.LastLoginDatetime = DateTime.Now;
|
||||
|
||||
repository.UpdateNow(securityUserEntity);
|
||||
|
||||
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
|
||||
|
||||
SecurityUserWebLoginEntity webLogin = new SecurityUserWebLoginEntity
|
||||
@@ -445,7 +458,7 @@ namespace CloudBuilder.Security.Service
|
||||
claims.Add(ClaimPolicy.CLAINM_NAME, clainmName);
|
||||
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, userMobile);
|
||||
claims.Add(ClaimPolicy.CLAINM_USERID, username);
|
||||
claims.Add(ClaimPolicy.CLAINM_LOCALE, locale??CulturePolicy.ZH_CN);
|
||||
claims.Add(ClaimPolicy.CLAINM_LOCALE, locale ?? CulturePolicy.ZH_CN);
|
||||
|
||||
var accessToken = jwtGenerate.Generate(claims);
|
||||
|
||||
@@ -484,5 +497,495 @@ namespace CloudBuilder.Security.Service
|
||||
repositoryLog.UpdateNow(log);
|
||||
}
|
||||
|
||||
|
||||
// ===== Biometric / WebAuthn login (fingerprint, face, device PIN) =====
|
||||
// Two-phase registration + two-phase assertion. The browser drives
|
||||
// navigator.credentials.create() / navigator.credentials.get(); we
|
||||
// generate challenges server-side, verify the response, and persist
|
||||
// the credential on the user. Login re-uses LoginProfileFactory so
|
||||
// the JWT + SecurityUserWebLogin flow is identical to the password
|
||||
// login in AuthProcess.
|
||||
|
||||
// Verifies the password of the currently-authenticated user.
|
||||
// Used as a step-up authentication gate before sensitive
|
||||
// operations like enabling biometric login on a new device.
|
||||
//
|
||||
// The username is taken from `loginCredential` (the JWT/cookie
|
||||
// identity), not from the request body — the request only carries
|
||||
// the password. This means:
|
||||
// 1. A valid JWT is required to even hit this method.
|
||||
// 2. The user can only verify their OWN password, not someone
|
||||
// else's (even if they craft a request with a different
|
||||
// username, that field is ignored).
|
||||
// 3. No new session is minted — the existing one is preserved.
|
||||
//
|
||||
// Returns true if the password matches, throws ValidationException
|
||||
// otherwise. The frontend treats the exception as "wrong password".
|
||||
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: Operate.Find)]
|
||||
public bool ConfirmPassword(string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ValidationException("password is required");
|
||||
|
||||
var username = service.UserId;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var descKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_DescKey];
|
||||
|
||||
var privateKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_RsaPrivateKey];
|
||||
var rsa = new System.RSA(privateKey, true);
|
||||
|
||||
var psd = rsa.DecodeOrNull(password);
|
||||
if (string.IsNullOrEmpty(psd)) return false;
|
||||
|
||||
return user.UserPassword == psd.Substring(36, psd.Length - 36);
|
||||
}
|
||||
|
||||
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: Operate.Find)]
|
||||
public string BeginBiometricRegistration(string deviceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceName))
|
||||
deviceName = "Unnamed device";
|
||||
|
||||
var username = service.UserId;// loginCredential.GetUsername();
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var userIdBytes = Encoding.UTF8.GetBytes(username);
|
||||
// v1: one credential per user — exclude list is empty.
|
||||
var options = webAuthnService.BuildCreationOptions(username, userIdBytes, Array.Empty<byte[]>());
|
||||
|
||||
return options.ToJson();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("BeginBiometricRegistration ERROR: " + ex.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: Operate.Find)]
|
||||
public BiometricRegistrationResultDto FinishBiometricRegistration(FinishBiometricRegistrationData dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.AttestationJson) || string.IsNullOrWhiteSpace(dto.CreationOptionsJson))
|
||||
throw new ValidationException("attestationJson and creationOptionsJson are required");
|
||||
|
||||
var username = service.UserId;// loginCredential.GetUsername();
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
// Diagnostic: log the raw JSON we received so we can diagnose
|
||||
// Fido2NetLib "Expected rawResponse, got null" failures. The
|
||||
// Fido2NetLib 1.0.0-alpha VerifyAsync checks `Raw == null` at
|
||||
// the top, before any field-level checks — if it fires, the
|
||||
// wrapper was constructed with a null raw, which means either
|
||||
// (a) CredentialCreateOptions.FromJson returned null and the
|
||||
// constructor blew up, or (b) the C# deserializer produced a
|
||||
// shell object whose required properties are all null.
|
||||
//logger.LogInformation(
|
||||
// "FinishBiometricRegistration_DIAG attestationJson[0..400]={att} creationOptionsJson[0..200]={opts}",
|
||||
// dto.AttestationJson.Substring(0, Math.Min(400, dto.AttestationJson.Length)),
|
||||
// dto.CreationOptionsJson.Substring(0, Math.Min(200, dto.CreationOptionsJson.Length)));
|
||||
|
||||
CredentialCreateOptions originalOptions;
|
||||
try
|
||||
{
|
||||
originalOptions = CredentialCreateOptions.FromJson(dto.CreationOptionsJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ValidationException("Invalid creationOptionsJson: " + ex.Message);
|
||||
}
|
||||
if (originalOptions == null)
|
||||
throw new ValidationException("CredentialCreateOptions.FromJson returned null (creationOptionsJson parsed as the JSON literal 'null' or empty)");
|
||||
|
||||
AuthenticatorAttestationRawResponse raw;
|
||||
try
|
||||
{
|
||||
//logger.LogInformation("DTO: " + dto.AttestationJson);
|
||||
// MUST use Newtonsoft.Json here, not System.Text.Json. Fido2NetLib's
|
||||
// AuthenticatorAttestationRawResponse relies on Newtonsoft.Json-specific
|
||||
// attributes that System.Text.Json does not honor:
|
||||
// * [JsonConverter(typeof(Base64UrlConverter))] on Id/RawId/AttestationObject/ClientDataJson
|
||||
// (converts base64url strings to byte[]; System.Text.Json would leave them as strings)
|
||||
// * [JsonConverter(typeof(StringEnumConverter))] on PublicKeyCredentialType.Type
|
||||
// with [EnumMember(Value = "public-key")] on the PublicKey member
|
||||
//
|
||||
// And Fido2NetLib's AuthenticatorAttestationResponse.Parse throws
|
||||
// "Expected rawResponse, got null" when rawResponse.Response is null —
|
||||
// which is exactly what System.Text.Json produces, because it is
|
||||
// case-sensitive by default and looks for `Response` (PascalCase) in
|
||||
// the JSON, while the browser sends `response` (lowercase).
|
||||
// Newtonsoft.Json's default ContractResolver is case-insensitive.
|
||||
raw = JsonConvert.DeserializeObject<AuthenticatorAttestationRawResponse>(dto.AttestationJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ValidationException("Invalid attestationJson: " + ex.Message);
|
||||
}
|
||||
if (raw == null) throw new ValidationException("Invalid attestationJson");
|
||||
if (raw.Response == null) throw new ValidationException("raw.Response is null after deserialization — the browser's JSON didn't bind to Fido2NetLib's Response property");
|
||||
|
||||
// Diagnostic: log what Fido2NetLib will see. If any of these
|
||||
// are null, Fido2NetLib's VerifyAsync will throw the cryptic
|
||||
// "Expected rawResponse, got null" or "Id is missing" / "RawId
|
||||
// is missing" / "clientDataJson cannot be null" etc.
|
||||
//logger.LogInformation(
|
||||
// "FinishBiometricRegistration_DIAG raw.Id={id} raw.RawId={rawId} raw.Type={type} raw.Response.AttestationObject={ao} raw.Response.ClientDataJson={cdj}",
|
||||
// raw.Id,
|
||||
// raw.RawId,
|
||||
// raw.Type,
|
||||
// raw.Response?.AttestationObject == null ? "<null>" : $"<{raw.Response.AttestationObject.Length} chars>",
|
||||
// raw.Response?.ClientDataJson == null ? "<null>" : $"<{raw.Response.ClientDataJson.Length} chars>");
|
||||
|
||||
// Fido2NetLib verifies signature, certificate chain, challenge,
|
||||
// origin, and rpId. We only need to await it.
|
||||
var stored = webAuthnService
|
||||
.FinishRegistrationAsync(raw, originalOptions, _ => Task.FromResult(true))
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// Persist on the user. v1 = one credential per user: just
|
||||
// overwrite. (The excludeCredentials list passed to
|
||||
// BuildCreationOptions is empty, so the authenticator will
|
||||
// refuse to re-enroll the same key on the same device — but
|
||||
// re-enrolling on a new device overwrites. That's intentional.)
|
||||
//
|
||||
// SecurityUser.BiometricCredentialId / BiometricPublicKey are
|
||||
// `string` columns (per the request to keep the schema as-is).
|
||||
// Fido2NetLib gives us `byte[]`; we encode with base64url so
|
||||
// the stored value is directly usable as the credential id
|
||||
// (matches the format returned to the browser in
|
||||
// BiometricRegistrationResultDto.CredentialIdBase64Url) and
|
||||
// the public key can be decoded back to byte[] for the
|
||||
// assertion verification path.
|
||||
user.BiometricCredentialId = Fido2NetLib.Base64Url.Encode(stored.CredentialId);
|
||||
user.BiometricPublicKey = Fido2NetLib.Base64Url.Encode(stored.PublicKey);
|
||||
user.BiometricCounter = 0;
|
||||
user.BiometricDeviceName = dto.AttestationJson.Length > 0 ? "(set on the device)" : null;
|
||||
user.BiometricEnrolledAt = DateTime.Now;
|
||||
user.BiometricLastUsedAt = null;
|
||||
|
||||
repository.UpdateNow(user);
|
||||
|
||||
return new BiometricRegistrationResultDto
|
||||
{
|
||||
CredentialIdBase64Url = Fido2NetLib.Base64Url.Encode(stored.CredentialId)
|
||||
};
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public BiometricLoginChallengeDto BeginBiometricLogin(string username)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("username is required");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
if (user.BiometricCredentialId == null || user.BiometricCredentialId.Length == 0)
|
||||
throw new ValidationException("Biometric not enrolled for this user");
|
||||
|
||||
|
||||
// user.BiometricCredentialId is stored as base64url (see
|
||||
// FinishBiometricRegistration). Decode back to byte[] for
|
||||
// Fido2NetLib's PublicKeyCredentialDescriptor.Id.
|
||||
// BuildRequestOptionsJson bypasses Fido2NetLib 1.0.0-alpha's
|
||||
// PascalCase serializer. See WebAuthnService.BuildRequestOptionsJson
|
||||
// for why the wire shape has to be hand-built.
|
||||
var optionsJson = webAuthnService.BuildRequestOptionsJson(
|
||||
Fido2NetLib.Base64Url.Decode(user.BiometricCredentialId));
|
||||
return new BiometricLoginChallengeDto
|
||||
{
|
||||
RequestOptionsJson = optionsJson,
|
||||
ChallengeId = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public LoginProfile FinishBiometricLogin(FinishBiometricLoginData dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.Username) || string.IsNullOrWhiteSpace(dto.AssertionJson) || string.IsNullOrWhiteSpace(dto.RequestOptionsJson))
|
||||
throw new ValidationException("username, assertionJson, and requestOptionsJson are required");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserMobile == dto.Username || x.UserName == dto.Username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
if (user.BiometricCredentialId == null || user.BiometricPublicKey == null)
|
||||
throw new ValidationException("Biometric not enrolled for this user");
|
||||
|
||||
AssertionOptions originalOptions;
|
||||
try
|
||||
{
|
||||
// dto.RequestOptionsJson arrives double-JSON-encoded: the
|
||||
// backend returns the options as a JSON string (e.g.
|
||||
// '{"challenge":"...","rpId":"..."}'
|
||||
// ), and the frontend then does JSON.stringify() on that
|
||||
// string, wrapping it in another layer of quotes:
|
||||
// '"{\"challenge\":\"...\",\"rpId\":\"..."}"'
|
||||
// Fido2NetLib's AssertionOptions.FromJson then tries to
|
||||
// deserialize that as an AssertionOptions object and throws
|
||||
// "Could not cast or convert from System.String to
|
||||
// Fido2NetLib.AssertionOptions" because it sees a string
|
||||
// token at the top level.
|
||||
//
|
||||
// Unwrap one layer with JToken.Parse: if the result is a
|
||||
// JValue (the JSON-encoded string case), parse its string
|
||||
// value to get the inner JSON. If it's already a JObject
|
||||
// (no double-encoding), just use ToString().
|
||||
var token = Newtonsoft.Json.Linq.JToken.Parse(dto.RequestOptionsJson);
|
||||
if (token is Newtonsoft.Json.Linq.JValue v && v.Type == Newtonsoft.Json.Linq.JTokenType.String)
|
||||
{
|
||||
token = Newtonsoft.Json.Linq.JToken.Parse((string)v.Value!);
|
||||
}
|
||||
originalOptions = AssertionOptions.FromJson(token.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishBiometricLogin_ERROR_1: " + ex.ToString());
|
||||
throw new ValidationException("Invalid requestOptionsJson: " + ex.Message);
|
||||
}
|
||||
|
||||
AuthenticatorAssertionRawResponse raw;
|
||||
try
|
||||
{
|
||||
// Fido2NetLib 1.0.0-alpha declares Id/RawId/Response.* as `string`
|
||||
// (no [JsonConverter] — that was added in v1.1.0+), and internally
|
||||
// does (byte[])raw.Id to look up the credential. The (byte[]) cast
|
||||
// throws "Unable to cast object of type 'System.String' to type
|
||||
// 'System.Byte[]'" on the assertion flow (the registration flow
|
||||
// doesn't hit it because its Parse() path only touches
|
||||
// Response.AttestationObject, never Id/RawId).
|
||||
//
|
||||
// Fix: deserialize normally with Newtonsoft.Json, then patch the
|
||||
// base64url string fields to byte[] via reflection. If the C#
|
||||
// property is byte[] (1.1.0+ shape) this is a no-op. If it's
|
||||
// string (1.0.0-alpha shape) the reflection write will fail at
|
||||
// runtime with a clear ArgumentException — see the catch below
|
||||
// for the workaround.
|
||||
raw = JsonConvert.DeserializeObject<AuthenticatorAssertionRawResponse>(dto.AssertionJson);
|
||||
WebAuthnAssertionPatcher.PatchResponseToByteArrays(raw);
|
||||
|
||||
// DIAG: log the actual runtime property type so we can confirm
|
||||
// the shape of the Fido2NetLib 1.0.0-alpha class and choose the
|
||||
// right patching strategy.
|
||||
//logger.LogInformation(
|
||||
// "FinishBiometricLogin_DIAG Id={idType} RawId={rawIdType} Response.Id={respIdType} Response.AuthenticatorData={adType} Response.Signature={sigType} Response.ClientDataJson={cdjType}",
|
||||
// typeof(AuthenticatorAssertionRawResponse).GetProperty("Id")?.PropertyType.Name,
|
||||
// typeof(AuthenticatorAssertionRawResponse).GetProperty("RawId")?.PropertyType.Name,
|
||||
// typeof(AuthenticatorAssertionRawResponse.AssertionResponse).GetProperty("Id")?.PropertyType.Name,
|
||||
// typeof(AuthenticatorAssertionRawResponse.AssertionResponse).GetProperty("AuthenticatorData")?.PropertyType.Name,
|
||||
// typeof(AuthenticatorAssertionRawResponse.AssertionResponse).GetProperty("Signature")?.PropertyType.Name,
|
||||
// typeof(AuthenticatorAssertionRawResponse.AssertionResponse).GetProperty("ClientDataJson")?.PropertyType.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishBiometricLogin_ERROR_2: " + ex.ToString());
|
||||
throw new ValidationException("Invalid assertionJson: " + ex.Message);
|
||||
}
|
||||
if (raw == null)
|
||||
{
|
||||
//logger.LogError("FinishBiometricLogin_ERROR_3:");
|
||||
throw new ValidationException("Invalid assertionJson");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// user.BiometricPublicKey is stored as base64url; decode to
|
||||
// byte[] for Fido2NetLib's MakeAssertionAsync signature.
|
||||
var newCounter = webAuthnService
|
||||
.FinishAssertionAsync(raw, originalOptions, Fido2NetLib.Base64Url.Decode(user.BiometricPublicKey), (uint)user.BiometricCounter)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// Persist counter + last-used timestamp.
|
||||
user.BiometricCounter = (int)newCounter;
|
||||
user.BiometricLastUsedAt = DateTime.Now;
|
||||
repository.UpdateNow(user);
|
||||
|
||||
// Mint the session the same way AuthProcess.Login does.
|
||||
|
||||
return PackLoginProfile(user, CulturePolicy.ZH_CN);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishBiometricLogin_ERROR_4: " + ex.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Epayslip biometric verification (step-up auth, not a new login) =====
|
||||
//
|
||||
// BeginBiometricLogin / FinishBiometricLogin mint a new JWT session
|
||||
// (they are the password-less login flow). The two webmethods below
|
||||
// reuse the same WebAuthn ceremony against the CURRENTLY-authenticated
|
||||
// user's stored credential, but instead of issuing a fresh profile
|
||||
// they just stamp a 5-minute unlock in EpayslipBiometricUnlockStore
|
||||
// so SalaryTaxWebProcess.SalaryTax can accept biometricVerified=true
|
||||
// as a substitute for the epayslip password.
|
||||
//
|
||||
// Both webmethods require the user to be logged in (the auto-generated
|
||||
// UserProfileController has [Authorize] at the class level) AND
|
||||
// require biometric_for_epayslip=1 -- we re-check the server flag
|
||||
// here even though the frontend gates on it, so a user cannot
|
||||
// accidentally hit Begin from a stale tab after disabling the flag.
|
||||
|
||||
// Returns the WebAuthn request options JSON for the current user's
|
||||
// stored credential. The frontend parses the string and passes it to
|
||||
// @simplewebauthn/browser's startAuthentication.
|
||||
//
|
||||
// Wrapped in EpayslipBiometricAssertionChallengeDto (not returned
|
||||
// as a raw string) so the auto-generated TS service stub's
|
||||
// resp.requestOptionsJson mirrors the BiometricLoginChallengeDto
|
||||
// contract used by LoginView. A raw-string return would serialize
|
||||
// as a JSON-encoded quoted string and confuse the frontend's
|
||||
// JSON.parse step.
|
||||
|
||||
public BiometricLoginChallengeDto BeginEpayslipBiometricAssertion()
|
||||
{
|
||||
var username = service.UserId;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
// Defense in depth: refuse to issue a challenge if the user
|
||||
// has not enrolled biometric login or has not opted in to
|
||||
// biometric-for-epayslip.
|
||||
if (string.IsNullOrEmpty(user.BiometricCredentialId))
|
||||
throw new ValidationException("Biometric login must be enabled first");
|
||||
|
||||
// user.BiometricCredentialId is stored as base64url (see
|
||||
// FinishBiometricRegistration). Decode back to byte[] for
|
||||
// Fido2NetLib's PublicKeyCredentialDescriptor.Id.
|
||||
return new BiometricLoginChallengeDto
|
||||
{
|
||||
RequestOptionsJson = webAuthnService.BuildRequestOptionsJson(
|
||||
Fido2NetLib.Base64Url.Decode(user.BiometricCredentialId))
|
||||
};
|
||||
}
|
||||
|
||||
// Verifies the assertion produced by the browser from
|
||||
// BeginEpayslipBiometricAssertion's challenge. On success stamps
|
||||
// the unlock store for 5 minutes -- SalaryTax can then accept
|
||||
// biometricVerified=true instead of the epayslip password hash.
|
||||
//
|
||||
// Same Newtonsoft + double-JSON-decode dance as FinishBiometricLogin
|
||||
// (see that method's comments for why Newtonsoft specifically and
|
||||
// why the options string needs unwrapping); only the persistence
|
||||
// step is different -- we do not mint a new session.
|
||||
|
||||
public bool FinishEpayslipBiometricAssertion(string assertionJson, string requestOptionsJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assertionJson) || string.IsNullOrWhiteSpace(requestOptionsJson))
|
||||
throw new ValidationException("assertionJson and requestOptionsJson are required");
|
||||
|
||||
var username = service.UserId;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
if (string.IsNullOrEmpty(user.BiometricCredentialId) || string.IsNullOrEmpty(user.BiometricPublicKey))
|
||||
throw new ValidationException("Biometric not enrolled for this user");
|
||||
|
||||
AssertionOptions originalOptions;
|
||||
try
|
||||
{
|
||||
// Same unwrap as FinishBiometricLogin: the frontend
|
||||
// double-JSON-encodes the options string, so the top-level
|
||||
// JToken is a JSON string of a JSON object. Unwrap one layer
|
||||
// before handing to Fido2NetLib's FromJson.
|
||||
var token = Newtonsoft.Json.Linq.JToken.Parse(requestOptionsJson);
|
||||
if (token is Newtonsoft.Json.Linq.JValue v && v.Type == Newtonsoft.Json.Linq.JTokenType.String)
|
||||
{
|
||||
token = Newtonsoft.Json.Linq.JToken.Parse((string)v.Value!);
|
||||
}
|
||||
originalOptions = AssertionOptions.FromJson(token.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishEpayslipBiometricAssertion_ERROR_1: " + ex.ToString());
|
||||
throw new ValidationException("Invalid requestOptionsJson: " + ex.Message);
|
||||
}
|
||||
|
||||
AuthenticatorAssertionRawResponse raw;
|
||||
try
|
||||
{
|
||||
// See WebAuthnAssertionPatcher for why this is needed.
|
||||
raw = JsonConvert.DeserializeObject<AuthenticatorAssertionRawResponse>(assertionJson);
|
||||
WebAuthnAssertionPatcher.PatchResponseToByteArrays(raw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishEpayslipBiometricAssertion_ERROR_2: " + ex.ToString());
|
||||
throw new ValidationException("Invalid assertionJson: " + ex.Message);
|
||||
}
|
||||
if (raw == null)
|
||||
throw new ValidationException("Invalid assertionJson");
|
||||
if (raw.Response == null)
|
||||
throw new ValidationException("assertionJson is missing the response field");
|
||||
|
||||
try
|
||||
{
|
||||
// user.BiometricPublicKey is stored as base64url; decode to
|
||||
// byte[] for Fido2NetLib's MakeAssertionAsync signature.
|
||||
var newCounter = webAuthnService
|
||||
.FinishAssertionAsync(raw, originalOptions, Fido2NetLib.Base64Url.Decode(user.BiometricPublicKey), (uint)user.BiometricCounter)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
user.BiometricCounter = (int)newCounter;
|
||||
user.BiometricLastUsedAt = DateTime.Now;
|
||||
|
||||
repository.UpdateNow(user);
|
||||
|
||||
|
||||
// Stamp the unlock. Default duration is 5 minutes -- see
|
||||
// EpayslipBiometricUnlockStore.DefaultDuration.
|
||||
//epayslipUnlockStore.Unlock(username);
|
||||
//logger.LogInformation("FinishEpayslipBiometricAssertion: unlocked for username={username}", username);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//logger.LogError("FinishEpayslipBiometricAssertion_ERROR_3: " + ex.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: Operate.Find)]
|
||||
public void RemoveBiometric()
|
||||
{
|
||||
var username = service.UserId;// loginCredential.GetUsername();
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
var user = repository.DetachedEntities.FirstOrDefault(x => x.UserName == username);
|
||||
if (user == null)
|
||||
throw new ValidationException("User not found.");
|
||||
|
||||
user.BiometricCredentialId = null;
|
||||
user.BiometricPublicKey = null;
|
||||
user.BiometricCounter = 0;
|
||||
user.BiometricDeviceName = null;
|
||||
user.BiometricEnrolledAt = null;
|
||||
user.BiometricLastUsedAt = null;
|
||||
|
||||
repository.UpdateNow(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
|
||||
<PackageReference Include="Fido2NetLib" Version="1.0.0-alpha" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using Fido2NetLib;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Service
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Re-encodes the base64url string fields on a freshly-deserialized
|
||||
/// <see cref="AuthenticatorAssertionRawResponse"/> as <c>byte[]</c>.
|
||||
///
|
||||
/// Fido2NetLib 1.0.0-alpha has these properties typed as <c>string</c>
|
||||
/// (no [JsonConverter] — that was added in v1.1.0+), but its
|
||||
/// <c>MakeAssertionAsync</c> internally does
|
||||
/// <c>(byte[])raw.Id</c> and
|
||||
/// <c>(byte[])raw.Response.ClientDataJson</c> to look up the credential
|
||||
/// and verify the signature. The hard cast throws
|
||||
/// "Unable to cast object of type 'System.String' to type 'System.Byte[]'".
|
||||
///
|
||||
/// This helper was originally a private static method on
|
||||
/// <c>UserProfileProcess</c>; it was extracted so
|
||||
/// <c>FinishEpayslipBiometricAssertion</c> (the new epayslip-unlock
|
||||
/// flow) can share the same Fido2NetLib 1.0.0-alpha workaround without
|
||||
/// duplicating the reflection-patching logic.
|
||||
///
|
||||
/// Behaviour:
|
||||
/// 1. Checks the runtime property type.
|
||||
/// 2. If it's already <c>byte[]</c> (v1.1.0+), base64url-decodes the
|
||||
/// incoming string and stores the bytes.
|
||||
/// 3. If it's <c>string</c> (v1.0.0-alpha), we can't store <c>byte[]</c>
|
||||
/// in a <c>string</c> field — CLR enforces type safety. Fido2NetLib's
|
||||
/// internal (byte[]) cast will then throw on the next call with a
|
||||
/// clear InvalidCastException we can diagnose from the stack trace.
|
||||
/// </summary>
|
||||
public static class WebAuthnAssertionPatcher
|
||||
{
|
||||
public static void PatchResponseToByteArrays(AuthenticatorAssertionRawResponse raw)
|
||||
{
|
||||
if (raw == null) return;
|
||||
|
||||
PatchStringPropertyToByteArray(raw, "Id");
|
||||
PatchStringPropertyToByteArray(raw, "RawId");
|
||||
|
||||
if (raw.Response != null)
|
||||
{
|
||||
PatchStringPropertyToByteArray(raw.Response, "Id");
|
||||
PatchStringPropertyToByteArray(raw.Response, "AuthenticatorData");
|
||||
PatchStringPropertyToByteArray(raw.Response, "Signature");
|
||||
PatchStringPropertyToByteArray(raw.Response, "ClientDataJson");
|
||||
PatchStringPropertyToByteArray(raw.Response, "UserHandle");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PatchStringPropertyToByteArray(object target, string propertyName)
|
||||
{
|
||||
if (target == null) return;
|
||||
var prop = target.GetType().GetProperty(propertyName);
|
||||
if (prop == null) return;
|
||||
if (!prop.CanWrite) return;
|
||||
var current = prop.GetValue(target);
|
||||
// If Fido2NetLib 1.1.0+ shape — property is already byte[].
|
||||
if (prop.PropertyType == typeof(byte[]))
|
||||
{
|
||||
if (current is byte[]) return; // already decoded by the converter
|
||||
if (current is string s && !string.IsNullOrEmpty(s))
|
||||
{
|
||||
try { prop.SetValue(target, Fido2NetLib.Base64Url.Decode(s)); }
|
||||
catch { /* leave original value; Fido2NetLib will surface a clearer error */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the property is `object` (1.0.0-alpha may store the raw
|
||||
// deserialized value as object), we can safely put a byte[] in it.
|
||||
if (prop.PropertyType == typeof(object))
|
||||
{
|
||||
if (current is string s && !string.IsNullOrEmpty(s))
|
||||
{
|
||||
try { prop.SetValue(target, Fido2NetLib.Base64Url.Decode(s)); } catch { }
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the property is `string` (1.0.0-alpha default), we cannot
|
||||
// store byte[] in a string-typed field — CLR enforces type safety
|
||||
// even via reflection. Fido2NetLib's internal (byte[]) cast will
|
||||
// throw on the next call. The caller will see a clear
|
||||
// InvalidCastException that we can diagnose from the stack trace.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using CloudBuilder.Security.Data;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Service
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the Fido2NetLib ceremony primitives (challenge generation,
|
||||
/// attestation verification, assertion verification) so the process
|
||||
/// layer can stay focused on the workflow. The frontend drives both
|
||||
/// registration and login through this service via webmethods on
|
||||
/// UserProfileProcess.
|
||||
/// </summary>
|
||||
public class WebAuthnService: IWebAuthnService
|
||||
{
|
||||
private readonly Fido2 _fido2;
|
||||
private readonly WebAuthnOptions _options;
|
||||
|
||||
public WebAuthnService(IOptions<WebAuthnOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
|
||||
var config = new Fido2.Configuration
|
||||
{
|
||||
ServerDomain = _options.RpId,
|
||||
ServerName = _options.RpName,
|
||||
Origin = _options.Origin,
|
||||
// Fido2NetLib expresses the challenge length in BYTES here.
|
||||
// 32 bytes = 256 bits, the WebAuthn-recommended minimum.
|
||||
ChallengeSize = 32,
|
||||
Timeout = (uint)_options.TimeoutMs
|
||||
};
|
||||
_fido2 = new Fido2(config);
|
||||
}
|
||||
|
||||
public string Origin => _options.Origin;
|
||||
public string RpId => _options.RpId;
|
||||
|
||||
/// <summary>
|
||||
/// Build the PublicKeyCredentialCreationOptions that the browser
|
||||
/// will hand to <c>navigator.credentials.create()</c>.
|
||||
/// </summary>
|
||||
/// <param name="username">HR+ username (becomes the WebAuthn user.name + user.displayName).</param>
|
||||
/// <param name="userId">Stable per-user byte array. We use the username's UTF-8 bytes — fine for v1 because usernames are unique.</param>
|
||||
/// <param name="existingCredentialIds">If the user is re-enrolling on a new device, the existing credential ID is passed via <c>excludeCredentials</c> so the authenticator refuses to re-enroll the same one. For v1 we don't track this (one-device-only); pass an empty list.</param>
|
||||
public CredentialCreateOptions BuildCreationOptions(string username, byte[] userId, IReadOnlyList<byte[]> existingCredentialIds)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Name = username,
|
||||
Id = userId,
|
||||
DisplayName = username
|
||||
};
|
||||
|
||||
var exclude = existingCredentialIds
|
||||
.Select(id => new PublicKeyCredentialDescriptor { Id = id })
|
||||
.ToList();
|
||||
|
||||
return _fido2.RequestNewCredential(
|
||||
user,
|
||||
exclude,
|
||||
new AuthenticatorSelection
|
||||
{
|
||||
// Prefer the platform authenticator (Touch ID / Windows Hello /
|
||||
// fingerprint) over roaming keys.
|
||||
AuthenticatorAttachment = AuthenticatorAttachment.Platform,
|
||||
UserVerification = UserVerificationRequirement.Preferred
|
||||
},
|
||||
AttestationConveyancePreference.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify the attestation returned by the browser and return the
|
||||
/// stored credential fields. Throws on validation failure.
|
||||
/// </summary>
|
||||
public async Task<StoredBiometricCredential> FinishRegistrationAsync(
|
||||
AuthenticatorAttestationRawResponse attestationResponse,
|
||||
CredentialCreateOptions originalOptions,
|
||||
Func<byte[], Task<bool>> isCredentialIdUnique)
|
||||
{
|
||||
// Fido2NetLib's MakeNewCredentialAsync takes a delegate that, given
|
||||
// the new credential id, decides whether the credential is unique
|
||||
// for the user. In our schema the credential id is stored on
|
||||
// SecurityUser.biometric_credential_id, so uniqueness is automatic
|
||||
// — the same credential id can never be assigned to two users.
|
||||
// We always return true.
|
||||
var makeResult = await _fido2.MakeNewCredentialAsync(
|
||||
attestationResponse,
|
||||
originalOptions,
|
||||
(args) => Task.FromResult(true),
|
||||
requestTokenBindingId: null);
|
||||
|
||||
if (makeResult?.Result is not AttestationVerificationSuccess success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Fido2 attestation verification failed: " +
|
||||
(makeResult?.ErrorMessage ?? "unknown error"));
|
||||
}
|
||||
|
||||
return new StoredBiometricCredential
|
||||
{
|
||||
CredentialId = success.CredentialId,
|
||||
PublicKey = success.PublicKey
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the PublicKeyCredentialRequestOptions that the browser will
|
||||
/// hand to <c>navigator.credentials.get()</c>. Only the stored
|
||||
/// credential for this user is allowed.
|
||||
/// </summary>
|
||||
public AssertionOptions BuildRequestOptions(byte[] storedCredentialId)
|
||||
{
|
||||
var allowed = new List<PublicKeyCredentialDescriptor>
|
||||
{
|
||||
new() { Id = storedCredentialId }
|
||||
};
|
||||
return _fido2.GetAssertionOptions(allowed, UserVerificationRequirement.Preferred);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="BuildRequestOptions"/> but returns a
|
||||
/// browser-ready JSON string directly.
|
||||
///
|
||||
/// Fido2NetLib 1.0.0-alpha's <c>AssertionOptions.ToJson()</c> serializes
|
||||
/// <c>AllowCredentials[].Id</c> as PascalCase (the C# property is
|
||||
/// <c>Id</c> with no <c>[JsonProperty("id")]</c> attribute — the
|
||||
/// attribute was added in v1.1.0+). The browser's
|
||||
/// @simplewebauthn/browser <c>toPublicKeyCredentialDescriptor</c>
|
||||
/// does <c>const { id } = descriptor</c>, gets <c>undefined</c>, then
|
||||
/// passes it to <c>base64URLStringToBuffer(id)</c> which does
|
||||
/// <c>id.replace(...)</c> and throws
|
||||
/// "Cannot read properties of undefined (reading 'replace')".
|
||||
///
|
||||
/// We bypass Fido2NetLib's serializer entirely and emit the JSON
|
||||
/// shape the WebAuthn spec + the browser expect directly: camelCase
|
||||
/// keys, <c>allowCredentials[].id</c> as base64url, <c>type: "public-key"</c>,
|
||||
/// <c>userVerification: "preferred"</c>.
|
||||
/// </summary>
|
||||
public string BuildRequestOptionsJson(byte[] storedCredentialId)
|
||||
{
|
||||
var options = _fido2.GetAssertionOptions(
|
||||
new List<PublicKeyCredentialDescriptor> { new() { Id = storedCredentialId } },
|
||||
UserVerificationRequirement.Preferred);
|
||||
|
||||
var result = new
|
||||
{
|
||||
challenge = options.Challenge,
|
||||
rpId = options.RpId,
|
||||
allowCredentials = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = Fido2NetLib.Base64Url.Encode(storedCredentialId),
|
||||
type = "public-key",
|
||||
},
|
||||
},
|
||||
userVerification = options.UserVerification?.ToString().ToLower() ?? "preferred",
|
||||
timeout = options.Timeout,
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify the assertion returned by the browser. Returns the new
|
||||
/// signature counter to persist. Throws on validation failure.
|
||||
/// </summary>
|
||||
public async Task<uint> FinishAssertionAsync(
|
||||
AuthenticatorAssertionRawResponse assertionResponse,
|
||||
AssertionOptions originalOptions,
|
||||
byte[] storedPublicKey,
|
||||
uint storedCounter)
|
||||
{
|
||||
var result = await _fido2.MakeAssertionAsync(
|
||||
assertionResponse,
|
||||
originalOptions,
|
||||
storedPublicKey,
|
||||
storedCounter,
|
||||
(args) => Task.FromResult(true), // userHandle ownership always true for our single-user case
|
||||
requestTokenBindingId: null);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Fido2 assertion verification returned null");
|
||||
}
|
||||
|
||||
// MakeAssertionAsync returns AssertionVerificationSuccess, but in
|
||||
// some Fido2NetLib versions it may surface a status/error wrapper.
|
||||
// Use the typed object via dynamic to stay version-agnostic.
|
||||
uint newCounter;
|
||||
try
|
||||
{
|
||||
newCounter = (uint)result.Counter;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException("Fido2 assertion verification did not return a counter");
|
||||
}
|
||||
|
||||
// The counter MUST be strictly greater than the stored value
|
||||
// (replay protection). Fido2NetLib already enforces this server-side
|
||||
// and would have thrown; the second check is a belt-and-suspenders.
|
||||
if (newCounter <= storedCounter)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"WebAuthn counter regression: stored={storedCounter}, new={newCounter}");
|
||||
}
|
||||
|
||||
return newCounter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
|
||||
<PackageReference Include="Fido2NetLib" Version="1.0.0-alpha" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Data
|
||||
{
|
||||
[TypeScript]
|
||||
public class BiometricLoginChallengeDto
|
||||
{
|
||||
/// <summary>AssertionOptions JSON to feed to navigator.credentials.get().</summary>
|
||||
public string RequestOptionsJson { get; set; } = null!;
|
||||
|
||||
/// <summary>Opaque id identifying this challenge — we round-trip the requestOptionsJson instead, so this is informational and currently unused.</summary>
|
||||
public string ChallengeId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Data
|
||||
{
|
||||
[TypeScript]
|
||||
public class BiometricRegistrationResultDto
|
||||
{
|
||||
public string CredentialIdBase64Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Data
|
||||
{
|
||||
[TypeScript]
|
||||
public class FinishBiometricLoginData
|
||||
{
|
||||
public string Username { get; set; } = null!;
|
||||
public string AssertionJson { get; set; } = null!;
|
||||
public string RequestOptionsJson { get; set; } = null!;
|
||||
|
||||
/// <summary>Opaque id; informational only. We round-trip the requestOptionsJson.</summary>
|
||||
public string ChallengeId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Data
|
||||
{
|
||||
[TypeScript]
|
||||
public class FinishBiometricRegistrationData
|
||||
{
|
||||
/// <summary>JSON the browser returns from navigator.credentials.create().</summary>
|
||||
public string AttestationJson { get; set; } = null!;
|
||||
|
||||
/// <summary>The CredentialCreateOptions JSON we returned from BeginBiometricRegistration. Round-tripped because Fido2NetLib verifies the challenge against it.</summary>
|
||||
public string CreationOptionsJson { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Security.Data
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Plain-old-data wrapper for the credential fields we persist on
|
||||
/// SecurityUser. The frontend never sees this type.
|
||||
/// </summary>
|
||||
public class StoredBiometricCredential
|
||||
{
|
||||
public byte[] CredentialId { get; set; } = Array.Empty<byte>();
|
||||
public byte[] PublicKey { get; set; } = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public class WebAuthnOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Relying Party ID — the registrable domain suffix of the
|
||||
/// origin's host. For dev: "localhost". For production:
|
||||
/// "eleave.1010printing.com".
|
||||
/// </summary>
|
||||
public string RpId { get; set; } = "localhost";
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable relying-party name. Shown in the OS biometric
|
||||
/// prompt ("Use HR Plus to sign in to localhost").
|
||||
/// </summary>
|
||||
public string RpName { get; set; } = "HR Plus";
|
||||
|
||||
/// <summary>
|
||||
/// The full origin (scheme + host + port) the user is loading
|
||||
/// the page from. Fido2NetLib verifies the attestation/assertion
|
||||
/// origin matches this exactly. Must be HTTPS in production
|
||||
/// (localhost is exempt).
|
||||
/// </summary>
|
||||
public string Origin { get; set; } = "https://localhost:8000";
|
||||
|
||||
/// <summary>
|
||||
/// WebAuthn ceremony timeout in milliseconds. Default 60s.
|
||||
/// </summary>
|
||||
public int TimeoutMs { get; set; } = 60000;
|
||||
}
|
||||
}
|
||||
@@ -26,42 +26,22 @@ namespace CloudBuilder.Security.Entity
|
||||
public string UserName { get; set; }
|
||||
[Column("user_local_name")]
|
||||
public string UserLocalName { get; set; }
|
||||
[Column("user_eng_name")]
|
||||
public string? UserEngName { get; set; }
|
||||
[Column("company_name")]
|
||||
public string? CompanyName { get; set; }
|
||||
[Column("industry")]
|
||||
public string? Industry { get; set; }
|
||||
[Column("user_password")]
|
||||
public string? UserPassword { get; set; }
|
||||
[Column("user_mobile")]
|
||||
public string? UserMobile { get; set; }
|
||||
[Column("user_address")]
|
||||
public string? UserAddress { get; set; }
|
||||
[Column("postcode")]
|
||||
public string? Postcode { get; set; }
|
||||
[Column("community")]
|
||||
public int Community { get; set; }
|
||||
[Column("user_grade")]
|
||||
public int UserGrade { get; set; }
|
||||
[Column("user_type")]
|
||||
public string? UserType { get; set; }
|
||||
[Column("user_wechat_account")]
|
||||
public string? UserWechatAccount { get; set; }
|
||||
[Column("user_alipay_account")]
|
||||
public string? UserAlipayAccount { get; set; }
|
||||
[Column("user_alipay_account_img_path")]
|
||||
public string? UserAlipayAccountImgPath { get; set; }
|
||||
[Column("user_wechat_account_img_path")]
|
||||
public string? UserWechatAccountImgPath { get; set; }
|
||||
[Column("user_icon_img_path")]
|
||||
public string? UserIconImgPath { get; set; }
|
||||
[Column("user_accounts_expire_datetime")]
|
||||
public Nullable<DateTime> UserAccountsExpireDatetime { get; set; }
|
||||
[Column("user_is_valid_indc")]
|
||||
public string? UserIsValidIndc { get; set; }
|
||||
[Column("is_forece_change_password_indc")]
|
||||
public string? IsForeceChangePasswordIndc { get; set; }
|
||||
[Column("biometric_enrolled_at")]
|
||||
public Nullable<DateTime> BiometricEnrolledAt { get; set; }
|
||||
[Column("biometric_last_used_at")]
|
||||
public Nullable<DateTime> BiometricLastUsedAt { get; set; }
|
||||
[Column("last_login_datetime")]
|
||||
public Nullable<DateTime> LastLoginDatetime { get; set; }
|
||||
[Column("error_login_count")]
|
||||
public int ErrorLoginCount { get; set; }
|
||||
[Column("biometric_credential_id")]
|
||||
public string? BiometricCredentialId { get; set; }
|
||||
[Column("biometric_public_key")]
|
||||
public string? BiometricPublicKey { get; set; }
|
||||
[Column("biometric_counter")]
|
||||
public int BiometricCounter { get; set; }
|
||||
[Column("biometric_device_name")]
|
||||
public string? BiometricDeviceName { get; set; }
|
||||
[Column("is_accounts_locked_indc")]
|
||||
public string? IsAccountsLockedIndc { get; set; }
|
||||
[Column("accounts_locked_datetime")]
|
||||
@@ -74,10 +54,42 @@ namespace CloudBuilder.Security.Entity
|
||||
public Nullable<DateTime> UserTypeEndDatetime { get; set; }
|
||||
[Column("room_name")]
|
||||
public string? RoomName { get; set; }
|
||||
[Column("last_login_datetime")]
|
||||
public Nullable<DateTime> LastLoginDatetime { get; set; }
|
||||
[Column("error_login_count")]
|
||||
public int ErrorLoginCount { get; set; }
|
||||
[Column("user_alipay_account_img_path")]
|
||||
public string? UserAlipayAccountImgPath { get; set; }
|
||||
[Column("user_wechat_account_img_path")]
|
||||
public string? UserWechatAccountImgPath { get; set; }
|
||||
[Column("user_icon_img_path")]
|
||||
public string? UserIconImgPath { get; set; }
|
||||
[Column("user_accounts_expire_datetime")]
|
||||
public Nullable<DateTime> UserAccountsExpireDatetime { get; set; }
|
||||
[Column("user_is_valid_indc")]
|
||||
public string? UserIsValidIndc { get; set; }
|
||||
[Column("is_forece_change_password_indc")]
|
||||
public string? IsForeceChangePasswordIndc { get; set; }
|
||||
[Column("postcode")]
|
||||
public string? Postcode { get; set; }
|
||||
[Column("community")]
|
||||
public int Community { get; set; }
|
||||
[Column("user_grade")]
|
||||
public int UserGrade { get; set; }
|
||||
[Column("user_type")]
|
||||
public string? UserType { get; set; }
|
||||
[Column("user_wechat_account")]
|
||||
public string? UserWechatAccount { get; set; }
|
||||
[Column("user_alipay_account")]
|
||||
public string? UserAlipayAccount { get; set; }
|
||||
[Column("user_eng_name")]
|
||||
public string? UserEngName { get; set; }
|
||||
[Column("company_name")]
|
||||
public string? CompanyName { get; set; }
|
||||
[Column("industry")]
|
||||
public string? Industry { get; set; }
|
||||
[Column("user_password")]
|
||||
public string? UserPassword { get; set; }
|
||||
[Column("user_mobile")]
|
||||
public string? UserMobile { get; set; }
|
||||
[Column("user_address")]
|
||||
public string? UserAddress { get; set; }
|
||||
|
||||
public const string CREATED_DATETIME = "CreatedDatetime";
|
||||
public const string CREATED_BY = "CreatedBy";
|
||||
@@ -85,35 +97,41 @@ namespace CloudBuilder.Security.Entity
|
||||
public const string UPDATED_BY = "UpdatedBy";
|
||||
public const string USER_NAME = "UserName";
|
||||
public const string USER_LOCAL_NAME = "UserLocalName";
|
||||
public const string USER_ENG_NAME = "UserEngName";
|
||||
public const string COMPANY_NAME = "CompanyName";
|
||||
public const string INDUSTRY = "Industry";
|
||||
public const string USER_PASSWORD = "UserPassword";
|
||||
public const string USER_MOBILE = "UserMobile";
|
||||
public const string USER_ADDRESS = "UserAddress";
|
||||
public const string POSTCODE = "Postcode";
|
||||
public const string COMMUNITY = "Community";
|
||||
public const string USER_GRADE = "UserGrade";
|
||||
public const string USER_TYPE = "UserType";
|
||||
public const string USER_WECHAT_ACCOUNT = "UserWechatAccount";
|
||||
public const string USER_ALIPAY_ACCOUNT = "UserAlipayAccount";
|
||||
public const string USER_ALIPAY_ACCOUNT_IMG_PATH = "UserAlipayAccountImgPath";
|
||||
public const string USER_WECHAT_ACCOUNT_IMG_PATH = "UserWechatAccountImgPath";
|
||||
public const string USER_ICON_IMG_PATH = "UserIconImgPath";
|
||||
public const string USER_ACCOUNTS_EXPIRE_DATETIME = "UserAccountsExpireDatetime";
|
||||
public const string USER_IS_VALID_INDC = "UserIsValidIndc";
|
||||
public const string IS_FORECE_CHANGE_PASSWORD_INDC = "IsForeceChangePasswordIndc";
|
||||
public const string BIOMETRIC_ENROLLED_AT = "BiometricEnrolledAt";
|
||||
public const string BIOMETRIC_LAST_USED_AT = "BiometricLastUsedAt";
|
||||
public const string LAST_LOGIN_DATETIME = "LastLoginDatetime";
|
||||
public const string ERROR_LOGIN_COUNT = "ErrorLoginCount";
|
||||
public const string BIOMETRIC_CREDENTIAL_ID = "BiometricCredentialId";
|
||||
public const string BIOMETRIC_PUBLIC_KEY = "BiometricPublicKey";
|
||||
public const string BIOMETRIC_COUNTER = "BiometricCounter";
|
||||
public const string BIOMETRIC_DEVICE_NAME = "BiometricDeviceName";
|
||||
public const string IS_ACCOUNTS_LOCKED_INDC = "IsAccountsLockedIndc";
|
||||
public const string ACCOUNTS_LOCKED_DATETIME = "AccountsLockedDatetime";
|
||||
public const string USER_REMARK = "UserRemark";
|
||||
public const string WELCOME_PAGE = "WelcomePage";
|
||||
public const string USER_TYPE_END_DATETIME = "UserTypeEndDatetime";
|
||||
public const string ROOM_NAME = "RoomName";
|
||||
public const string LAST_LOGIN_DATETIME = "LastLoginDatetime";
|
||||
public const string ERROR_LOGIN_COUNT = "ErrorLoginCount";
|
||||
public const string USER_ALIPAY_ACCOUNT_IMG_PATH = "UserAlipayAccountImgPath";
|
||||
public const string USER_WECHAT_ACCOUNT_IMG_PATH = "UserWechatAccountImgPath";
|
||||
public const string USER_ICON_IMG_PATH = "UserIconImgPath";
|
||||
public const string USER_ACCOUNTS_EXPIRE_DATETIME = "UserAccountsExpireDatetime";
|
||||
public const string USER_IS_VALID_INDC = "UserIsValidIndc";
|
||||
public const string IS_FORECE_CHANGE_PASSWORD_INDC = "IsForeceChangePasswordIndc";
|
||||
public const string POSTCODE = "Postcode";
|
||||
public const string COMMUNITY = "Community";
|
||||
public const string USER_GRADE = "UserGrade";
|
||||
public const string USER_TYPE = "UserType";
|
||||
public const string USER_WECHAT_ACCOUNT = "UserWechatAccount";
|
||||
public const string USER_ALIPAY_ACCOUNT = "UserAlipayAccount";
|
||||
public const string USER_ENG_NAME = "UserEngName";
|
||||
public const string COMPANY_NAME = "CompanyName";
|
||||
public const string INDUSTRY = "Industry";
|
||||
public const string USER_PASSWORD = "UserPassword";
|
||||
public const string USER_MOBILE = "UserMobile";
|
||||
public const string USER_ADDRESS = "UserAddress";
|
||||
|
||||
public const string DB_NAME_SECURITY_USER = "security_user";
|
||||
public const string DB_NAME_FIELDS="created_datetime,created_by,updated_datetime,updated_by,user_name,user_local_name,user_eng_name,company_name,industry,user_password,user_mobile,user_address,postcode,community,user_grade,user_type,user_wechat_account,user_alipay_account,user_alipay_account_img_path,user_wechat_account_img_path,user_icon_img_path,user_accounts_expire_datetime,user_is_valid_indc,is_forece_change_password_indc,is_accounts_locked_indc,accounts_locked_datetime,user_remark,welcome_page,user_type_end_datetime,room_name,last_login_datetime,error_login_count";
|
||||
public const string DB_NAME_FIELDS="created_datetime,created_by,updated_datetime,updated_by,user_name,user_local_name,biometric_enrolled_at,biometric_last_used_at,last_login_datetime,error_login_count,biometric_credential_id,biometric_public_key,biometric_counter,biometric_device_name,is_accounts_locked_indc,accounts_locked_datetime,user_remark,welcome_page,user_type_end_datetime,room_name,user_alipay_account_img_path,user_wechat_account_img_path,user_icon_img_path,user_accounts_expire_datetime,user_is_valid_indc,is_forece_change_password_indc,postcode,community,user_grade,user_type,user_wechat_account,user_alipay_account,user_eng_name,company_name,industry,user_password,user_mobile,user_address";
|
||||
|
||||
public const string DB_CREATED_DATETIME = "created_datetime";
|
||||
public const string DB_CREATED_BY = "created_by";
|
||||
@@ -121,32 +139,38 @@ namespace CloudBuilder.Security.Entity
|
||||
public const string DB_UPDATED_BY = "updated_by";
|
||||
public const string DB_USER_NAME = "user_name";
|
||||
public const string DB_USER_LOCAL_NAME = "user_local_name";
|
||||
public const string DB_USER_ENG_NAME = "user_eng_name";
|
||||
public const string DB_COMPANY_NAME = "company_name";
|
||||
public const string DB_INDUSTRY = "industry";
|
||||
public const string DB_USER_PASSWORD = "user_password";
|
||||
public const string DB_USER_MOBILE = "user_mobile";
|
||||
public const string DB_USER_ADDRESS = "user_address";
|
||||
public const string DB_POSTCODE = "postcode";
|
||||
public const string DB_COMMUNITY = "community";
|
||||
public const string DB_USER_GRADE = "user_grade";
|
||||
public const string DB_USER_TYPE = "user_type";
|
||||
public const string DB_USER_WECHAT_ACCOUNT = "user_wechat_account";
|
||||
public const string DB_USER_ALIPAY_ACCOUNT = "user_alipay_account";
|
||||
public const string DB_USER_ALIPAY_ACCOUNT_IMG_PATH = "user_alipay_account_img_path";
|
||||
public const string DB_USER_WECHAT_ACCOUNT_IMG_PATH = "user_wechat_account_img_path";
|
||||
public const string DB_USER_ICON_IMG_PATH = "user_icon_img_path";
|
||||
public const string DB_USER_ACCOUNTS_EXPIRE_DATETIME = "user_accounts_expire_datetime";
|
||||
public const string DB_USER_IS_VALID_INDC = "user_is_valid_indc";
|
||||
public const string DB_IS_FORECE_CHANGE_PASSWORD_INDC = "is_forece_change_password_indc";
|
||||
public const string DB_BIOMETRIC_ENROLLED_AT = "biometric_enrolled_at";
|
||||
public const string DB_BIOMETRIC_LAST_USED_AT = "biometric_last_used_at";
|
||||
public const string DB_LAST_LOGIN_DATETIME = "last_login_datetime";
|
||||
public const string DB_ERROR_LOGIN_COUNT = "error_login_count";
|
||||
public const string DB_BIOMETRIC_CREDENTIAL_ID = "biometric_credential_id";
|
||||
public const string DB_BIOMETRIC_PUBLIC_KEY = "biometric_public_key";
|
||||
public const string DB_BIOMETRIC_COUNTER = "biometric_counter";
|
||||
public const string DB_BIOMETRIC_DEVICE_NAME = "biometric_device_name";
|
||||
public const string DB_IS_ACCOUNTS_LOCKED_INDC = "is_accounts_locked_indc";
|
||||
public const string DB_ACCOUNTS_LOCKED_DATETIME = "accounts_locked_datetime";
|
||||
public const string DB_USER_REMARK = "user_remark";
|
||||
public const string DB_WELCOME_PAGE = "welcome_page";
|
||||
public const string DB_USER_TYPE_END_DATETIME = "user_type_end_datetime";
|
||||
public const string DB_ROOM_NAME = "room_name";
|
||||
public const string DB_LAST_LOGIN_DATETIME = "last_login_datetime";
|
||||
public const string DB_ERROR_LOGIN_COUNT = "error_login_count";
|
||||
public const string DB_USER_ALIPAY_ACCOUNT_IMG_PATH = "user_alipay_account_img_path";
|
||||
public const string DB_USER_WECHAT_ACCOUNT_IMG_PATH = "user_wechat_account_img_path";
|
||||
public const string DB_USER_ICON_IMG_PATH = "user_icon_img_path";
|
||||
public const string DB_USER_ACCOUNTS_EXPIRE_DATETIME = "user_accounts_expire_datetime";
|
||||
public const string DB_USER_IS_VALID_INDC = "user_is_valid_indc";
|
||||
public const string DB_IS_FORECE_CHANGE_PASSWORD_INDC = "is_forece_change_password_indc";
|
||||
public const string DB_POSTCODE = "postcode";
|
||||
public const string DB_COMMUNITY = "community";
|
||||
public const string DB_USER_GRADE = "user_grade";
|
||||
public const string DB_USER_TYPE = "user_type";
|
||||
public const string DB_USER_WECHAT_ACCOUNT = "user_wechat_account";
|
||||
public const string DB_USER_ALIPAY_ACCOUNT = "user_alipay_account";
|
||||
public const string DB_USER_ENG_NAME = "user_eng_name";
|
||||
public const string DB_COMPANY_NAME = "company_name";
|
||||
public const string DB_INDUSTRY = "industry";
|
||||
public const string DB_USER_PASSWORD = "user_password";
|
||||
public const string DB_USER_MOBILE = "user_mobile";
|
||||
public const string DB_USER_ADDRESS = "user_address";
|
||||
|
||||
|
||||
public static SecurityUserEntity GetInstance()
|
||||
@@ -172,32 +196,38 @@ namespace CloudBuilder.Security.Entity
|
||||
this.UpdatedBy = source.UpdatedBy;
|
||||
this.UserName = source.UserName;
|
||||
this.UserLocalName = source.UserLocalName;
|
||||
this.UserEngName = source.UserEngName;
|
||||
this.CompanyName = source.CompanyName;
|
||||
this.Industry = source.Industry;
|
||||
this.UserPassword = source.UserPassword;
|
||||
this.UserMobile = source.UserMobile;
|
||||
this.UserAddress = source.UserAddress;
|
||||
this.Postcode = source.Postcode;
|
||||
this.Community = source.Community;
|
||||
this.UserGrade = source.UserGrade;
|
||||
this.UserType = source.UserType;
|
||||
this.UserWechatAccount = source.UserWechatAccount;
|
||||
this.UserAlipayAccount = source.UserAlipayAccount;
|
||||
this.UserAlipayAccountImgPath = source.UserAlipayAccountImgPath;
|
||||
this.UserWechatAccountImgPath = source.UserWechatAccountImgPath;
|
||||
this.UserIconImgPath = source.UserIconImgPath;
|
||||
this.UserAccountsExpireDatetime = source.UserAccountsExpireDatetime;
|
||||
this.UserIsValidIndc = source.UserIsValidIndc;
|
||||
this.IsForeceChangePasswordIndc = source.IsForeceChangePasswordIndc;
|
||||
this.BiometricEnrolledAt = source.BiometricEnrolledAt;
|
||||
this.BiometricLastUsedAt = source.BiometricLastUsedAt;
|
||||
this.LastLoginDatetime = source.LastLoginDatetime;
|
||||
this.ErrorLoginCount = source.ErrorLoginCount;
|
||||
this.BiometricCredentialId = source.BiometricCredentialId;
|
||||
this.BiometricPublicKey = source.BiometricPublicKey;
|
||||
this.BiometricCounter = source.BiometricCounter;
|
||||
this.BiometricDeviceName = source.BiometricDeviceName;
|
||||
this.IsAccountsLockedIndc = source.IsAccountsLockedIndc;
|
||||
this.AccountsLockedDatetime = source.AccountsLockedDatetime;
|
||||
this.UserRemark = source.UserRemark;
|
||||
this.WelcomePage = source.WelcomePage;
|
||||
this.UserTypeEndDatetime = source.UserTypeEndDatetime;
|
||||
this.RoomName = source.RoomName;
|
||||
this.LastLoginDatetime = source.LastLoginDatetime;
|
||||
this.ErrorLoginCount = source.ErrorLoginCount;
|
||||
this.UserAlipayAccountImgPath = source.UserAlipayAccountImgPath;
|
||||
this.UserWechatAccountImgPath = source.UserWechatAccountImgPath;
|
||||
this.UserIconImgPath = source.UserIconImgPath;
|
||||
this.UserAccountsExpireDatetime = source.UserAccountsExpireDatetime;
|
||||
this.UserIsValidIndc = source.UserIsValidIndc;
|
||||
this.IsForeceChangePasswordIndc = source.IsForeceChangePasswordIndc;
|
||||
this.Postcode = source.Postcode;
|
||||
this.Community = source.Community;
|
||||
this.UserGrade = source.UserGrade;
|
||||
this.UserType = source.UserType;
|
||||
this.UserWechatAccount = source.UserWechatAccount;
|
||||
this.UserAlipayAccount = source.UserAlipayAccount;
|
||||
this.UserEngName = source.UserEngName;
|
||||
this.CompanyName = source.CompanyName;
|
||||
this.Industry = source.Industry;
|
||||
this.UserPassword = source.UserPassword;
|
||||
this.UserMobile = source.UserMobile;
|
||||
this.UserAddress = source.UserAddress;
|
||||
|
||||
if (includeSystemFields)
|
||||
{
|
||||
|
||||
@@ -24,5 +24,16 @@ namespace CloudBuilder.Security.Service
|
||||
void Logout();
|
||||
|
||||
string GetRsaPublicKey();
|
||||
|
||||
BiometricLoginChallengeDto BeginBiometricLogin(string username);
|
||||
|
||||
LoginProfile FinishBiometricLogin(FinishBiometricLoginData dto);
|
||||
|
||||
bool ConfirmPassword(string password);
|
||||
|
||||
string BeginBiometricRegistration(string deviceName);
|
||||
BiometricRegistrationResultDto FinishBiometricRegistration(FinishBiometricRegistrationData dto);
|
||||
|
||||
void RemoveBiometric();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using CloudBuilder.Core.Service;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CloudBuilder.Security.Data;
|
||||
|
||||
namespace CloudBuilder.Security.Service
|
||||
{
|
||||
public interface IWebAuthnService : IService
|
||||
{
|
||||
CredentialCreateOptions BuildCreationOptions(string username, byte[] userId, IReadOnlyList<byte[]> existingCredentialIds);
|
||||
|
||||
Task<StoredBiometricCredential> FinishRegistrationAsync(
|
||||
AuthenticatorAttestationRawResponse attestationResponse,
|
||||
CredentialCreateOptions originalOptions,
|
||||
Func<byte[], Task<bool>> isCredentialIdUnique);
|
||||
|
||||
AssertionOptions BuildRequestOptions(byte[] storedCredentialId);
|
||||
|
||||
string BuildRequestOptionsJson(byte[] storedCredentialId);
|
||||
|
||||
Task<uint> FinishAssertionAsync(
|
||||
AuthenticatorAssertionRawResponse assertionResponse,
|
||||
AssertionOptions originalOptions,
|
||||
byte[] storedPublicKey,
|
||||
uint storedCounter);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Web.Entry", "C
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Security.Service", "CloudBuilder.Security.Service\CloudBuilder.Security.Service.csproj", "{D4371FF5-55AA-F6FB-9D02-FA0486C7F492}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.AI", "CloudBuilder.AI\CloudBuilder.AI.csproj", "{8C849E66-BEC0-F09D-791F-FB353DA7DBE7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.AI.Service", "CloudBuilder.AI.Service\CloudBuilder.AI.Service.csproj", "{5767BF2F-70B3-4FF6-CB85-9C0F8F0C8612}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Core", "CloudBuilder.Core\CloudBuilder.Core.csproj", "{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Security", "CloudBuilder.Security\CloudBuilder.Security.csproj", "{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -27,18 +25,14 @@ Global
|
||||
{D4371FF5-55AA-F6FB-9D02-FA0486C7F492}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4371FF5-55AA-F6FB-9D02-FA0486C7F492}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4371FF5-55AA-F6FB-9D02-FA0486C7F492}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C849E66-BEC0-F09D-791F-FB353DA7DBE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C849E66-BEC0-F09D-791F-FB353DA7DBE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C849E66-BEC0-F09D-791F-FB353DA7DBE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C849E66-BEC0-F09D-791F-FB353DA7DBE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5767BF2F-70B3-4FF6-CB85-9C0F8F0C8612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5767BF2F-70B3-4FF6-CB85-9C0F8F0C8612}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5767BF2F-70B3-4FF6-CB85-9C0F8F0C8612}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5767BF2F-70B3-4FF6-CB85-9C0F8F0C8612}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -25,16 +25,10 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
|
||||
<PackageReference Include="ClosedXML" Version="0.102.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.2" />
|
||||
|
||||
<PackageReference Include="Fido2NetLib" Version="1.0.0-alpha" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CloudBuilder.AI">
|
||||
<HintPath>..\CloudBuilder.AI\bin\Debug\net8.0\CloudBuilder.AI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CloudBuilder.AI.Service">
|
||||
<HintPath>..\CloudBuilder.AI.Service\bin\Debug\net8.0\CloudBuilder.AI.Service.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CloudBuilder.Core">
|
||||
<HintPath>..\CloudBuilder.Core\bin\Debug\net8.0\CloudBuilder.Core.dll</HintPath>
|
||||
</Reference>
|
||||
@@ -47,7 +41,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\Ai\" />
|
||||
<Folder Include="Controllers\Security\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,301 +0,0 @@
|
||||
|
||||
using CloudBuilder.Core.DatabaseAccessor;
|
||||
using CloudBuilder.Core.Policy;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Transactions;
|
||||
using CloudBuilder.Core.Service;
|
||||
using CloudBuilder.AI.Service;
|
||||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||
using CloudBuilder.AI.Data;
|
||||
using System;
|
||||
using CloudBuilder.AI.Entity;
|
||||
using CloudBuilder.AI.SearchCriteria;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Ai
|
||||
{
|
||||
[Authorize(AuthorizePolicy.PERMISSION)]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AiBookController : ControllerBase
|
||||
{
|
||||
private readonly IApplicationService service;
|
||||
private readonly IAiBookService aiBookService;
|
||||
|
||||
public AiBookController(IApplicationService service, IAiBookService aiBookService)
|
||||
{
|
||||
this.service = service;
|
||||
this.aiBookService= aiBookService;
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByKey(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.FindByKey(key);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByTitle(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.FindByTitle(title);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindBySearchCriteria(AiBookSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.FindBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Insert(AiBookData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.Insert(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Update(AiBookData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.Update(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Delete")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Delete(string key){
|
||||
try
|
||||
{
|
||||
aiBookService.Delete(key);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Delete")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Deletes(string[] keys){
|
||||
try
|
||||
{
|
||||
aiBookService.Deletes(keys);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class ProcessBookCompositionData
|
||||
{
|
||||
public string key { get; set; }
|
||||
public string encoding { get; set; }
|
||||
public string pattern { get; set; }
|
||||
|
||||
}
|
||||
[PermissionOperate(name: "AiBook", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic ProcessBook(ProcessBookCompositionData compositionData)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.ProcessBook(compositionData.key,compositionData.encoding,compositionData.pattern);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic GetMp3Stream(AiSentenceViewEntity aiSentence)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiBookService.GetMp3Stream(aiSentence);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic GenerateMp3(AiChapterEntity[] chapters){
|
||||
try
|
||||
{
|
||||
aiBookService.GenerateMp3(chapters);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic GenerateMp3ByBookId(string bookId){
|
||||
try
|
||||
{
|
||||
aiBookService.GenerateMp3ByBookId(bookId);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic PackPersonNameByBookId(string bookId){
|
||||
try
|
||||
{
|
||||
aiBookService.PackPersonNameByBookId(bookId);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic PackPersonName(AiChapterEntity[] chapters){
|
||||
try
|
||||
{
|
||||
aiBookService.PackPersonName(chapters);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
|
||||
using CloudBuilder.Core.DatabaseAccessor;
|
||||
using CloudBuilder.Core.Policy;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Transactions;
|
||||
using CloudBuilder.Core.Service;
|
||||
using CloudBuilder.AI.Service;
|
||||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||
using CloudBuilder.AI.Data;
|
||||
using System;
|
||||
using CloudBuilder.AI.Entity;
|
||||
using CloudBuilder.AI.SearchCriteria;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CloudBuilder.AI;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Ai
|
||||
{
|
||||
[Authorize(AuthorizePolicy.PERMISSION)]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AiChapterController : ControllerBase
|
||||
{
|
||||
private readonly IApplicationService service;
|
||||
private readonly IAiChapterService aiChapterService;
|
||||
|
||||
public AiChapterController(IApplicationService service, IAiChapterService aiChapterService)
|
||||
{
|
||||
this.service = service;
|
||||
this.aiChapterService= aiChapterService;
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByKey(AiChapterEntityKey key)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiChapterService.FindByKey(key);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindBySearchCriteria(AiChapterSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiChapterService.FindBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Insert(AiChapterEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiChapterService.Insert(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Update(AiChapterEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiChapterService.Update(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Delete")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Delete(AiChapterEntityKey key){
|
||||
try
|
||||
{
|
||||
aiChapterService.Delete(key);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
|
||||
using CloudBuilder.Core.DatabaseAccessor;
|
||||
using CloudBuilder.Core.Policy;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Transactions;
|
||||
using CloudBuilder.Core.Service;
|
||||
using CloudBuilder.AI.Service;
|
||||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||
using CloudBuilder.AI.Data;
|
||||
using System;
|
||||
using CloudBuilder.AI.Entity;
|
||||
using CloudBuilder.AI.SearchCriteria;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CloudBuilder.AI;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Ai
|
||||
{
|
||||
[Authorize(AuthorizePolicy.PERMISSION)]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AiSentenceController : ControllerBase
|
||||
{
|
||||
private readonly IApplicationService service;
|
||||
private readonly IAiSentenceService aiSentenceService;
|
||||
|
||||
public AiSentenceController(IApplicationService service, IAiSentenceService aiSentenceService)
|
||||
{
|
||||
this.service = service;
|
||||
this.aiSentenceService= aiSentenceService;
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByKey(Int64 key)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiSentenceService.FindByKey(key);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class FindByChapterIdCompositionData
|
||||
{
|
||||
public string bookId { get; set; }
|
||||
public Int64 chapterId { get; set; }
|
||||
|
||||
}
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByChapterId(FindByChapterIdCompositionData compositionData)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiSentenceService.FindByChapterId(compositionData.bookId,compositionData.chapterId);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindBySearchCriteria(AiSentenceSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiSentenceService.FindBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Insert(AiSentenceEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiSentenceService.Insert(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiParagraph", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Inserts(AiSentenceEntity[] datas){
|
||||
try
|
||||
{
|
||||
aiSentenceService.Inserts(datas);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Update(AiSentenceEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = aiSentenceService.Update(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "AiBook", operate: "Delete")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Delete(Int64 key){
|
||||
try
|
||||
{
|
||||
aiSentenceService.Delete(key);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
|
||||
using CloudBuilder.Core.DatabaseAccessor;
|
||||
using CloudBuilder.Core.Policy;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Transactions;
|
||||
using CloudBuilder.Core.Service;
|
||||
using CloudBuilder.AI.Service;
|
||||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||
using CloudBuilder.AI.Data;
|
||||
using System;
|
||||
using CloudBuilder.AI.Entity;
|
||||
using CloudBuilder.AI.SearchCriteria;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CloudBuilder.AI;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Ai
|
||||
{
|
||||
[Authorize(AuthorizePolicy.PERMISSION)]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class DmsFileController : ControllerBase
|
||||
{
|
||||
private readonly IApplicationService service;
|
||||
private readonly IDmsFileService dmsFileService;
|
||||
|
||||
public DmsFileController(IApplicationService service, IDmsFileService dmsFileService)
|
||||
{
|
||||
this.service = service;
|
||||
this.dmsFileService= dmsFileService;
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindByKey(Int32 key)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindByKey(key);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindBySearchCriteria(DmsFileSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindDatasBySearchCriteria(DmsFileSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindDatasBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Print")]
|
||||
[HttpPost]
|
||||
public dynamic PrintBySearchCriteria(DmsFileSearchCriteria criteria)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.PrintBySearchCriteria(criteria);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Insert(DmsFileEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.Insert(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Update(DmsFileEntity data)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.Update(data);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Delete")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic Delete(Int32 key){
|
||||
try
|
||||
{
|
||||
dmsFileService.Delete(key);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic GetFolderName(string folderLocalName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.GetFolderName(folderLocalName);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic CreateDmsFolder(DmsFileEntity dmsFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.CreateDmsFolder(dmsFile);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic UpdateDmsFolder(DmsFileEntity dmsFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.UpdateDmsFolder(dmsFile);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindDmsFolders()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindDmsFolders();
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindDmsFolderTreeData()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindDmsFolderTreeData();
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Update")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic MoveDmsFolder(DmsFileEntity dmsFile){
|
||||
try
|
||||
{
|
||||
dmsFileService.MoveDmsFolder(dmsFile);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Insert")]
|
||||
[HttpPost]
|
||||
[UnitOfWork]
|
||||
public dynamic UploadFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.UploadFile();
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic DownloadFile(Int32 fileId)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.DownloadFile(fileId);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic DownloadFilePreview(DmsFileEntity dmsFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.DownloadFilePreview(dmsFile);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic DownloadFileCombine(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.DownloadFileCombine(fileName);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindDmsFolderMapByFolderName(string folderName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindDmsFolderMapByFolderName(folderName);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "DmsFile", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FindFolderNameParentByFolderName(string folderName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = dmsFileService.FindFolderNameParentByFolderName(folderName);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
@@ -174,6 +175,127 @@ this.authService= authService;
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "SecurityUser", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic ConfirmPassword(string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = authService.ConfirmPassword(password);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "SecurityUser", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic BeginBiometricRegistration(string deviceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = authService.BeginBiometricRegistration(deviceName);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "SecurityUser", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic FinishBiometricRegistration(FinishBiometricRegistrationData dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = authService.FinishBiometricRegistration(dto);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost]
|
||||
public dynamic BeginBiometricLogin(string username)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = authService.BeginBiometricLogin(username);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost]
|
||||
public dynamic FinishBiometricLogin(FinishBiometricLoginData dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var result = authService.FinishBiometricLogin(dto);
|
||||
return result;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[PermissionOperate(name: "SecurityUser", operate: "Find")]
|
||||
[HttpPost]
|
||||
public dynamic RemoveBiometric()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
authService.RemoveBiometric();
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (ValidatedException vex)
|
||||
{
|
||||
return vex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceErrorException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
+1
-1
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
+1
-1
@@ -12,10 +12,10 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -12,10 +12,11 @@ using CloudBuilder.Security.Service;
|
||||
using CloudBuilder.Core.DependencyInjection.Request;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Core.Authorization;
|
||||
using System;
|
||||
using CloudBuilder.Security.Entity;
|
||||
using CloudBuilder.Security.SearchCriteria;
|
||||
using CloudBuilder.Security;
|
||||
using System;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CloudBuilder.Web.Entry.Controllers.Security
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using CloudBuilder.Core.DependencyInjection.Form;
|
||||
using CloudBuilder.Core.Extensions;
|
||||
using CloudBuilder.Core.Service;
|
||||
using CloudBuilder.Security.Data;
|
||||
using CloudBuilder.Security.Service;
|
||||
using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
@@ -14,7 +16,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
builder.Services.Initialize();
|
||||
|
||||
builder.Services.Configure<WebAuthnOptions>(builder.Configuration.GetSection("WebAuthn"));
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddHttpClient().AddControllers()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>C:\inetpub\wwwroot\webapi</_PublishTargetUrl>
|
||||
<History>True|2026-06-24T00:26:55.0324041Z||;True|2026-06-24T08:21:40.3334931+08:00||;True|2026-06-24T08:20:33.0781131+08:00||;True|2026-06-24T08:09:48.1948679+08:00||;True|2026-06-23T10:11:00.8522806+08:00||;</History>
|
||||
<History>True|2026-08-06T05:11:18.2517555Z||;True|2026-08-06T13:09:44.6703394+08:00||;True|2026-08-06T10:39:04.1814630+08:00||;True|2026-08-06T10:03:51.6290574+08:00||;True|2026-08-06T10:03:46.6433716+08:00||;True|2026-08-06T09:57:39.5500548+08:00||;True|2026-08-06T09:43:56.5264669+08:00||;False|2026-08-06T09:42:30.9515003+08:00||;True|2026-08-06T09:39:51.6165569+08:00||;True|2026-08-06T09:38:12.2610385+08:00||;True|2026-08-06T08:18:49.5758328+08:00||;True|2026-06-24T08:26:55.0324041+08:00||;True|2026-06-24T08:21:40.3334931+08:00||;True|2026-06-24T08:20:33.0781131+08:00||;True|2026-06-24T08:09:48.1948679+08:00||;True|2026-06-23T10:11:00.8522806+08:00||;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -102,5 +102,12 @@
|
||||
]
|
||||
|
||||
},
|
||||
"WebAuthn": {
|
||||
"//": "RpId is the registrable domain suffix of Origin's host. It is a domain (or IP), NOT a full URL — never include the port. Origin is the full URL (scheme + host + port + path) the browser is loading the page from. For IP-based origins the rpId is just the IP (e.g. '172.16.253.243'); the port goes in Origin only. Examples: dev → RpId 'localhost' / Origin 'https://localhost:8000'; UAT → RpId '172.16.253.243' / Origin 'https://172.16.253.243:8002'; prod → RpId 'www.youjiala.com' / Origin 'https://www.youjiala.com'.",
|
||||
"RpId": "uat.youjiala.com",
|
||||
"RpName": "Cloud Builder",
|
||||
"Origin": "https://uat.youjiala.com",
|
||||
"TimeoutMs": 60000
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in new issue
Block a user