complete controllers

This commit is contained in:
2023-05-25 14:41:03 +08:00
parent 1400fcdeb4
commit 3b5bd291f9
13 changed files with 664 additions and 56 deletions

View File

@ -1,6 +1,8 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text;
@ -9,10 +11,23 @@ namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 基础服务抽象类
/// </summary>
public abstract class BaseController : ControllerBase
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>
@ -114,7 +129,7 @@ public abstract class BaseController : ControllerBase
logger?.LogWarning("token \"{hash}\" has expired after {date}", token.Id, token.ExpireDate);
return (Unauthorized(), null);
}
var user = database.Users.Find(token.UserId);
var user = QueryUserItem(token.UserId);
if (user == null)
{
logger?.LogWarning("user not found with id {id}", token.UserId);
@ -133,10 +148,8 @@ public abstract class BaseController : ControllerBase
{
token.ExpireDateUnixTime = expires;
}
database.Tokens.Update(token);
user.ActiveDateUnixTime = now.ToUnixTimeMilliseconds();
database.Users.Update(user);
}
return (null, user);
@ -159,4 +172,62 @@ public abstract class BaseController : ControllerBase
}
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;
name = $"{Path.GetFileNameWithoutExtension(name)}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.{Path.GetExtension(name)}";
return new FileResult
{
Filename = name,
Content = data
};
}
return null;
}
}
/// <summary>
/// 文件结果
/// </summary>
public record FileResult
{
/// <summary>
/// 文件名
/// </summary>
public string? Filename { get; init; }
/// <summary>
/// 文件内容
/// </summary>
[Required]
public required byte[] Content { get; init; }
}

View File

@ -0,0 +1,75 @@
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.EntityFrameworkCore;
namespace Blahblah.FlowerStory.Server.Controller;
partial class BaseController
{
/// <summary>
/// 根据 uid 获取用户对象
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <returns></returns>
protected UserItem? QueryUserItem(int uid)
{
return database.Users
.FromSql($"SELECT \"uid\",\"id\",\"\" AS \"password\",\"level\",\"regdate\",\"activedate\",\"name\",\"email\",\"mobile\",NULL AS \"avatar\" FROM [users] WHERE \"uid\" = {uid} LIMIT 1")
.SingleOrDefault();
}
/// <summary>
/// 根据 id 获取登录使用的用户对象
/// </summary>
/// <param name="id">用户 id</param>
/// <returns></returns>
protected UserItem? QueryUserItemForAuthentication(string id)
{
return database.Users
.FromSql($"SELECT \"uid\",\"id\",\"password\",0 AS \"level\",0 AS \"regdate\",\"activedate\",\"\" AS \"name\",NULL AS \"email\",NULL AS \"mobile\",NULL as \"avatar\" FROM [users] WHERE \"id\" = {id} LIMIT 1")
.SingleOrDefault();
}
/// <summary>
/// 获取用户头像数据
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <returns></returns>
protected byte[]? QueryUserAvatar(int uid)
{
return database.Database.SqlQuery<byte[]>($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"uid\" = {uid} LIMIT 1").SingleOrDefault();
}
/// <summary>
/// 移除用户头像
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <returns></returns>
protected int RemoveUserAvatar(int uid)
{
return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}");
}
/// <summary>
/// 移除用户名下的花草
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <param name="fid">花草唯一 id</param>
/// <returns></returns>
protected int RemoveUserFlower(int uid, int fid)
{
return database.Database.ExecuteSql($"DELETE FROM \"flowers\" WHERE \"uid\" = {uid} AND \"fid\" = {fid}");
}
/// <summary>
/// 批量移除用户名下的花草
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <param name="fids">花草唯一 id 的数组</param>
/// <returns></returns>
protected int RemoveUserFlowers(int uid, int[] fids)
{
var idfilter = string.Join(", ", fids);
return database.Database.ExecuteSql($"DELETE FROM \"flowers\" WHERE \"uid\" = {uid} AND \"fid\" IN ({idfilter})");
}
}

View File

@ -8,7 +8,6 @@ namespace Blahblah.FlowerStory.Server.Controller;
/// 事件相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("api/event")]
public class EventController : BaseController
@ -46,7 +45,7 @@ public class EventController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("query")]
[Route("query", Name = "queryEvents")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]

View File

