add users documents
This commit is contained in:
135
Server/Controller/BaseController.cs
Normal file
135
Server/Controller/BaseController.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
30
Server/Controller/UserController.structs.cs
Normal file
30
Server/Controller/UserController.structs.cs
Normal 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);
|
Reference in New Issue
Block a user