Files
flower-story/Server/Controller/BaseController.cs
T
2023-08-04 09:05:13 +08:00

388 lines
12 KiB
C#

using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
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>
/// 临时 id
/// </summary>
protected const int TempId = -1;
private static string? uploadsDirectory;
/// <summary>
/// 文件上传路径
/// </summary>
protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads");
/// <summary>
/// 支持的图片文件签名
/// </summary>
protected static readonly List<byte[]> PhotoSignatures = new()
{
// jpeg
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
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 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 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;
}
/// <summary>
/// 写入文件到用户的花草目录中
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="file">文件对象</param>
/// <param name="token">取消令牌</param>
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);
}
/// <summary>
/// 删除花草下的文件
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="path">文件路径</param>
/// <returns>返回是否已删除</returns>
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;
}
/// <summary>
/// 移动临时照片到花草目录
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="path">文件路径</param>
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;
}
/// <summary>
/// 获取两个经纬度之间的距离
/// </summary>
/// <param name="lat1">纬度1</param>
/// <param name="lon1">经度1</param>
/// <param name="lat2">纬度2</param>
/// <param name="lon2">经度2</param>
/// <returns></returns>
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));
}
/// <inheritdoc/>
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;
}
}
/// <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; init; }
/// <summary>
/// 文件内容
/// </summary>
public required byte[] Content { get; init; }
/// <summary>
/// 照片宽度
/// </summary>
public required int Width { get; init; }
/// <summary>
/// 照片高度
/// </summary>
public required int Height { get; init; }
}