@ -10,7 +10,6 @@ namespace Blahblah.FlowerStory.Server.Controller;
/// 花草相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("api/flower")]
public class FlowerController : BaseController
@ -50,7 +49,7 @@ public class FlowerController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("query")]
[Route("query", Name = "queryFlowers")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -128,7 +127,7 @@ public class FlowerController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("remove")]
[Route("remove", Name = "removeFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -146,7 +145,7 @@ public class FlowerController : BaseController
return NotFound();
}
var count = database.Database.ExecuteSql($"DELETE FROM [flowers] WHERE \"uid\" = {user.Id} AND \"fid\" = {id}");
var count = RemoveUserFlower(user.Id, id);
SaveDatabase();
@ -172,12 +171,13 @@ public class FlowerController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("removeany")]
[Route("removeany", Name = "removeFlowers")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> RemoveFlower([FromBody] int[] ids)
{
var (result, user) = CheckPermission();
@ -190,8 +190,7 @@ public class FlowerController : BaseController
return NotFound();
}
var idfilter = string.Join(", ", ids);
var count = database.Database.ExecuteSql($"DELETE FROM [flowers] WHERE \"uid\" = {user.Id} AND \"fid\" IN ({idfilter})");
var count = RemoveUserFlowers(user.Id, ids);
SaveDatabase();
@ -221,12 +220,13 @@ public class FlowerController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("add")]
[Route("add", Name = "addFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<FlowerItem> AddFlower([FromBody] FlowerParameter flower)
{
var (result, user) = CheckPermission();
@ -278,12 +278,13 @@ public class FlowerController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到将修改的花草对象</response>
[Route("update")]
[Route("update", Name = "updateFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPut]
[Consumes("application/json")]
public ActionResult<FlowerItem> Update([FromBody] FlowerUpdateParameter update)
{
var (result, user) = CheckPermission();
@ -296,7 +297,7 @@ public class FlowerController : BaseController
return NotFound();
}
var flower = database.Flowers.FirstOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id);
var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id);
if (flower == null)
{
return NotFound(update.Id);
@ -306,7 +307,6 @@ public class FlowerController : BaseController
flower.DateBuyUnixTime = update.DateBuy;
flower.Cost = update.Cost;
flower.Purchase = update.Purchase;
database.Flowers.Update(flower);
SaveDatabase();
return Ok(user);

View File

@ -0,0 +1,55 @@
using Blahblah.FlowerStory.Server.Data;
using Microsoft.AspNetCore.Mvc;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 图片相关服务
/// </summary>
[Produces("image/png")]
[Route("photo")]
public class ImageController : BaseController
{
/// <inheritdoc/>
public ImageController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 请求用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /photo/avatar
/// Authorization: authorization id
///
/// </remarks>
/// <returns>认证通过则显示用户头像</returns>
/// <response code="200">返回头像</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到头像</response>
[Route("avatar", Name = "getAvatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult GetUserAvatar()
{
var (result, token) = CheckToken();
if (result != null)
{
return result;
}
if (token == null)
{
return Unauthorized();
}
var avatar = QueryUserAvatar(token.UserId);
if (avatar == null)
{
return NotFound();
}
return File(avatar, "image/png");
}
}

View File

@ -9,7 +9,6 @@ namespace Blahblah.FlowerStory.Server.Controller
/// 用户会话相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("api/user")]
public partial class UserController : BaseController
@ -37,17 +36,19 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <response code="204">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
[Route("auth")]
[Route("auth", Name = "authenticate")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[Consumes("application/json")]
public ActionResult Authenticate([FromBody] LoginParamter login)
{
#if DEBUG
logger?.LogInformation("user \"{user}\" try to login with password \"{password}\"", login.Id, login.Password);
#endif
var user = database.Users.FirstOrDefault(u => u.UserId == login.Id);
//var user = database.Users.SingleOrDefault(u => u.UserId == login.Id);
var user = QueryUserItemForAuthentication(login.Id);
if (user == null)
{
logger?.LogWarning("user \"{user}\" not found", login.Id);
@ -63,8 +64,19 @@ namespace Blahblah.FlowerStory.Server.Controller
}
// record the session
// TODO: singleton token, mobile
var expires = 1200;
// TODO: singleton token
string clientApp;
int expires;
if (Request.Headers.TryGetValue("X-ClientAgent", out var clientAgent))
{
clientApp = "app";
expires = 30 * 24 * 60 * 60; // 30 days
}
else
{
clientApp = "browser";
expires = 20 * 60; // 20 mins
}
var now = DateTimeOffset.UtcNow;
var token = new TokenItem
{
@ -74,13 +86,13 @@ namespace Blahblah.FlowerStory.Server.Controller
ActiveDateUnixTime = now.ToUnixTimeMilliseconds(),
ExpireDateUnixTime = now.AddSeconds(expires).ToUnixTimeMilliseconds(),
ExpireSeconds = expires,
ClientApp = "browser", // TODO: support app later
ClientAgent = Request.Headers.UserAgent
ClientApp = clientApp,
ClientAgent = clientAgent.Count > 0 ? clientAgent : Request.Headers.UserAgent,
DeviceId = Request.Headers.TryGetValue("X-DeviceId", out var deviceId) ? deviceId.ToString() : null
};
database.Tokens.Add(token);
user.ActiveDateUnixTime = token.ActiveDateUnixTime;
database.Users.Update(user);
SaveDatabase();
Response.Headers.Add(AuthHeader, token.Id);
@ -100,7 +112,7 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <returns>注销失败则返回错误内容</returns>
/// <response code="204">注销成功</response>
/// <response code="401">认证失败</response>
[Route("logout")]
[Route("logout", Name = "logout")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[HttpPost]
@ -142,17 +154,17 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <returns>成功注册则返回已注册的用户对象</returns>
/// <response code="200">返回已注册的用户对象</response>
/// <response code="500">用户重复或其他服务器错误</response>
[Route("register")]
[Route("register", Name = "register")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<UserItem> Register([FromBody] UserParameter user)
{
#if DEBUG
logger?.LogInformation("user register, {user}", user);
#endif
var u = database.Users.FirstOrDefault(u => u.UserId == user.Id);
if (u != null)
if (database.Users.Any(u => u.UserId == user.Id))
{
logger?.LogWarning("duplicate user \"{id}\"", user.Id);
return Problem("duplicateUser", "user/register", 500);
@ -191,7 +203,7 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("profile")]
[Route("profile", Name = "getProfile")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -236,12 +248,15 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("update")]
/// <response code="413">提交正文过大</response>
[Route("update", Name = "updateProfile")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[HttpPut]
[Consumes("application/json")]
public ActionResult<UserItem> Update([FromBody] UpdateParameter update)
{
#if DEBUG
@ -257,23 +272,120 @@ namespace Blahblah.FlowerStory.Server.Controller
return NotFound();
}
//var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
//user.ActiveDateUnixTime = now;
user.Name = update.UserName;
user.Email = update.Email;
user.Mobile = update.Mobile;
database.Users.Update(user);
SaveDatabase();
return Ok(user);
}
//#if DEBUG
/// <summary>
/// 修改用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/user/update_avatar
/// Authorization: authorization id
///
/// 参数:
///
/// avatar: IFormFile?
///
/// </remarks>
/// <param name="avatar">用户头像</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">头像内容格式非法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[Route("update_avatar", Name = "updateAvatar")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[HttpPut]
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
public ActionResult<UserItem> UploadAvatar(IFormFile? avatar)
{
#if DEBUG
logger?.LogInformation("user update avatar, {avatar}", avatar);
#endif
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
if (avatar?.Length > 0)
{
var file = WrapFormFile(avatar);
if (file == null)
{
return BadRequest();
}
user.Avatar = file.Content;
}
SaveDatabase();
return Ok(user);
}
/// <summary>
/// 移除用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/user/remove_avatar
/// Authorization: authorization id
///
/// </remarks>
/// <returns>移除成功则返回修改的数据库行数</returns>
/// <response code="200">修改的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("remove_avatar", Name = "removeAvatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpDelete]
public ActionResult RemoveAvatar()
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var count = RemoveUserAvatar(user.Id);
SaveDatabase();
return Ok(count);
}
//#if DEBUG
/// <summary>
/// #DEBUG 获取所有用户
/// </summary>
/// <returns></returns>
[Route("debug_list")]
[Route("debug_list", Name = "debug_list")]
[HttpGet]
public ActionResult<UserItem[]> GetUsers()
{
@ -284,12 +396,12 @@ namespace Blahblah.FlowerStory.Server.Controller
/// #DEBUG 获取所有 token
/// </summary>
/// <returns></returns>
[Route("debug_tokens")]
[Route("debug_tokens", Name = "debug_tokens")]
[HttpGet]
public ActionResult<TokenItem[]> GetTokens()
{
return Ok(database.Tokens.ToArray());
}
//#endif
//#endif
}
}