add flower controller

This commit is contained in:
Tsanie Lily 2023-05-24 22:01:37 +08:00
parent 589940adc2
commit 1400fcdeb4
14 changed files with 1099 additions and 69 deletions

View File

@ -1,7 +1,6 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using System.Text;
@ -17,7 +16,7 @@ public abstract class BaseController : ControllerBase
/// <summary>
/// 自定义认证头的关键字
/// </summary>
protected const string AuthHeader = "X-Auth";
protected const string AuthHeader = "Authorization";
/// <summary>
/// 禁用用户
/// </summary>
@ -74,16 +73,15 @@ public abstract class BaseController : ControllerBase
}
/// <summary>
/// 检查当前会话权限
/// 检出当前会话
/// </summary>
/// <param name="level">需要大于等于该权限,默认为 0 - UserCommon</param>
/// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话对应的用户对象</returns>
protected (ActionResult? Result, UserItem? User) CheckPermission(int? level = 0)
/// <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 (BadRequest(), null);
return (Unauthorized(), null);
}
string hash = h.ToString();
var token = database.Tokens.Find(hash);
@ -92,26 +90,55 @@ public abstract class BaseController : ControllerBase
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}", hash, token.ExpireDate);
logger?.LogWarning("token \"{hash}\" has expired after {date}", token.Id, token.ExpireDate);
return (Unauthorized(), null);
}
var user = database.Users.Find(token.UserId);
if (user == null)
{
logger?.LogWarning("user not found with id {id}", token.UserId);
return (NotFound(), null);
}
if (user.Level < level)
else if (user.Level < level)
{
logger?.LogWarning("user \"{id}\" level ({level}) lower than required ({required})", user.UserId, user.Level, level);
return (Forbid(), user);
}
token.ActiveDateUnixTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
else
{
var now = DateTimeOffset.UtcNow;
token.ActiveDateUnixTime = now.ToUnixTimeMilliseconds();
var expires = now.AddSeconds(token.ExpireSeconds).ToUnixTimeMilliseconds();
if (expires > token.ExpireDateUnixTime)
{
token.ExpireDateUnixTime = expires;
}
database.Tokens.Update(token);
user.ActiveDateUnixTime = now.ToUnixTimeMilliseconds();
database.Users.Update(user);
}
return (null, user);
}

View File

@ -0,0 +1,96 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 事件相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("api/event")]
public class EventController : BaseController
{
/// <inheritdoc/>
public EventController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 获取用户相关所有符合条件的事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/event/query
/// Authorization: authorization id
///
/// 参数:
///
/// cid: int?
/// key: string?
/// from: long?
/// to: long?
/// cfrom: decimal?
/// cto: decimal?
///
/// </remarks>
/// <param name="eventId">事件类型 id</param>
/// <param name="key">查询关键字</param>
/// <param name="from">起始日期</param>
/// <param name="to">结束日期</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("query")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult<RecordItem[]> GetRecords(
[FromQuery(Name = "eid")] int? eventId,
[FromQuery] string? key,
[FromQuery] long? from,
[FromQuery] long? to)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
SaveDatabase();
var records = database.Records.Where(r => r.OwnerId == user.Id);
if (eventId != null)
{
records = records.Where(r => r.EventId == eventId);
}
if (key != null)
{
records = records.Where(r =>
r.ByUserName != null && r.ByUserName.ToLower().Contains(key.ToLower())
// TODO: notes
);
}
if (from != null)
{
records = records.Where(r => r.DateUnixTime >= from);
}
if (to != null)
{
records = records.Where(r => r.DateUnixTime <= to);
}
return Ok(records.ToArray());
}
}

View File

