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; /// /// 基础服务抽象类 /// public abstract partial class BaseController : ControllerBase { private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden."; /// /// 临时 id /// protected const int TempId = -1; private static string? uploadsDirectory; /// /// 文件上传路径 /// protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads"); /// /// 支持的图片文件签名 /// protected static readonly List 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 } }; /// /// 自定义认证头的关键字 /// protected const string AuthHeader = "Authorization"; /// /// 禁用用户 /// protected const int UserDisabled = -1; /// /// 普通用户 /// protected const int UserCommon = 0; /// /// 管理员用户 /// protected const int UserAdmin = 99; /// /// 封面事件 /// protected const int EventCover = 0; /// /// 数据库对象 /// protected readonly FlowerDatabase database; /// /// 日志对象 /// protected readonly ILogger? logger; /// /// 构造基础服务类 /// /// 数据库对象 /// 日志对象 protected BaseController(FlowerDatabase database, ILogger? logger = null) { this.database = database; this.logger = logger; } /// /// 计算密码的 hash /// /// 密码原文 /// 用户 id /// 密码 hash,值为 SHA256(password+id+salt) /// 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); } /// /// 检出当前会话 /// /// 若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话令牌对象 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); } /// /// 检查当前会话权限 /// /// 需要大于等于该权限,默认为 0 - UserCommon /// 若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话对应的用户对象 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); } /// /// 保存数据库变动并输出日志 /// /// 操作的数据库行数 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; } /// /// 读取文件到 byte 数组 /// /// 来自请求的文件 /// 文件结果对象 protected static FileResult? WrapFormFile(IFormFile file) { if (file?.Length > 0) { 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); #if __OLD_READER__ // reading const int size = 16384; var buffer = new byte[size]; while ((count = stream.Read(buffer, 0, size)) > 0) { ms.Write(buffer, 0, count); } #else stream.CopyTo(ms); #endif var data = ms.ToArray(); var name = file.FileName; var ext = Path.GetExtension(name); var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}"; var image = SkiaSharp.SKImage.FromEncodedData(data); return new FileResult { Filename = name, FileType = ext, Path = path, Content = data, Width = image.Width, Height = image.Height }; } } return null; } private static string GetFlowerDirectory(int fid, bool create = false) { var directory = Path.Combine(UploadsDirectory, fid.ToString()); if (create && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } return directory; } /// /// 写入文件到用户的花草目录中 /// /// 花草唯一 id /// 文件对象 /// 取消令牌 protected static async Task WriteToFile(int fid, FileResult file, CancellationToken token = default) { var directory = GetFlowerDirectory(fid, true); var path = Path.Combine(directory, file.Path); await System.IO.File.WriteAllBytesAsync(path, file.Content, token); } /// /// 删除花草下的文件 /// /// 花草唯一 id /// 文件路径 /// 返回是否已删除 protected static bool DeleteFile(int fid, string path) { var directory = GetFlowerDirectory(fid); if (Directory.Exists(directory)) { path = Path.Combine(directory, path); if (System.IO.File.Exists(path)) { System.IO.File.Delete(path); return true; } } return false; } /// /// 移动临时照片到花草目录 /// /// 花草唯一 id /// 文件路径 protected static void MoveTempFileToFlower(int fid, string path) { var directory = GetFlowerDirectory(TempId); if (Directory.Exists(directory)) { var file = Path.Combine(directory, path); if (System.IO.File.Exists(file)) { directory = GetFlowerDirectory(fid, true); var to = Path.Combine(directory, path); if (System.IO.File.Exists(to)) { System.IO.File.Move(to, $"{to}.bak"); } System.IO.File.Move(file, to); } } } private const double EarthRadius = 6378137; private static double Radius(double degree) { return degree * Math.PI / 180.0; } /// /// 获取两个经纬度之间的距离 /// /// 纬度1 /// 经度1 /// 纬度2 /// 经度2 /// protected static double GetDistance(double lat1, double lon1, double lat2, double lon2) { double rlat1 = Radius(lat1); double rlat2 = Radius(lat2); return EarthRadius * Math.Acos( Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Cos(Radius(lon1) - Radius(lon2)) + Math.Sin(rlat1) * Math.Sin(rlat2)); } /// protected static double GetDistance2(double lat1, double lon1, double lat2, double lon2) { double rlat1 = Radius(lat1); double rlat2 = Radius(lat2); double a = rlat1 - rlat2; double b = Radius(lon1) - Radius(lon2); double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) + Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Pow(Math.Sin(b / 2), 2))); return s * EarthRadius; } } /// /// 文件结果 /// public record FileResult { /// /// 文件名 /// public required string Filename { get; init; } /// /// 文件类型 /// public required string FileType { get; init; } /// /// 储存路径 /// public required string Path { get; init; } /// /// 文件内容 /// public required byte[] Content { get; init; } /// /// 照片宽度 /// public required int Width { get; init; } /// /// 照片高度 /// public required int Height { get; init; } }