388 lines
12 KiB
C#
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; }
|
|
}
|