using Blahblah.FlowerStory.Server.Data; using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; using System.Net; using System.Security.Cryptography; using System.Text; namespace Blahblah.FlowerStory.Server.Controller; /// <summary> /// 基础服务抽象类 /// </summary> public abstract partial class BaseController : ControllerBase { private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden."; /// <summary> /// 支持的图片文件签名 /// </summary> protected static readonly List<byte[]> PhotoSignatures = new() { // jpeg new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 }, // png new byte[] { 0x89, 0x50, 0x4E, 0x47 } }; /// <summary> /// 自定义认证头的关键字 /// </summary> protected const string AuthHeader = "Authorization"; /// <summary> /// 禁用用户 /// </summary> protected const int UserDisabled = -1; /// <summary> /// 普通用户 /// </summary> protected const int UserCommon = 0; /// <summary> /// 管理员用户 /// </summary> protected const int UserAdmin = 99; /// <summary> /// 封面事件 /// </summary> protected const int EventCover = 0; /// <summary> /// 数据库对象 /// </summary> protected readonly FlowerDatabase database; /// <summary> /// 日志对象 /// </summary> protected readonly ILogger<BaseController>? logger; /// <summary> /// 构造基础服务类 /// </summary> /// <param name="database">数据库对象</param> /// <param name="logger">日志对象</param> protected BaseController(FlowerDatabase database, ILogger<BaseController>? logger = null) { this.database = database; this.logger = logger; } /// <summary> /// 计算密码的 hash /// </summary> /// <param name="password">密码原文</param> /// <param name="id">用户 id</param> /// <returns>密码 hash,值为 SHA256(password+id+salt)</returns> /// <exception cref="ArgumentNullException"></exception> protected string HashPassword(string password, string id) { if (string.IsNullOrEmpty(password)) { throw new ArgumentNullException(nameof(password)); } if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } var data = Encoding.UTF8.GetBytes($"{password}{id}{Salt}"); var hash = SHA256.HashData(data); return Convert.ToHexString(hash); } /// <summary> /// 检出当前会话 /// </summary> /// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话令牌对象</returns> protected (ActionResult? Result, TokenItem? Token) CheckToken() { if (!Request.Headers.TryGetValue(AuthHeader, out var h)) { logger?.LogWarning("request with no {auth} header", AuthHeader); return (Unauthorized(), null); } string hash = h.ToString(); var token = database.Tokens.Find(hash); if (token == null) { logger?.LogWarning("token \"{hash}\" not found", hash); return (Unauthorized(), null); } return (null, token); } /// <summary> /// 检查当前会话权限 /// </summary> /// <param name="level">需要大于等于该权限,默认为 0 - UserCommon</param> /// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话对应的用户对象</returns> protected (ActionResult? Result, UserItem? User) CheckPermission(int? level = 0) { var (result, token) = CheckToken(); if (result != null) { return (result, null); } if (token == null) { return (Unauthorized(), null); } if (token.ExpireDate < DateTimeOffset.UtcNow) { logger?.LogWarning("token \"{hash}\" has expired after {date}", token.Id, token.ExpireDate); return (Unauthorized(), null); } var user = QueryUserItem(token.UserId); //var user = database.Users.Where(u => u.Id == token.UserId).Select(u => new UserItem //{ // Id = u.Id, // UserId = u.UserId, // Level = u.Level, // RegisterDateUnixTime = u.RegisterDateUnixTime, // ActiveDateUnixTime = u.ActiveDateUnixTime, // Name = u.Name, // Email = u.Email, // Mobile = u.Mobile //}).SingleOrDefault(); if (user == null) { logger?.LogWarning("user not found with id {id}", token.UserId); } else if (user.Level < level) { logger?.LogWarning("user \"{id}\" level ({level}) lower than required ({required})", user.UserId, user.Level, level); return (Forbid(), user); } else { var now = DateTimeOffset.UtcNow; token.ActiveDateUnixTime = now.ToUnixTimeMilliseconds(); var expires = now.AddSeconds(token.ExpireSeconds).ToUnixTimeMilliseconds(); if (expires > token.ExpireDateUnixTime) { token.ExpireDateUnixTime = expires; } user.ActiveDateUnixTime = now.ToUnixTimeMilliseconds(); } return (null, user); } /// <summary> /// 保存数据库变动并输出日志 /// </summary> /// <returns>操作的数据库行数</returns> protected int SaveDatabase() { var count = database.SaveChanges(); if (count > 0) { logger?.LogInformation("{number} of entries written to database.", count); } else { logger?.LogWarning("no data written to database."); } return count; } /// <summary> /// 读取文件到 byte 数组 /// </summary> /// <param name="file">来自请求的文件</param> /// <returns>文件结果对象</returns> protected FileResult? WrapFormFile(IFormFile file) { if (file == null) { return null; } using var stream = file.OpenReadStream(); // check header var headers = new byte[PhotoSignatures.Max(s => s.Length)]; int count = stream.Read(headers, 0, headers.Length); if (PhotoSignatures.Any(s => headers.Take(s.Length).SequenceEqual(s))) { using var ms = new MemoryStream(); ms.Write(headers, 0, count); // reading const int size = 16384; var buffer = new byte[size]; while ((count = stream.Read(buffer, 0, size)) > 0) { ms.Write(buffer, 0, count); } var data = ms.ToArray(); var name = file.FileName; var ext = Path.GetExtension(name); var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}"; return new FileResult { Filename = name, FileType = ext, Path = path, Content = data }; } return null; } /// <summary> /// 写入文件到用户的花草目录中 /// </summary> /// <param name="uid">用户唯一 id</param> /// <param name="fid">花草唯一 id</param> /// <param name="file">文件对象</param> /// <param name="token">取消令牌</param> protected async Task WriteToFile(int uid, int fid, FileResult file, CancellationToken token = default) { var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString()); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var path = Path.Combine(directory, file.Path); await System.IO.File.WriteAllBytesAsync(path, file.Content, token); } /// <summary> /// 删除花草下的文件 /// </summary> /// <param name="uid">用户唯一 id</param> /// <param name="fid">花草唯一 id</param> /// <param name="path">文件路径</param> /// <returns>返回是否已删除</returns> protected bool DeleteFile(int uid, int fid, string path) { var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString()); if (Directory.Exists(directory)) { path = Path.Combine(directory, path); if (System.IO.File.Exists(path)) { System.IO.File.Delete(path); return true; } } return false; } } /// <summary> /// 文件结果 /// </summary> public record FileResult { /// <summary> /// 文件名 /// </summary> public required string Filename { get; init; } /// <summary> /// 文件类型 /// </summary> public required string FileType { get; init; } /// <summary> /// 储存路径 /// </summary> public required string Path { get; set; } /// <summary> /// 文件内容 /// </summary> [Required] public required byte[] Content { get; init; } } /// <summary> /// 照片参数 /// </summary> public record PhotoParameter { /// <summary> /// 花草 id /// </summary> [Required] public int Id { get; set; } /// <summary> /// 封面照片 /// </summary> [Required] public required IFormFile Photo { get; set; } }