@ -0,0 +1,314 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 花草相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("api/flower")]
public class FlowerController : BaseController
{
/// <inheritdoc/>
public FlowerController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 获取用户名下所有符合条件的花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/flower/query
/// Authorization: authorization id
///
/// 参数:
///
/// cid: int?
/// key: string?
/// from: long?
/// to: long?
/// cfrom: decimal?
/// cto: decimal?
///
/// </remarks>
/// <param name="categoryId">类别 id</param>
/// <param name="key">查询关键字</param>
/// <param name="buyFrom">起始购买日期</param>
/// <param name="buyTo">结束购买日期</param>
/// <param name="costFrom">开销最小值</param>
/// <param name="costTo">开销最大值</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("query")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult<FlowerItem[]> GetFlowers(
[FromQuery(Name = "cid")] int? categoryId,
[FromQuery] string? key,
[FromQuery(Name = "from")] long? buyFrom,
[FromQuery(Name = "to")] long? buyTo,
[FromQuery(Name = "cfrom")] decimal? costFrom,
[FromQuery(Name = "cto")] decimal? costTo)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
SaveDatabase();
var flowers = database.Flowers.Where(f => f.OwnerId == user.Id);
if (categoryId != null)
{
flowers = flowers.Where(f => f.CategoryId == categoryId);
}
if (key != null)
{
flowers = flowers.Where(f =>
f.Name.ToLower().Contains(key.ToLower()) ||
f.Purchase != null &&
f.Purchase.ToLower().Contains(key.ToLower()));
}
if (buyFrom != null)
{
flowers = flowers.Where(f => f.DateBuyUnixTime >= buyFrom);
}
if (buyTo != null)
{
flowers = flowers.Where(f => f.DateBuyUnixTime <= buyTo);
}
if (costFrom != null)
{
flowers = flowers.Where(f => f.Cost != null && f.Cost >= costFrom);
}
if (costTo != null)
{
flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo);
}
return Ok(flowers.ToArray());
}
/// <summary>
/// 移除用户的花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/flower/remove
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
///
/// </remarks>
/// <param name="id">花草唯一 id</param>
/// <returns>会话有效则返回操作影响的数据库行数</returns>
/// <response code="200">返回操作影响的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("remove")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpDelete]
public ActionResult<int> RemoveFlower([FromQuery][Required] int id)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var count = database.Database.ExecuteSql($"DELETE FROM [flowers] WHERE \"uid\" = {user.Id} AND \"fid\" = {id}");
SaveDatabase();
return Ok(count);
}
/// <summary>
/// 批量移除用户的花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/flower/remove
/// Authorization: authorization id
/// [
/// 2, 4, 5, 11
/// ]
///
/// </remarks>
/// <param name="ids">要移除的花草唯一 id 的数组</param>
/// <returns>会话有效则返回操作影响的数据库行数</returns>
/// <response code="200">返回操作影响的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("removeany")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
public ActionResult<int> RemoveFlower([FromBody] int[] ids)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var idfilter = string.Join(", ", ids);
var count = database.Database.ExecuteSql($"DELETE FROM [flowers] WHERE \"uid\" = {user.Id} AND \"fid\" IN ({idfilter})");
SaveDatabase();
return Ok(count);
}
/// <summary>
/// 用户添加花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/flower/add
/// Authorization: authorization id
/// {
/// "categoryId": 0,
/// "name": "玛格丽特",
/// "dateBuy": 1684919954743,
/// "cost": 5.00,
/// "purchase": "花鸟市场"
/// }
///
/// </remarks>
/// <param name="flower">花草参数</param>
/// <returns>添加成功则返回已添加的花草对象</returns>
/// <response code="200">返回已添加的花草对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("add")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
public ActionResult<FlowerItem> AddFlower([FromBody] FlowerParameter flower)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var item = new FlowerItem
{
OwnerId = user.Id,
CategoryId = flower.CategoryId,
Name = flower.Name,
DateBuyUnixTime = flower.DateBuy,
Cost = flower.Cost,
Purchase = flower.Purchase
};
database.Flowers.Add(item);
SaveDatabase();
return Ok(item);
}
/// <summary>
/// 修改花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/flower/update
/// Authorization: authorization id
/// {
/// "id": 0,
/// "categoryId": 1,
/// "name": "姬小菊",
/// "dateBuy": 1684935276117,
/// "cost": 15.00,
/// "purchase": null
/// }
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的花草对象</returns>
/// <response code="200">返回已修改的花草对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到将修改的花草对象</response>
[Route("update")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPut]
public ActionResult<FlowerItem> Update([FromBody] FlowerUpdateParameter update)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var flower = database.Flowers.FirstOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id);
if (flower == null)
{
return NotFound(update.Id);
}
flower.CategoryId = update.CategoryId;
flower.Name = update.Name;
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,49 @@
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 花草参数
/// </summary>
public record FlowerParameter
{
/// <summary>
/// 类别 id
/// </summary>
[Required]
public int CategoryId { get; init; }
/// <summary>
/// 花草名称
/// </summary>
[Required]
public required string Name { get; init; }
/// <summary>
/// 购买时间
/// </summary>
[Required]
public long DateBuy { get; init; }
/// <summary>
/// 购买花费
/// </summary>
public decimal? Cost { get; init; }
/// <summary>
/// 购买渠道
/// </summary>
public string? Purchase { get; init; }
}
/// <summary>
/// 花草修改参数
/// </summary>
public record FlowerUpdateParameter : FlowerParameter
{
/// <summary>
/// 花草 id
/// </summary>
[Required]
public int Id { get; set; }
}

