add users documents

This commit is contained in:
2023-05-23 22:08:56 +08:00
parent c8bf017a70
commit 589940adc2
16 changed files with 987 additions and 72 deletions

View File

@ -0,0 +1,135 @@
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;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 基础服务抽象类
/// </summary>
public abstract class BaseController : ControllerBase
{
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
/// <summary>
/// 自定义认证头的关键字
/// </summary>
protected const string AuthHeader = "X-Auth";
/// <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>
/// <param name="level">需要大于等于该权限,默认为 0 - UserCommon</param>
/// <returns>若检查失败,第一个参数返回异常 ActionResult。第二个参数返回会话对应的用户对象</returns>
protected (ActionResult? Result, UserItem? User) CheckPermission(int? level = 0)
{
if (!Request.Headers.TryGetValue(AuthHeader, out var h))
{
logger?.LogWarning("request with no {auth} header", AuthHeader);
return (BadRequest(), null);
}
string hash = h.ToString();
var token = database.Tokens.Find(hash);
if (token == null)
{
logger?.LogWarning("token \"{hash}\" not found", hash);
return (Unauthorized(), null);
}
if (token.ExpireDate < DateTimeOffset.UtcNow)
{
logger?.LogWarning("token \"{hash}\" has expired after {date}", hash, 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)
{
logger?.LogWarning("user \"{id}\" level ({level}) lower than required ({required})", user.UserId, user.Level, level);
return (Forbid(), user);
}
token.ActiveDateUnixTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
database.Tokens.Update(token);
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;
}
}

View File

@ -4,57 +4,224 @@ using Microsoft.AspNetCore.Mvc;
namespace Blahblah.FlowerStory.Server.Controller
{
/// <summary>
/// 用户会话相关服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("users")]
public class UserController : ControllerBase
public partial class UserController : BaseController
{
private readonly FlowerDatabase database;
private readonly ILogger<UserController> logger;
public UserController(FlowerDatabase db, ILogger<UserController> logger)
/// <summary>
/// 构造用户会话服务
/// </summary>
/// <param name="db">数据库对象</param>
/// <param name="logger">日志对象</param>
public UserController(FlowerDatabase db, ILogger<UserController> logger) : base(db, logger)
{
database = db;
this.logger = logger;
}
/// <summary>
/// 用户登录
/// </summary>
/// <remarks>
/// 提交示例:
///
/// POST /users/auth
/// {
/// "id": "blahblah",
/// "password": "pwd123"
/// }
///
/// </remarks>
/// <param name="login">登录参数</param>
/// <returns>成功登录则返回自定义认证头</returns>
/// <response code="200">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
[Route("auth")]
[Consumes("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
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);
if (user == null)
{
logger?.LogWarning("user \"{user}\" not found", login.Id);
return NotFound();
}
// comput password hash with salt
string hash = HashPassword(login.Password, login.Id);
if (hash != user.Password)
{
logger?.LogWarning("hash result {hash}, unauthorized with hash {password}", hash, user.Password);
return Unauthorized();
}
// record the session
// TODO: singleton token
var now = DateTimeOffset.UtcNow;
var expires = 1200; // 20 minutes
var token = new TokenItem
{
Id = Guid.NewGuid().ToString("N"),
UserId = user.Id,
LogonDateUnixTime = now.ToUnixTimeMilliseconds(),
ActiveDateUnixTime = now.ToUnixTimeMilliseconds(),
ExpireDateUnixTime = now.AddSeconds(expires).ToUnixTimeMilliseconds(),
ExpireSeconds = expires,
ClientApp = "browser", // TODO: support app later
ClientAgent = Request.Headers.UserAgent
};
database.Tokens.Add(token);
user.ActiveDateUnixTime = token.ActiveDateUnixTime;
database.Users.Update(user);
SaveDatabase();
Response.Headers.Add(AuthHeader, token.Id);
return Ok();
}
/// <summary>
/// 注册用户
/// </summary>
/// <remarks>
/// 提交示例:
///
/// POST /users/register
/// {
/// "id": "blahblah",
/// "password": "pwd123",
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="user">注册参数</param>
/// <returns>成功注册则返回已注册的用户对象</returns>
/// <response code="200">返回已注册的用户对象</response>
/// <response code="500">用户重复或其他服务器错误</response>
[Route("register")]
[Consumes("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
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)
{
logger?.LogWarning("duplicate user \"{id}\"", user.Id);
return Problem("duplicateUser", "users/register", 500);
}
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var item = new UserItem
{
UserId = user.Id,
Password = HashPassword(user.Password, user.Id),
Level = UserCommon,
RegisterDateUnixTime = now,
ActiveDateUnixTime = now,
Name = user.UserName,
Email = user.Email,
Mobile = user.Mobile
};
database.Users.Add(item);
SaveDatabase();
return Ok(item);
}
/// <summary>
/// 修改用户
/// </summary>
/// <remarks>
/// 提交示例:
///
/// POST /users/update
/// {
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的用户对象</returns>
/// <response code="200">返回已修改的用户对象</response>
/// <response code="400">认证头未找到</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]
public ActionResult<UserItem> Update([FromBody] UpdateParameter update)
{
#if DEBUG
logger?.LogInformation("user update, {user}", update);
#endif
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
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>
/// <returns></returns>
[Route("query")]
[HttpGet]
public ActionResult<UserItem[]> GetUsers()
{
//var forecast = Enumerable.Range(1, 5).Select(index =>
// new WeatherForecast
// {
// Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
// TemperatureC = Random.Shared.Next(-20, 55),
// Summary = summaries[Random.Shared.Next(summaries.Length)]
// })
// .ToArray();
//return Ok(forecast);
return Ok(database.Users.ToArray());
}
[Route("update")]
[HttpPost]
public ActionResult<int> UpdateUser([FromBody] UserItem item)
/// <summary>
/// 获取所有 token
/// </summary>
/// <returns></returns>
[Route("tokens")]
[HttpGet]
public ActionResult<TokenItem[]> GetTokens()
{
if (item.Id > 0)
{
database.Update(item);
}
else
{
database.Add(item);
}
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 Ok(item.Id);
return Ok(database.Tokens.ToArray());
}
//#endif
}
}

View File

@ -0,0 +1,30 @@
namespace Blahblah.FlowerStory.Server.Controller;
partial class UserController
{
}
/// <summary>
/// 登录参数
/// </summary>
/// <param name="Id">用户 id</param>
/// <param name="Password">密码</param>
public record LoginParamter(string Id, string Password);
/// <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);
/// <summary>
/// 用户修改参数
/// </summary>
/// <param name="UserName">用户名</param>
/// <param name="Email">邮箱</param>
/// <param name="Mobile">联系电话</param>
public record UpdateParameter(string UserName, string? Email, string? Mobile);