complete controllers
This commit is contained in:
410
Server/Controller/UserApiController.cs
Normal file
410
Server/Controller/UserApiController.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using Blahblah.FlowerStory.Server.Data;
|
||||
using Blahblah.FlowerStory.Server.Data.Model;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Blahblah.FlowerStory.Server.Controller;
|
||||
|
||||
/// <summary>
|
||||
/// 用户会话相关 API 服务
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("api/user")]
|
||||
public partial class UserApiController : BaseController
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public UserApiController(FlowerDatabase db, ILogger<UserApiController> logger) : base(db, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 请求示例:
|
||||
///
|
||||
/// POST /api/user/auth
|
||||
/// {
|
||||
/// "id": "blahblah",
|
||||
/// "password": "pwd123"
|
||||
/// }
|
||||
///
|
||||
/// </remarks>
|
||||
/// <param name="login">登录参数</param>
|
||||
/// <returns>成功登录则返回自定义认证头</returns>
|
||||
/// <response code="204">返回自定义认证头</response>
|
||||
/// <response code="401">认证失败</response>
|
||||
/// <response code="404">未找到用户</response>
|
||||
[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.SingleOrDefault(u => u.UserId == login.Id);
|
||||
var user = QueryUserItemForAuthentication(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
|
||||
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
|
||||
{
|
||||
Id = Convert.ToBase64String(SHA256.HashData(Guid.NewGuid().ToByteArray())),
|
||||
UserId = user.Id,
|
||||
LogonDateUnixTime = now.ToUnixTimeMilliseconds(),
|
||||
ActiveDateUnixTime = now.ToUnixTimeMilliseconds(),
|
||||
ExpireDateUnixTime = now.AddSeconds(expires).ToUnixTimeMilliseconds(),
|
||||
ExpireSeconds = expires,
|
||||
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;
|
||||
SaveDatabase();
|
||||
|
||||
Response.Headers.Add(AuthHeader, token.Id);
|
||||
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", Name = "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 /api/user/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", 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
|
||||
if (database.Users.Any(u => u.UserId == user.Id))
|
||||
{
|
||||
logger?.LogWarning("duplicate user \"{id}\"", user.Id);
|
||||
return Problem("duplicateUser", "api/user/register");
|
||||
}
|
||||
|
||||
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>
|
||||
/// 请求示例:
|
||||
///
|
||||
/// 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", Name = "getProfile")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[HttpGet]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
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>
|
||||
/// 请求示例:
|
||||
///
|
||||
/// PUT /api/user/update
|
||||
/// Authorization: authorization id
|
||||
/// {
|
||||
/// "userName": "Blah blah",
|
||||
/// "email": "blah@example.com",
|
||||
/// "mobile": "18012345678"
|
||||
/// }
|
||||
///
|
||||
/// </remarks>
|
||||
/// <param name="update">修改参数</param>
|
||||
/// <returns>修改成功则返回已修改的用户对象</returns>
|
||||
/// <response code="200">返回已修改的用户对象</response>
|
||||
/// <response code="401">未找到登录会话或已过期</response>
|
||||
/// <response code="403">用户已禁用</response>
|
||||
/// <response code="404">未找到关联用户</response>
|
||||
/// <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
|
||||
logger?.LogInformation("user update, {user}", update);
|
||||
#endif
|
||||
var (result, user) = CheckPermission();
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
user.Name = update.UserName;
|
||||
user.Email = update.Email;
|
||||
user.Mobile = update.Mobile;
|
||||
SaveDatabase();
|
||||
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
/// <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([Required] 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 NoContent();
|
||||
}
|
||||
|
||||
/// <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", Name = "debug_list")]
|
||||
[HttpGet]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
public ActionResult<UserItem[]> GetUsers()
|
||||
{
|
||||
return Ok(database.Users.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #DEBUG 获取所有 token
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Route("debug_tokens", Name = "debug_tokens")]
|
||||
[HttpGet]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
public ActionResult<TokenItem[]> GetTokens()
|
||||
{
|
||||
return Ok(database.Tokens.ToArray());
|
||||
}
|
||||
//#endif
|
||||
}
|
Reference in New Issue
Block a user