V1.2 新构架

This commit is contained in:
Aigio Liu
2021-02-20 16:45:44 +08:00
parent c2d95a1833
commit 4d76decf73
343 changed files with 41333 additions and 4 deletions
@@ -0,0 +1,56 @@
using System.IO;
namespace System.Application
{
/// <summary>
/// 指定用于检索应用程序所使用的文件夹目录路径的枚举常数
/// </summary>
public enum AppFolder
{
/// <summary>
/// 图片
/// </summary>
Images,
/// <summary>
/// 数据库
/// </summary>
Database,
/// <summary>
/// 缓存、临时文件夹(注意清理!)
/// </summary>
Cache,
}
/// <summary>
/// Enum 扩展 <see cref="AppFolder"/>
/// </summary>
public static class AppFolderEnumExtensions
{
/// <summary>
/// 获取文件夹路径,返回的路径必定存在
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
public static string GetPath(this AppFolder folder)
{
var path1 = folder switch
{
AppFolder.Images or AppFolder.Cache => IOPath.CacheDirectory,
AppFolder.Database => IOPath.AppDataDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(folder), folder, null),
};
var path = Path.Combine(path1, folder.ToString());
IOPath.DirCreateByNotExists(path);
return path;
}
#if DEBUG
[Obsolete("use GetPath", true)]
public static string GetFolderPath(this AppFolder folder) => folder.GetPath();
#endif
}
}
@@ -0,0 +1,22 @@
using SQLite;
using System.Diagnostics;
namespace System.Application.Entities
{
[Table("D5428AED")]
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class User : IEntity<Guid>
{
string DebuggerDisplay => Id.ToString();
[Column("5E72F0AE")]
[PrimaryKey]
public Guid Id { get; set; }
[Column("A931B798")]
public byte[]? NickName { get; set; }
[Column("4C12F9EE")]
public Guid? Avatar { get; set; }
}
}
@@ -0,0 +1,28 @@
using System.Application.Services.CloudService;
namespace System.Application
{
public static class ImageUrlHelper
{
static string ApiBaseUrl
{
get
{
var client = DI.Get<CloudServiceClientBase>();
return client.ApiBaseUrl;
}
}
/// <summary>
/// 获取图片的APIURL(相对路径)
/// </summary>
const string GetImageApiUrl = "{1}/api/images/{0}";
/// <summary>
/// 根据图片Id获取APIURL(绝对路径)
/// </summary>
/// <param name="imageId"></param>
/// <returns></returns>
public static string GetImageApiUrlById(Guid imageId) => string.Format(GetImageApiUrl, imageId, ApiBaseUrl);
}
}
@@ -0,0 +1,10 @@
namespace System.Application
{
public static class KeyConstants
{
/// <summary>
/// 当前登录用户
/// </summary>
public const string KEY_CURRENT_LOGIN_USER = "KEY_CURRENT_LOGIN_USER";
}
}
@@ -0,0 +1,53 @@
using System.Security.Cryptography;
using MPIgnore = MessagePack.IgnoreMemberAttribute;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
[MPObj]
public sealed class AppSettings : IAppSettings, ICloudServiceSettings
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public Guid AppVersion { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? ApiBaseUrl { get; set; }
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public byte[]? AesSecret { get; set; }
Aes? aes;
[MPIgnore]
[N_JsonIgnore]
[S_JsonIgnore]
public Aes Aes
{
get
{
if (aes == null)
{
if (AesSecret == null) throw new ArgumentNullException(nameof(AesSecret));
aes = AESUtils.Create(AesSecret);
}
return aes;
}
}
[MPKey(3)]
[N_JsonProperty("3")]
[S_JsonProperty("3")]
public string? AppSecretVisualStudioAppCenter { get; set; }
}
}
@@ -0,0 +1,44 @@
using System.Application.Columns;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
/// <summary>
/// 当前登录用户模型,如需增加字段,还需要在 <see cref="Clone"/> 中赋值新添加字段
/// </summary>
[MPObj]
public sealed class CurrentUser : ILoginResponse, IExplicitHasValue, IPhoneNumber
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public Guid UserId { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? AuthToken { get; set; }
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public string? PhoneNumber { get; set; }
bool IExplicitHasValue.ExplicitHasValue()
{
if (string.IsNullOrWhiteSpace(AuthToken)) return false;
return true;
}
public CurrentUser? Clone() => this.HasValue() ?
new CurrentUser
{
UserId = UserId,
AuthToken = AuthToken,
PhoneNumber = PhoneNumber,
} : null;
}
}
@@ -0,0 +1,33 @@
namespace System.Application
{
/// <summary>
/// 通知渠道类型(一个渠道可包含一个或多个<see cref="NotificationType"/>)
/// <list type="bullet">
/// <item>
/// <term>iOS:</term>
/// </item>
/// <item>
/// <term>Android:将枚举通过 GetChannelId 转换为 <see cref="string"/> 用作 NotificationCompat.Builder 中的 channelId</term>
/// </item>
/// <item>
/// <term>UWP:</term>
/// </item>
/// <item>
/// <term>Win32:</term>
/// </item>
/// </list>
/// <para>添加新的枚举常量注意事项:</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetName(NotificationChannelType)"/>中添加渠道名称</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetDescription(NotificationChannelType)"/>中添加渠道描述</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetImportanceLevel(NotificationChannelType)"/>中添加渠道重要性级别</para>
/// </summary>
public enum NotificationChannelType
{
/// <summary>
/// 新版本
/// <para>例如:</para>
/// <para>新版本下载进度</para>
/// </summary>
NewVersion = 2,
}
}
@@ -0,0 +1,29 @@
namespace System.Application
{
/// <summary>
/// 通知类型
/// <list type="bullet">
/// <item>
/// <term>iOS:</term>
/// </item>
/// <item>
/// <term>Android:将枚举转换为 <see cref="int"/> 用作 Android.App.NotificationManager 中的 id</term>
/// </item>
/// <item>
/// <term>UWP:</term>
/// </item>
/// <item>
/// <term>Win32:</term>
/// </item>
/// </list>
/// <para>添加新的枚举常量注意事项:</para>
/// <para>(客户端)如果要显示在设备的通知栏上,则需要在<see cref="NotificationType_Channel_EnumExtensions.GetChannel(NotificationType)"/>中添加对应渠道</para>
/// </summary>
public enum NotificationType
{
/// <summary>
/// 新版本下载进度
/// </summary>
NewVersionDownloadProgress = 7,
}
}
@@ -0,0 +1,69 @@
using System.Application.Properties;
using System.Application.Services;
namespace System.Application
{
/// <summary>
/// Enum 扩展 <see cref="NotificationType"/> AND <see cref="NotificationChannelType"/>
/// </summary>
public static class NotificationType_Channel_EnumExtensions
{
/// <summary>
/// 获取所属的通知渠道
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static NotificationChannelType GetChannel(this NotificationType value) => value switch
{
NotificationType.NewVersionDownloadProgress => NotificationChannelType.NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// 获取渠道的用户可见名称
/// <para>建议的最大长度为40个字符,如果该值太长,可能会被截断</para>
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#setName%28java.lang.CharSequence%29 </para>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static string GetName(this NotificationChannelType value)
{
return value switch
{
NotificationChannelType.NewVersion => SR.NotificationChannelType_Name_NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
/// <summary>
/// 获取渠道的用户可见描述
/// <para>建议的最大长度为300个字符,如果该值太长,可能会被截断</para>
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#setDescription%28java.lang.String%29 </para>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static string GetDescription(this NotificationChannelType value)
{
return value switch
{
NotificationChannelType.NewVersion => SR.NotificationChannelType_Description_NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
/// <summary>
/// 获取渠道的重要性级别
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#getImportance%28%29 </para>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static NotificationImportanceLevel GetImportanceLevel(this NotificationChannelType value)
=> value switch
{
NotificationChannelType.NewVersion => NotificationImportanceLevel.Medium,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
}
@@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Application.Properties {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Application.Properties.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 Steam++ 的本地化字符串。
/// </summary>
internal static string AppName {
get {
return ResourceManager.GetString("AppName", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在下载:{0}% 的本地化字符串。
/// </summary>
internal static string Downloading_ {
get {
return ResourceManager.GetString("Downloading_", resourceCulture);
}
}
/// <summary>
/// 查找类似 新版本更新 的本地化字符串。
/// </summary>
internal static string NewVersionUpdate {
get {
return ResourceManager.GetString("NewVersionUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 当下载新版本时的下载进度条通知。 的本地化字符串。
/// </summary>
internal static string NotificationChannelType_Description_NewVersion {
get {
return ResourceManager.GetString("NotificationChannelType_Description_NewVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 新版本通知 的本地化字符串。
/// </summary>
internal static string NotificationChannelType_Name_NewVersion {
get {
return ResourceManager.GetString("NotificationChannelType_Name_NewVersion", resourceCulture);
}
}
}
}
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppName" xml:space="preserve">
<value>Steam++</value>
</data>
<data name="Downloading_" xml:space="preserve">
<value>正在下载:{0}%</value>
</data>
<data name="NewVersionUpdate" xml:space="preserve">
<value>新版本更新</value>
</data>
<data name="NotificationChannelType_Description_NewVersion" xml:space="preserve">
<value>当下载新版本时的下载进度条通知。</value>
</data>
<data name="NotificationChannelType_Name_NewVersion" xml:space="preserve">
<value>新版本通知</value>
</data>
</root>
@@ -0,0 +1,8 @@
using System.Application.Entities;
namespace System.Application.Repositories
{
public interface IUserRepository : IRepository<User, Guid>
{
}
}
@@ -0,0 +1,8 @@
using System.Application.Entities;
namespace System.Application.Repositories.Implementation
{
internal sealed class UserRepository : Repository<User, Guid>, IUserRepository
{
}
}
@@ -0,0 +1,20 @@
using System.Application.Properties;
namespace System.Application.Services
{
/// <inheritdoc cref="INotificationService{TNotificationType}"/>
public interface INotificationService : INotificationService<NotificationType>
{
/// <summary>
/// 下载新版本进度通知
/// </summary>
/// <returns></returns>
public Progress<float> NotifyNewVersionDownloadProgress()
{
return NotifyDownload(
SR.Downloading_,
NotificationType.NewVersionDownloadProgress,
SR.AppName + SR.NewVersionUpdate);
}
}
}
@@ -0,0 +1,28 @@
namespace System.Application.Services
{
/// <summary>
/// 安全服务
/// </summary>
public interface ISecurityService
{
/// <summary>
/// 加密
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
byte[]? E(string? value);
/// <inheritdoc cref="E(string?)"/>
byte[]? EB(byte[]? value);
/// <summary>
/// 解密
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
string? D(byte[]? value);
/// <inheritdoc cref="D(byte[]?)"/>
byte[]? DB(byte[]? value);
}
}
@@ -0,0 +1,64 @@
using System.Application.Models;
using System.Threading.Tasks;
namespace System.Application.Services
{
/// <summary>
/// 用户管理
/// </summary>
public interface IUserManager : IAuthHelper
{
/// <inheritdoc cref="GetCurrentUserAsync"/>
CurrentUser? GetCurrentUser();
/// <summary>
/// 获取当前登录用户
/// <para>如果[退出登录]则为 <see langword="null"/>,对于接收到的推送消息,要求在服务端时传入接收人用户Id,客户端根据Id读取用户信息,而不使用此值</para>
/// </summary>
/// <returns></returns>
ValueTask<CurrentUser?> GetCurrentUserAsync();
/// <summary>
/// 设置当前登录用户,当[退出登录]时可传入<see langword="null"/>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
Task SetCurrentUserAsync(CurrentUser? value);
///// <summary>
///// 获取当前登录用户资料
///// <para>如果[退出登录]则为 <see langword="null"/>,对于接收到的推送消息,要求在服务端时传入接收人用户Id,客户端根据Id读取用户信息,而不使用此值</para>
///// </summary>
///// <returns></returns>
//ValueTask<UserInfoDTO?> GetCurrentUserInfoAsync();
///// <summary>
///// 设置当前登录用户资料
///// </summary>
///// <param name="value"></param>
///// <param name="updateToDataBase">更新到数据库中</param>
///// <returns></returns>
//Task SetCurrentUserInfoAsync(UserInfoDTO value, bool updateToDataBase);
/// <summary>
/// 根据Id获取用户数据
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<UserDTO?> GetUserByIdAsync(Guid userId);
///// <summary>
///// 根据Id获取用户资料
///// </summary>
///// <param name="userId"></param>
///// <returns></returns>
//Task<UserInfoDTO?> GetUserInfoByIdAsync(Guid userId);
/// <summary>
/// 添加或更新用户数据到数据库中
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task InsertOrUpdateAsync(IUserDTO user);
}
}
@@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Application.Columns;
using System.Application.Models;
using System.Application.Services.CloudService;
using System.Threading.Tasks;
namespace System.Application.Services.Implementation
{
public abstract class CloudServiceClient : CloudServiceClientBase
{
public const string TAG = "ServerApiClient";
protected readonly IUserManager userManager;
protected readonly IToast toast;
public CloudServiceClient(
ILoggerFactory loggerFactory,
IUserManager userManager,
IToast toast,
IOptions<AppSettings> options,
IModelValidator validator) : base(loggerFactory.CreateLogger(TAG), userManager, options, validator)
{
this.userManager = userManager;
this.toast = toast;
}
public override async Task SaveAuthTokenAsync(string authToken)
{
var user = await userManager.GetCurrentUserAsync();
if (user != null)
{
user.AuthToken = authToken;
await userManager.SetCurrentUserAsync(user);
}
}
public override async Task OnLoginedAsync(IReadOnlyPhoneNumber? phoneNumber, ILoginResponse response)
{
var cUser = new CurrentUser
{
UserId = response.UserId,
AuthToken = response.AuthToken,
PhoneNumber = phoneNumber?.PhoneNumber ?? string.Empty,
};
await userManager.SetCurrentUserAsync(cUser);
}
public override void ShowResponseErrorMessage(string message)
{
toast.Show(message);
}
}
}
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Options;
using System.Application.Models;
using System.Security.Cryptography;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="ISecurityService"/>
public class SecurityService : ISecurityService
{
protected readonly AppSettings settings;
public SecurityService(IOptions<AppSettings> options)
{
settings = options.Value;
}
public virtual byte[]? E(string? value) => AESUtils.EncryptToByteArray_Nullable(settings.Aes, value);
public virtual byte[]? EB(byte[]? value) => AESUtils.Encrypt_Nullable(settings.Aes, value);
public virtual string? D(byte[]? value) => AESUtils.DecryptToString_Nullable(settings.Aes, value);
public virtual byte[]? DB(byte[]? value) => AESUtils.Decrypt_Nullable(settings.Aes, value);
}
}
@@ -0,0 +1,260 @@
using Microsoft.Extensions.Logging;
using System.Application.Entities;
using System.Application.Models;
using System.Application.Repositories;
using System.Diagnostics;
using System.Security;
using System.Threading.Tasks;
using static System.Application.KeyConstants;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="IUserManager"/>
public class UserManager : IUserManager
{
protected const string TAG = "UserManager";
protected readonly IStorage storage;
protected readonly ILogger logger;
protected readonly IUserRepository userRepository;
protected readonly ISecurityService security;
public UserManager(
ILoggerFactory loggerFactory,
IStorage storage,
IUserRepository userRepository,
ISecurityService security)
{
logger = loggerFactory.CreateLogger(TAG);
this.storage = storage;
this.userRepository = userRepository;
this.security = security;
}
protected bool isAnonymous;
protected CurrentUser? currentUser;
//protected UserInfoDTO? currentUserInfo;
protected CurrentUser? CurrentUser
{
set
{
currentUser = value;
isAnonymous = value == null;
}
}
[Conditional("DEBUG")]
void PrintCurrentUser(string name)
{
logger.LogInformation(
$"{name}: {currentUser?.ToStringHideMiddleFour()}");
}
protected async ValueTask<CurrentUser?> GetCurrentUserAsync(bool clone)
{
if (currentUser == null && !isAnonymous)
{
try
{
CurrentUser = await storage.GetAsync<CurrentUser>(KEY_CURRENT_LOGIN_USER);
}
catch (Exception e)
{
logger.LogError(e, nameof(GetCurrentUserAsync));
}
PrintCurrentUser(nameof(GetCurrentUserAsync));
}
return clone ? currentUser?.Clone() : currentUser;
}
public CurrentUser? GetCurrentUser()
{
var hasCurrentUser = currentUser != null;
#if DEBUG
var read_cache = Random2.Next(100) % 2 == 0;
hasCurrentUser = read_cache && hasCurrentUser;
#endif
CurrentUser? result;
if (hasCurrentUser)
{
result = currentUser?.Clone();
}
else
{
Func<ValueTask<CurrentUser?>> func = GetCurrentUserAsync;
result = func.RunSync();
}
#if DEBUG
logger.LogDebug("read_cache: {0}, PhoneNumber: {1}", read_cache, result?.ToStringHideMiddleFour());
#endif
return result;
}
public ValueTask<CurrentUser?> GetCurrentUserAsync()
{
return GetCurrentUserAsync(true);
}
public async Task SetCurrentUserAsync(CurrentUser? value)
{
await storage.SetAsync(KEY_CURRENT_LOGIN_USER, currentUser);
CurrentUser = value;
PrintCurrentUser("SetCurrentUser");
}
//public async ValueTask<UserInfoDTO?> GetCurrentUserInfoAsync()
//{
// if (currentUserInfo == null && !isAnonymous)
// {
// var cUser = await GetCurrentUserAsync();
// if (cUser != null)
// {
// currentUserInfo = await GetUserInfoByIdAsync(cUser.UserId);
// }
// }
// return currentUserInfo;
//}
//public async Task SetCurrentUserInfoAsync(UserInfoDTO value, bool updateToDataBase)
//{
// currentUserInfo = value;
// if (updateToDataBase)
// {
// await InsertOrUpdateAsync(value);
// }
//}
public async ValueTask<string?> GetAuthTokenAsync()
{
var value = await GetCurrentUserAsync(false);
return value?.AuthToken;
}
public async Task SignOutAsync()
{
PrintCurrentUser("SignOut");
//currentUserInfo = default;
await SetCurrentUserAsync(null);
}
ValueTask<User?> GetUserTableByIdAsync(Guid userId)
{
return userRepository.FindAsync(userId);
}
TUserDTO? GetUserByTable<TUserDTO>(User? user, Func<User, TUserDTO?> binding) where TUserDTO : IUserDTO
{
if (user == null) return default;
var value = binding.Invoke(user);
return value;
}
TUserDTO? BindingUser<TUserDTO>(User user) where TUserDTO : IUserDTO, new()
{
var value = new TUserDTO
{
Id = user.Id,
NickName = security.D(user.NickName),
Avatar = user.Avatar,
};
return value;
}
public async Task<UserDTO?> GetUserByIdAsync(Guid userId)
{
var user = await GetUserTableByIdAsync(userId);
var value = GetUserByTable(user, BindingUser<UserDTO>);
return value;
}
//bool VerifyUserInfo(User user, UserInfoDTO userInfo)
//{
// if (user.Id != userInfo.Id)
// {
// logger.LogError("VerifyUserInfo Fail(Id).");
// return false;
// }
// if (security.D(user.NickName) != userInfo.NickName)
// {
// logger.LogError("VerifyUserInfo Fail(NickName).");
// return false;
// }
// if (user.Avatar != userInfo.Avatar)
// {
// logger.LogError("VerifyUserInfo Fail(Avatar).");
// return false;
// }
// return true;
//}
//UserInfoDTO? BindingUserInfo(User user)
//{
// if (user.UserInfo != null)
// {
// try
// {
// var userInfoBytes = security.DB(user.UserInfo);
// if (userInfoBytes != null)
// {
// var value = Serializable.DMP<UserInfoDTO>(userInfoBytes);
// if (value != null)
// {
// var v = VerifyUserInfo(user, value);
// if (v)
// {
// return value;
// }
// }
// }
// }
// catch (Exception e)
// {
// logger.LogError(e, "BindingUserInfo Serializable & Verify Fail.");
// }
// }
// return BindingUser<UserInfoDTO>(user);
//}
//public async Task<UserInfoDTO?> GetUserInfoByIdAsync(Guid userId)
//{
// var user = await GetUserTableByIdAsync(userId);
// return GetUserByTable(user, BindingUserInfo);
//}
public async Task InsertOrUpdateAsync(IUserDTO user)
{
var userTable = new User
{
Id = user.Id,
NickName = security.E(user.NickName),
Avatar = user.Avatar,
};
//if (user is UserInfoDTO userInfo)
//{
// userTable.UserInfo = Serializable.SMP(userInfo);
// userTable.UserInfo = security.EB(userTable.UserInfo);
//}
//else
//{
// var userTable2 = await GetUserTableByIdAsync(user.Id);
// if (userTable2?.UserInfo != null)
// {
// userTable.UserInfo = userTable2.UserInfo;
// }
//}
(var rowCount, var result) = await userRepository.InsertOrUpdateAsync(userTable);
PrintInsertOrUpdateResult(user, rowCount, result);
}
[Conditional("DEBUG")]
void PrintInsertOrUpdateResult(IUserDTO user, int rowCount, DbRowExecResult result)
{
logger.LogInformation(
$"User.InsertOrUpdate " +
$"rowCount: {rowCount}, " +
$"result: {result}, " +
$"user: [{user.NickName}-{user.Id}]");
}
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>..\Unicorn.pfx</AssemblyOriginatorKeyFile>
<RootNamespace>System.Application</RootNamespace>
<DefineConstants>ORM_SQLITE_PCL;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Services.CloudService\System.Application.SteamTools.Services.CloudService.csproj" />
<ProjectReference Include="..\System.Application.SteamTools\System.Application.SteamTools.csproj" />
<ProjectReference Include="..\System.Common.ClientLib\System.Common.ClientLib.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>