aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2022-04-16 22:11:29 +0800
committercrupest <crupest@outlook.com>2022-04-16 22:11:29 +0800
commitb4f783c20aa47cb601dc81e0dad07aa92517c229 (patch)
tree5555a97984df994910c26b3d5f2fc897cfbdfd28 /BackEnd/Timeline
parent750785728f57af11dfc682ee9ee870e4dc191981 (diff)
downloadtimeline-b4f783c20aa47cb601dc81e0dad07aa92517c229.tar.gz
timeline-b4f783c20aa47cb601dc81e0dad07aa92517c229.tar.bz2
timeline-b4f783c20aa47cb601dc81e0dad07aa92517c229.zip
...
Diffstat (limited to 'BackEnd/Timeline')
-rw-r--r--BackEnd/Timeline/Controllers/TokenController.cs4
-rw-r--r--BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs132
-rw-r--r--BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs2
-rw-r--r--BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs25
-rw-r--r--BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs7
-rw-r--r--BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs4
-rw-r--r--BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs16
-rw-r--r--BackEnd/Timeline/Services/Token/IUserTokenService.cs5
-rw-r--r--BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs6
9 files changed, 186 insertions, 15 deletions
diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs
index 9ee5a09f..7fba0bc5 100644
--- a/BackEnd/Timeline/Controllers/TokenController.cs
+++ b/BackEnd/Timeline/Controllers/TokenController.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
@@ -77,7 +77,7 @@ namespace Timeline.Controllers
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult<HttpVerifyTokenResponse>> Verify([FromBody] HttpVerifyTokenRequest request)
+ public async Task<ActionResult<HttpVerifyTokenResponse>> Verify([FromBody] HttpVerifyOrRevokeTokenRequest request)
{
try
{
diff --git a/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs
new file mode 100644
index 00000000..b129758a
--- /dev/null
+++ b/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs
@@ -0,0 +1,132 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Services.Token;
+using Timeline.Services.User;
+
+namespace Timeline.Controllers.V2
+{
+ [ApiController]
+ [Route("v2/token")]
+ public class TokenV2Controller : V2ControllerBase
+ {
+ private readonly IUserService _userService;
+ private readonly IUserTokenService _userTokenService;
+ private readonly IClock _clock;
+
+ public TokenV2Controller(IUserService userService, IUserTokenService userTokenService, IClock clock)
+ {
+ _userService = userService;
+ _userTokenService = userTokenService;
+ _clock = clock;
+ }
+
+ private const string BadCredentialMessage = "Username or password is wrong.";
+
+ /// <summary>
+ /// Create a new token for a user.
+ /// </summary>
+ /// <returns>Result of token creation.</returns>
+ [HttpPost("create")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task<ActionResult<HttpCreateTokenResponse>> CreateAsync([FromBody] HttpCreateTokenRequestV2 request)
+ {
+
+ try
+ {
+ DateTime? expireTime = null;
+ if (request.ValidDays is not null)
+ expireTime = _clock.GetCurrentTime().AddDays(request.ValidDays.Value);
+
+ var userId = await _userService.VerifyCredential(request.Username, request.Password);
+ var token = await _userTokenService.CreateTokenAsync(userId, expireTime);
+ var user = await _userService.GetUserAsync(userId);
+
+ return new HttpCreateTokenResponse
+ {
+ Token = token,
+ User = await MapAsync<HttpUser>(user)
+ };
+ }
+ catch (EntityNotExistException)
+ {
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, BadCredentialMessage));
+ }
+ catch (BadPasswordException)
+ {
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, BadCredentialMessage));
+ }
+ }
+
+ private const string TokenExpiredMessage = "The token has expired.";
+ private const string TokenInvalidMessage = "The token is invalid.";
+
+ /// <summary>
+ /// Verify a token.
+ /// </summary>
+ /// <returns>Result of token verification.</returns>
+ [HttpPost("verify")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task<ActionResult<HttpVerifyTokenResponseV2>> VerifyAsync([FromBody] HttpVerifyOrRevokeTokenRequest request)
+ {
+ try
+ {
+ var tokenInfo = await _userTokenService.ValidateTokenAsync(request.Token);
+ var user = await _userService.GetUserAsync(tokenInfo.UserId);
+ return new HttpVerifyTokenResponseV2
+ {
+ User = await MapAsync<HttpUser>(user),
+ ExpireAt = tokenInfo.ExpireAt
+ };
+ }
+ catch (UserTokenExpiredException)
+ {
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenExpiredMessage));
+ }
+ catch (UserTokenException)
+ {
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage));
+ }
+ }
+
+ [HttpPost("revoke")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ [Authorize]
+ public async Task<ActionResult> RevokeAsync([FromBody] HttpVerifyOrRevokeTokenRequest body)
+ {
+ UserTokenInfo userTokenInfo;
+ try
+ {
+ userTokenInfo = await _userTokenService.ValidateTokenAsync(body.Token, false);
+ }
+ catch (UserTokenException)
+ {
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage));
+ }
+
+ if (userTokenInfo.UserId != GetAuthUserId())
+ return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage));
+
+ await _userTokenService.RevokeTokenAsync(body.Token);
+
+ return NoContent();
+ }
+
+ [HttpPost("revokeall")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize]
+ public async Task<ActionResult> RevokeAllAsync()
+ {
+ await _userTokenService.RevokeAllTokenByUserIdAsync(GetAuthUserId());
+ return NoContent();
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs
index 2a20d490..5881447a 100644
--- a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs
+++ b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs
@@ -1,4 +1,4 @@
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using Timeline.Controllers;
namespace Timeline.Models.Http
diff --git a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs
new file mode 100644
index 00000000..acd8d2e5
--- /dev/null
+++ b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs
@@ -0,0 +1,25 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Timeline.Models.Http
+{
+ public class HttpCreateTokenRequestV2
+ {
+ /// <summary>
+ /// The username.
+ /// </summary>
+ [Required]
+ public string Username { get; set; } = default!;
+ /// <summary>
+ /// The password.
+ /// </summary>
+ [Required]
+ public string Password { get; set; } = default!;
+ /// <summary>
+ /// Optional token validation period. In days. If not specified, the token will be valid until being revoked explicited.
+ /// </summary>
+ [Range(1, 365)]
+ public int? ValidDays { get; set; }
+ }
+}
+
diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs
index 98f86455..a0cca2e9 100644
--- a/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs
+++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs
@@ -1,11 +1,8 @@
-using Timeline.Controllers;
+using Timeline.Controllers;
namespace Timeline.Models.Http
{
- /// <summary>
- /// Request model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
- /// </summary>
- public class HttpVerifyTokenRequest
+ public class HttpVerifyOrRevokeTokenRequest
{
/// <summary>
/// The token to verify.
diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs
index ae8eb018..35789081 100644
--- a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs
+++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs
@@ -1,10 +1,10 @@
-using Timeline.Controllers;
+using Timeline.Controllers;
namespace Timeline.Models.Http
{
/// <summary>
- /// Response model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
+ /// Response model for <see cref="TokenController.Verify(HttpVerifyOrRevokeTokenRequest)"/>.
/// </summary>
public class HttpVerifyTokenResponse
{
diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs
new file mode 100644
index 00000000..c91771cf
--- /dev/null
+++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs
@@ -0,0 +1,16 @@
+
+using System;
+
+namespace Timeline.Models.Http
+{
+
+ public class HttpVerifyTokenResponseV2
+ {
+ /// <summary>
+ /// The user owning the token.
+ /// </summary>
+ public HttpUser User { get; set; } = default!;
+
+ public DateTime? ExpireAt { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Token/IUserTokenService.cs b/BackEnd/Timeline/Services/Token/IUserTokenService.cs
index 22fb0fb4..a9689f57 100644
--- a/BackEnd/Timeline/Services/Token/IUserTokenService.cs
+++ b/BackEnd/Timeline/Services/Token/IUserTokenService.cs
@@ -17,11 +17,12 @@ namespace Timeline.Services.Token
/// Verify a token and get the info of the token.
/// </summary>
/// <param name="token">The token to verify.</param>
+ /// <param name="checkLifetime">Whether to check lifetime of token.</param>
/// <returns>The info of the token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
/// <exception cref="UserTokenException">Thrown when the token is not valid for reasons other than expired.</exception>
- /// <exception cref="UserTokenExpiredException">Thrown when the token is expired.</exception>
- Task<UserTokenInfo> ValidateTokenAsync(string token);
+ /// <exception cref="UserTokenExpiredException">Thrown when <paramref name="checkLifetime"/> is true and the token is expired.</exception>
+ Task<UserTokenInfo> ValidateTokenAsync(string token, bool checkLifetime = true);
/// <summary>
/// Revoke a token to make it no longer valid.
diff --git a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
index 4d79295a..ceef4798 100644
--- a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
+++ b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
@@ -81,7 +81,7 @@ namespace Timeline.Services.Token
}
/// <inheritdoc/>
- public async Task<UserTokenInfo> ValidateTokenAsync(string token)
+ public async Task<UserTokenInfo> ValidateTokenAsync(string token, bool checkLifetime = true)
{
var entity = await _databaseContext.UserTokens.Where(t => t.Token == token && !t.Deleted).SingleOrDefaultAsync();
@@ -92,7 +92,7 @@ namespace Timeline.Services.Token
var currentTime = _clock.GetCurrentTime();
- if (entity.ExpireAt.HasValue && entity.ExpireAt.Value <= currentTime)
+ if (checkLifetime && entity.ExpireAt.HasValue && entity.ExpireAt.Value <= currentTime)
{
throw new UserTokenExpiredException(token, entity.ExpireAt.Value, currentTime);
}