View File

@ -0,0 +1,139 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Text;
namespace Blahblah.FlowerStory.Server.Controller;
/// <inheritdoc/>
[Route("apidoc")]
public class SwaggerController : ControllerBase
{
private readonly SwaggerGenerator generator;
/// <inheritdoc/>
public SwaggerController(SwaggerGenerator generator)
{
this.generator = generator;
}
/// <inheritdoc/>
[Route("get/{version}")]
[HttpGet]
public ActionResult GetApi(string version)
{
var model = generator.GetSwagger(version);
var builder = new StringBuilder();
builder.Append($@"<!DOCTYPE html>
<html lang=""zh-cn"">
<head>
<meta charset=""UTF-8"">
<title>Flower Story - API </title>
<style type=""text/css"">
table,
table td,
table th {{
margin: 0;
padding: 0;
border: 1px solid #000;
border-collapse: collapse;
}}
table {{
table-layout: fixed;
word-break: break-all;
}}
tr {{
height: 20px;
font-size: 12px;
}}
.wrapper {{
width: 1000px;
margin: 0 auto;
}}
.operation {{
width: 100%;
}}
.bg {{
background-color: rgb(84, 127, 177);
}}
</style>
</head>
<body>
<div class=""wrapper"">
<h1>{model.Info.Title}</h1>
<h3> {model.Info.Version}</h3>
<p>{model.Info.Description}</p>");
foreach (var item in model.Paths)
{
if (item.Value.Operations != null)
{
foreach (var operation in item.Value.Operations)
{
if (string.IsNullOrEmpty(operation.Value.Summary))
{
continue;
}
builder.Append($@"
<h3>{operation.Value.Summary}</h3>
<table class=""operation"">
<tr class=""bg""><td colspan=""5""></td></tr>
<tr>
<td>URL</td>
<td colspan=""4"">{item.Key}</td>
</tr>
<tr>
<td></td>
<td colspan=""4"">{operation.Key}</td>
</tr>");
if (operation.Value.Parameters?.Count > 0)
{
builder.Append(@"
<tr class=""bg"">
<td></td>
<td></td>
<td></td>
<td colspan=""2""></td>
</tr>");
foreach (var param in operation.Value.Parameters)
{
builder.Append($@"
<tr>
<td>{param.Name}</td>
<td>{param.In}</td>
<td>{param.Required}</td>
<td colspan=""2"">{param.Description}</td>
</tr>");
}
}
if (operation.Value.Responses?.Count > 0)
{
builder.Append(@"
<tr class=""bg"">
<td></td>
<td colspan=""4""></td>
</tr>");
foreach (var response in operation.Value.Responses)
{
builder.Append($@"
<tr>
<td>{response.Key}</td>
<td colspan=""4"">{response.Value.Description}</td>
</tr>");
}
}
builder.Append(@"
</table>");
}
}
}
builder.Append(@"
</div>
</body>
</html>");
return Content(builder.ToString(), "text/html");
}
}

View File

@ -1,6 +1,7 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
namespace Blahblah.FlowerStory.Server.Controller
{
@ -8,15 +9,12 @@ namespace Blahblah.FlowerStory.Server.Controller
/// 用户会话相关服务
/// </summary>
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
[Route("users")]
[Route("api/user")]
public partial class UserController : BaseController
{
/// <summary>
/// 构造用户会话服务
/// </summary>
/// <param name="db">数据库对象</param>
/// <param name="logger">日志对象</param>
/// <inheritdoc/>
public UserController(FlowerDatabase db, ILogger<UserController> logger) : base(db, logger)
{
}
@ -25,9 +23,9 @@ namespace Blahblah.FlowerStory.Server.Controller
/// 用户登录
/// </summary>
/// <remarks>
/// 提交示例:
/// 请求示例:
///
/// POST /users/auth
/// POST /api/user/auth
/// {
/// "id": "blahblah",
/// "password": "pwd123"
@ -36,12 +34,11 @@ namespace Blahblah.FlowerStory.Server.Controller
/// </remarks>
/// <param name="login">登录参数</param>
/// <returns>成功登录则返回自定义认证头</returns>
/// <response code="200">返回自定义认证头</response>
/// <response code="204">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
[Route("auth")]
[Consumes("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
@ -66,12 +63,12 @@ namespace Blahblah.FlowerStory.Server.Controller
}
// record the session
// TODO: singleton token
// TODO: singleton token, mobile
var expires = 1200;
var now = DateTimeOffset.UtcNow;
var expires = 1200; // 20 minutes
var token = new TokenItem
{
Id = Guid.NewGuid().ToString("N"),
Id = Convert.ToBase64String(SHA256.HashData(Guid.NewGuid().ToByteArray())),
UserId = user.Id,
LogonDateUnixTime = now.ToUnixTimeMilliseconds(),
ActiveDateUnixTime = now.ToUnixTimeMilliseconds(),
@ -87,16 +84,51 @@ namespace Blahblah.FlowerStory.Server.Controller
SaveDatabase();
Response.Headers.Add(AuthHeader, token.Id);
return Ok();
return NoContent();
}
/// <summary>
/// 注销当前登录会话
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/logout
/// Authorization: authorization id
///
/// </remarks>
/// <returns>注销失败则返回错误内容</returns>
/// <response code="204">注销成功</response>
/// <response code="401">认证失败</response>
[Route("logout")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[HttpPost]
public ActionResult Logout()
{
var (result, token) = CheckToken();
if (result != null)
{
return result;
}
if (token == null)
{
return Unauthorized();
}
database.Tokens.Remove(token);
SaveDatabase();
return NoContent();
}
/// <summary>
/// 注册用户
/// </summary>
/// <remarks>
/// 提交示例:
/// 请求示例:
///
/// POST /users/register
/// POST /api/user/register
/// {
/// "id": "blahblah",
/// "password": "pwd123",
@ -111,7 +143,6 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <response code="200">返回已注册的用户对象</response>
/// <response code="500">用户重复或其他服务器错误</response>
[Route("register")]
[Consumes("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
@ -124,7 +155,7 @@ namespace Blahblah.FlowerStory.Server.Controller
if (u != null)
{
logger?.LogWarning("duplicate user \"{id}\"", user.Id);
return Problem("duplicateUser", "users/register", 500);
return Problem("duplicateUser", "user/register", 500);
}
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
@ -145,13 +176,53 @@ namespace Blahblah.FlowerStory.Server.Controller
return Ok(item);
}
/// <summary>
/// 查询当前会话关联的用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/user/profile
/// Authorization: authorization id
///
/// </remarks>
/// <returns>会话有效则返回关联的用户对象</returns>
/// <response code="200">返回关联的用户对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("profile")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult<UserItem> Profile()
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
// update last active time
SaveDatabase();
return Ok(user);
}
/// <summary>
/// 修改用户
/// </summary>
/// <remarks>
/// 提交示例:
/// 请求示例:
///
/// POST /users/update
/// PUT /api/user/update
/// Authorization: authorization id
/// {
/// "userName": "Blah blah",
/// "email": "blah@example.com",
@ -162,18 +233,15 @@ namespace Blahblah.FlowerStory.Server.Controller
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的用户对象</returns>
/// <response code="200">返回已修改的用户对象</response>
/// <response code="400">认证头未找到</response>
/// <response code="401">服务器未找到登录会话</response>
/// <response code="403">用户权限不足</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("update")]
[Consumes("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[HttpPut]
public ActionResult<UserItem> Update([FromBody] UpdateParameter update)
{
#if DEBUG
@ -189,8 +257,8 @@ namespace Blahblah.FlowerStory.Server.Controller
return NotFound();
}
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
user.ActiveDateUnixTime = now;
//var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
//user.ActiveDateUnixTime = now;
user.Name = update.UserName;
user.Email = update.Email;
user.Mobile = update.Mobile;
@ -202,10 +270,10 @@ namespace Blahblah.FlowerStory.Server.Controller
//#if DEBUG
/// <summary>
/// 获取所有用户
/// #DEBUG 获取所有用户
/// </summary>
/// <returns></returns>
[Route("query")]
[Route("debug_list")]
[HttpGet]
public ActionResult<UserItem[]> GetUsers()
{
@ -213,10 +281,10 @@ namespace Blahblah.FlowerStory.Server.Controller
}
/// <summary>
/// 获取所有 token
/// #DEBUG 获取所有 token
/// </summary>
/// <returns></returns>
[Route("tokens")]
[Route("debug_tokens")]
[HttpGet]
public ActionResult<TokenItem[]> GetTokens()
{

View File

@ -1,4 +1,6 @@
namespace Blahblah.FlowerStory.Server.Controller;
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
partial class UserController
{
@ -7,24 +9,57 @@ partial class UserController
/// <summary>
/// 登录参数
/// </summary>
/// <param name="Id">用户 id</param>
/// <param name="Password">密码</param>
public record LoginParamter(string Id, string Password);
public record LoginParamter
{
/// <summary>
/// 用户 id
/// </summary>
[Required]
public required string Id { get; init; }
/// <summary>
/// 密码
/// </summary>
[Required]
public required string Password { get; init; }
}
/// <summary>
/// 用户注册参数
/// </summary>
/// <param name="Id">用户 id</param>
/// <param name="Password">密码</param>
/// <param name="UserName">用户名</param>
/// <param name="Email">邮箱</param>
/// <param name="Mobile">联系电话</param>
public record UserParameter(string Id, string Password, string UserName, string? Email, string? Mobile) : UpdateParameter(UserName, Email, Mobile);
public record UserParameter : UpdateParameter
{
/// <summary>
/// 用户 id
/// </summary>
[Required]
public required string Id { get; init; }
/// <summary>
/// 密码
/// </summary>
[Required]
public required string Password { get; init; }
}
/// <summary>
/// 用户修改参数
/// </summary>
/// <param name="UserName">用户名</param>
/// <param name="Email">邮箱</param>
/// <param name="Mobile">联系电话</param>
public record UpdateParameter(string UserName, string? Email, string? Mobile);
public record UpdateParameter
{
/// <summary>
/// 用户名
/// </summary>
[Required]
public required string UserName { get; init; }
/// <summary>
/// 邮箱
/// </summary>
public string? Email { get; init; }
/// <summary>
/// 联系电话
/// </summary>
public string? Mobile { get; init; }
}

View File

@ -18,6 +18,13 @@ public class FlowerItem
[Required]
public int Id { get; set; }
/// <summary>
/// 所有人 uid
/// </summary>
[Column("uid")]
[Required]
public int OwnerId { get; set; }
/// <summary>
/// 类别 id
/// </summary>

View File

@ -18,6 +18,13 @@ public class RecordItem
[Required]
public int Id { get; set; }
/// <summary>
/// 关联人 uid
/// </summary>
[Column("uid")]
[Required]
public int OwnerId { get; set; }
/// <summary>
/// 事件类型
/// </summary>

View File

@ -0,0 +1,202 @@
// <auto-generated />
using System;
using Blahblah.FlowerStory.Server.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
[DbContext(typeof(FlowerDatabase))]
[Migration("20230524062207_AddOwner")]
partial class AddOwner
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER")
.HasColumnName("categoryid");
b.Property<decimal?>("Cost")
.HasColumnType("real")
.HasColumnName("cost");
b.Property<long>("DateBuyUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("datebuy")
.HasAnnotation("Relational:JsonPropertyName", "dateBuy");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
b.Property<string>("Purchase")
.HasColumnType("TEXT")
.HasColumnName("purchase");
b.HasKey("Id");
b.ToTable("flowers");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rid");
b.Property<int?>("ByUserId")
.HasColumnType("INTEGER")
.HasColumnName("byuid");
b.Property<string>("ByUserName")
.HasColumnType("TEXT")
.HasColumnName("byname");
b.Property<long>("DateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("date")
.HasAnnotation("Relational:JsonPropertyName", "date");
b.Property<int>("EventId")
.HasColumnType("INTEGER")
.HasColumnName("eid");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
b.HasKey("Id");
b.ToTable("records");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("tid");
b.Property<long>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate")
.HasAnnotation("Relational:JsonPropertyName", "activeDate");
b.Property<string>("ClientAgent")
.HasColumnType("TEXT")
.HasColumnName("clientagent");
b.Property<string>("ClientApp")
.HasColumnType("TEXT")
.HasColumnName("clientapp");
b.Property<string>("DeviceId")
.HasColumnType("TEXT")
.HasColumnName("deviceid");
b.Property<long>("ExpireDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("expiredate")
.HasAnnotation("Relational:JsonPropertyName", "expireDate");
b.Property<int>("ExpireSeconds")
.HasColumnType("INTEGER")
.HasColumnName("expiresecs");
b.Property<long>("LogonDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("logondate")
.HasAnnotation("Relational:JsonPropertyName", "logonDate");
b.Property<int>("UserId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("VerifyCode")
.HasColumnType("TEXT")
.HasColumnName("verifycode");
b.HasKey("Id");
b.ToTable("tokens");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<long?>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<int>("Level")
.HasColumnType("INTEGER")
.HasColumnName("level");
b.Property<string>("Mobile")
.HasColumnType("TEXT")
.HasColumnName("mobile");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<long>("RegisterDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("regdate")
.HasAnnotation("Relational:JsonPropertyName", "registerDate");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("id");
b.HasKey("Id");
b.ToTable("users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
/// <inheritdoc />
public partial class AddOwner : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "uid",
table: "records",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "uid",
table: "flowers",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "uid",
table: "records");
migrationBuilder.DropColumn(
name: "uid",
table: "flowers");
}
}
}

View File

@ -42,6 +42,10 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
@ -79,6 +83,10 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER")
.HasColumnName("eid");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
@ -96,7 +104,8 @@ namespace Blahblah.FlowerStory.Server.Migrations
b.Property<long>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate");
.HasColumnName("activedate")
.HasAnnotation("Relational:JsonPropertyName", "activeDate");
b.Property<string>("ClientAgent")
.HasColumnType("TEXT")
@ -112,7 +121,8 @@ namespace Blahblah.FlowerStory.Server.Migrations
b.Property<long>("ExpireDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("expiredate");
.HasColumnName("expiredate")
.HasAnnotation("Relational:JsonPropertyName", "expireDate");
b.Property<int>("ExpireSeconds")
.HasColumnType("INTEGER")
@ -120,7 +130,8 @@ namespace Blahblah.FlowerStory.Server.Migrations
b.Property<long>("LogonDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("logondate");
.HasColumnName("logondate")
.HasAnnotation("Relational:JsonPropertyName", "logonDate");
b.Property<int>("UserId")
.HasColumnType("INTEGER")

View File

@ -25,7 +25,26 @@ public class Program
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.OperationFilter<SwaggerHttpHeaderOperation>();
//options.OperationFilter<SwaggerHttpHeaderOperation>();
var scheme = new OpenApiSecurityScheme
{
Description = "ÊÚȨͷ¡£ ʾÀý£º \"RG//HkvcTZdBospBOT6OuoWfsc1GS+P/js9zFdflBr0=\"",
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Authorization"
},
Scheme = "oauth2",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
};
options.AddSecurityDefinition("Authorization", scheme);
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
[scheme] = Array.Empty<string>()
});
options.SwaggerDoc(Version, new OpenApiInfo
{
@ -38,6 +57,7 @@ public class Program
});
builder.Services.AddDbContext<FlowerDatabase>(options => options.UseSqlite("DataSource=flower.db;Cache=Shared"));
builder.Services.AddScoped<SwaggerGenerator>();
var app = builder.Build();
@ -63,13 +83,24 @@ public class SwaggerHttpHeaderOperation : IOperationFilter
{
/// <inheritdoc/>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var required = context.ApiDescription.RelativePath switch
{
"user/update" or
"user/profile" or
"user/logout" => true,
_ => false
};
if (required)
{
operation.Parameters.Add(new OpenApiParameter
{
Name = "X-Auth",
Name = "Authorization",
Description = "ÊÚȨ Token",
In = ParameterLocation.Header,
Required = false,
Required = true,
Schema = new OpenApiSchema { Type = "string" }
});
}
}
}

View File

@ -10,6 +10,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controller\SwaggerController.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />