aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2020-02-01 00:26:35 +0800
committerGitHub <noreply@github.com>2020-02-01 00:26:35 +0800
commitd703269e06d4c9e254fe2d5589ff04cdd6a9b366 (patch)
treef02f8d57440c777d4732bc4439f82e8b25c6732c /Timeline/Services
parent631731e5c2253116a53fdc435afca184251a34fc (diff)
parentbddf1d6eaac782672071df6527c40c81c3123f3a (diff)
downloadtimeline-d703269e06d4c9e254fe2d5589ff04cdd6a9b366.tar.gz
timeline-d703269e06d4c9e254fe2d5589ff04cdd6a9b366.tar.bz2
timeline-d703269e06d4c9e254fe2d5589ff04cdd6a9b366.zip
Merge pull request #56 from crupest/dev
Refactor API to be RESTful.
Diffstat (limited to 'Timeline/Services')
-rw-r--r--Timeline/Services/ConflictException.cs21
-rw-r--r--Timeline/Services/DatabaseExtensions.cs36
-rw-r--r--Timeline/Services/JwtBadVersionException.cs36
-rw-r--r--Timeline/Services/JwtUserTokenBadFormatException.cs48
-rw-r--r--Timeline/Services/JwtVerifyException.cs59
-rw-r--r--Timeline/Services/PasswordBadFormatException.cs27
-rw-r--r--Timeline/Services/TimelineAlreadyExistException.cs17
-rw-r--r--Timeline/Services/TimelineMemberOperationUserException.cs37
-rw-r--r--Timeline/Services/TimelineNameBadFormatException.cs21
-rw-r--r--Timeline/Services/TimelineService.cs394
-rw-r--r--Timeline/Services/User.cs18
-rw-r--r--Timeline/Services/UserAvatarService.cs50
-rw-r--r--Timeline/Services/UserDetailService.cs102
-rw-r--r--Timeline/Services/UserNotExistException.cs4
-rw-r--r--Timeline/Services/UserRoleConvert.cs43
-rw-r--r--Timeline/Services/UserService.cs552
-rw-r--r--Timeline/Services/UserTokenException.cs68
-rw-r--r--Timeline/Services/UserTokenManager.cs93
-rw-r--r--Timeline/Services/UserTokenService.cs (renamed from Timeline/Services/JwtService.cs)80
-rw-r--r--Timeline/Services/UsernameBadFormatException.cs27
-rw-r--r--Timeline/Services/UsernameConfictException.cs25
21 files changed, 825 insertions, 933 deletions
diff --git a/Timeline/Services/ConflictException.cs b/Timeline/Services/ConflictException.cs
new file mode 100644
index 00000000..6ede183a
--- /dev/null
+++ b/Timeline/Services/ConflictException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Thrown when a resource already exists and conflicts with the given resource.
+ /// </summary>
+ /// <remarks>
+ /// For example a username already exists and conflicts with the given username.
+ /// </remarks>
+ [Serializable]
+ public class ConflictException : Exception
+ {
+ public ConflictException() : base(Resources.Services.Exception.ConflictException) { }
+ public ConflictException(string message) : base(message) { }
+ public ConflictException(string message, Exception inner) : base(message, inner) { }
+ protected ConflictException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs
deleted file mode 100644
index 140c3146..00000000
--- a/Timeline/Services/DatabaseExtensions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Models.Validation;
-
-namespace Timeline.Services
-{
- internal static class DatabaseExtensions
- {
- private static readonly UsernameValidator usernameValidator = new UsernameValidator();
-
- /// <summary>
- /// Check the existence and get the id of the user.
- /// </summary>
- /// <param name="username">The username of the user.</param>
- /// <returns>The user id.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- internal static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string? username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- var (result, message) = usernameValidator.Validate(username);
- if (!result)
- throw new UsernameBadFormatException(username, message);
-
- var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
- if (userId == 0)
- throw new UserNotExistException(username);
- return userId;
- }
- }
-}
diff --git a/Timeline/Services/JwtBadVersionException.cs b/Timeline/Services/JwtBadVersionException.cs
deleted file mode 100644
index 4ce17710..00000000
--- a/Timeline/Services/JwtBadVersionException.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using Timeline.Helpers;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class JwtBadVersionException : Exception
- {
- public JwtBadVersionException() : base(Resources.Services.Exception.JwtBadVersionException) { }
- public JwtBadVersionException(string message) : base(message) { }
- public JwtBadVersionException(string message, Exception inner) : base(message, inner) { }
-
- public JwtBadVersionException(long tokenVersion, long requiredVersion)
- : base(Log.Format(Resources.Services.Exception.JwtBadVersionException,
- ("Token Version", tokenVersion),
- ("Required Version", requiredVersion)))
- {
- TokenVersion = tokenVersion;
- RequiredVersion = requiredVersion;
- }
-
- protected JwtBadVersionException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// The version in the token.
- /// </summary>
- public long? TokenVersion { get; set; }
-
- /// <summary>
- /// The version required.
- /// </summary>
- public long? RequiredVersion { get; set; }
- }
-}
diff --git a/Timeline/Services/JwtUserTokenBadFormatException.cs b/Timeline/Services/JwtUserTokenBadFormatException.cs
new file mode 100644
index 00000000..c528c3e3
--- /dev/null
+++ b/Timeline/Services/JwtUserTokenBadFormatException.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Globalization;
+using static Timeline.Resources.Services.Exception;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class JwtUserTokenBadFormatException : UserTokenBadFormatException
+ {
+ public enum ErrorKind
+ {
+ NoIdClaim,
+ IdClaimBadFormat,
+ NoVersionClaim,
+ VersionClaimBadFormat,
+ Other
+ }
+
+ public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { }
+ public JwtUserTokenBadFormatException(string message) : base(message) { }
+ public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; }
+ protected JwtUserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public ErrorKind ErrorType { get; set; }
+
+ private static string GetErrorMessage(ErrorKind type)
+ {
+ var reason = type switch
+ {
+ ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing,
+ ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat,
+ ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing,
+ ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat,
+ ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers,
+ _ => JwtUserTokenBadFormatExceptionUnknown
+ };
+
+ return string.Format(CultureInfo.CurrentCulture,
+ Resources.Services.Exception.JwtUserTokenBadFormatException, reason);
+ }
+ }
+}
diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs
deleted file mode 100644
index a915b51a..00000000
--- a/Timeline/Services/JwtVerifyException.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Microsoft.IdentityModel.Tokens;
-using System;
-using System.Globalization;
-using static Timeline.Resources.Services.Exception;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class JwtVerifyException : Exception
- {
- public static class ErrorCodes
- {
- // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server.
-
- public const int Others = -1001;
- public const int NoIdClaim = -1002;
- public const int IdClaimBadFormat = -1003;
- public const int NoVersionClaim = -1004;
- public const int VersionClaimBadFormat = -1005;
-
- /// <summary>
- /// Corresponds to <see cref="SecurityTokenExpiredException"/>.
- /// </summary>
- public const int Expired = -2001;
- public const int OldVersion = -2002;
- }
-
- public JwtVerifyException() : base(GetErrorMessage(0)) { }
- public JwtVerifyException(string message) : base(message) { }
- public JwtVerifyException(string message, Exception inner) : base(message, inner) { }
-
- public JwtVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
- public JwtVerifyException(string message, int code) : base(message) { ErrorCode = code; }
- public JwtVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
- public JwtVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
- protected JwtVerifyException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public int ErrorCode { get; set; }
-
- private static string GetErrorMessage(int errorCode)
- {
- var reason = errorCode switch
- {
- ErrorCodes.Others => JwtVerifyExceptionOthers,
- ErrorCodes.NoIdClaim => JwtVerifyExceptionNoIdClaim,
- ErrorCodes.IdClaimBadFormat => JwtVerifyExceptionIdClaimBadFormat,
- ErrorCodes.NoVersionClaim => JwtVerifyExceptionNoVersionClaim,
- ErrorCodes.VersionClaimBadFormat => JwtVerifyExceptionVersionClaimBadFormat,
- ErrorCodes.Expired => JwtVerifyExceptionExpired,
- ErrorCodes.OldVersion => JwtVerifyExceptionOldVersion,
- _ => JwtVerifyExceptionUnknown
- };
-
- return string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.JwtVerifyException, reason);
- }
- }
-}
diff --git a/Timeline/Services/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs
new file mode 100644
index 00000000..2029ebb4
--- /dev/null
+++ b/Timeline/Services/PasswordBadFormatException.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class PasswordBadFormatException : Exception
+ {
+ public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { }
+ public PasswordBadFormatException(string message) : base(message) { }
+ public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public PasswordBadFormatException(string password, string validationMessage) : this()
+ {
+ Password = password;
+ ValidationMessage = validationMessage;
+ }
+
+ protected PasswordBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Password { get; set; } = "";
+
+ public string ValidationMessage { get; set; } = "";
+ }
+}
diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs
deleted file mode 100644
index c2dea1f9..00000000
--- a/Timeline/Services/TimelineAlreadyExistException.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineAlreadyExistException : Exception
- {
- public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { }
- public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; }
- public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; }
- protected TimelineAlreadyExistException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public string? Name { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs
deleted file mode 100644
index 543ee160..00000000
--- a/Timeline/Services/TimelineMemberOperationUserException.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineMemberOperationUserException : Exception
- {
- public enum MemberOperation
- {
- Add,
- Remove
- }
-
- public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { }
- public TimelineMemberOperationUserException(string message) : base(message) { }
- public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { }
- protected TimelineMemberOperationUserException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner)
- : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; }
-
- private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture,
- Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index);
-
- public MemberOperation? Operation { get; set; }
-
- /// <summary>
- /// The index of the member on which the operation failed.
- /// </summary>
- public int? Index { get; set; }
-
- public string? Username { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs
deleted file mode 100644
index 5120a175..00000000
--- a/Timeline/Services/TimelineNameBadFormatException.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineNameBadFormatException : Exception
- {
- public TimelineNameBadFormatException()
- : base(Resources.Services.Exception.TimelineNameBadFormatException) { }
- public TimelineNameBadFormatException(string name)
- : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; }
- public TimelineNameBadFormatException(string name, Exception inner)
- : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; }
-
- protected TimelineNameBadFormatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public string? Name { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs
index f7b0e0e9..0ea68265 100644
--- a/Timeline/Services/TimelineService.cs
+++ b/Timeline/Services/TimelineService.cs
@@ -1,13 +1,15 @@
-using Microsoft.EntityFrameworkCore;
+using AutoMapper;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
-using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
+using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
@@ -28,12 +30,7 @@ namespace Timeline.Services
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>A list of all posts.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -46,26 +43,20 @@ namespace Timeline.Services
/// Create a new post in timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <ssee cref="IBaseTimelineService"/>.</param>
- /// <param name="author">The author's username.</param>
+ /// <param name="authorId">The author's id.</param>
/// <param name="content">The content.</param>
/// <param name="time">The time of the post. If null, then use current time.</param>
/// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="author"/> or <paramref name="content"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="content"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="author"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if <paramref name="author"/> does not exist.</exception>
- Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time);
+ /// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
+ Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time);
/// <summary>
/// Delete a post
@@ -73,12 +64,7 @@ namespace Timeline.Services
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="id">The id of the post to delete.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -95,50 +81,22 @@ namespace Timeline.Services
Task DeletePost(string name, long id);
/// <summary>
- /// Set the properties of a timeline.
- /// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="newProperties">The new properties. Null member means not to change.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="newProperties"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
- Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties);
-
- /// <summary>
/// Remove members to a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="add">A list of usernames of members to add. May be null.</param>
/// <param name="remove">A list of usernames of members to remove. May be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="ArgumentException">Thrown when names in <paramref name="add"/> or <paramref name="remove"/> is not a valid username.</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="TimelineMemberOperationUserException">
- /// Thrown when an exception occurs on the user list.
- /// The inner exception is <see cref="UsernameBadFormatException"/>
- /// when one of the username is invalid.
- /// The inner exception is <see cref="UserNotExistException"/>
- /// when one of the user to change does not exist.
+ /// <exception cref="UserNotExistException">
+ /// Thrown when one of the user to change does not exist.
/// </exception>
/// <remarks>
/// Operating on a username that is of bad format or does not exist always throws.
@@ -153,42 +111,30 @@ namespace Timeline.Services
/// Verify whether a visitor has the permission to read a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on. Null means visitor without account.</param>
+ /// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
/// <returns>True if can read, false if can't read.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when <paramref name="username"/> does not exist.
- /// </exception>
- Task<bool> HasReadPermission(string name, string? username);
+ /// <remarks>
+ /// This method does not check whether visitor is administrator.
+ /// Return false if user with visitor id does not exist.
+ /// </remarks>
+ Task<bool> HasReadPermission(string name, long? visitorId);
/// <summary>
/// Verify whether a user has the permission to modify a post.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on.</param>
+ /// <param name="modifierId">The id of the user to check on.</param>
/// <returns>True if can modify, false if can't modify.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -198,47 +144,32 @@ namespace Timeline.Services
/// <exception cref="TimelinePostNotExistException">
/// Thrown when the post with given id does not exist or is deleted already.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when <paramref name="username"/> does not exist.
- /// </exception>
/// <remarks>
/// This method does not check whether the user is administrator.
/// It only checks whether he is the author of the post or the owner of the timeline.
+ /// Return false when user with modifier id does not exist.
/// </remarks>
- Task<bool> HasPostModifyPermission(string name, long id, string username);
+ Task<bool> HasPostModifyPermission(string name, long id, long modifierId);
/// <summary>
/// Verify whether a user is member of a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on.</param>
+ /// <param name="userId">The id of user to check on.</param>
/// <returns>True if it is a member, false if not.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is not a valid username.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when user <paramref name="username"/> does not exist.
- /// </exception>
/// <remarks>
/// Timeline owner is also considered as a member.
+ /// Return false when user with user id does not exist.
/// </remarks>
- Task<bool> IsMemberOf(string name, string username);
+ Task<bool> IsMemberOf(string name, long userId);
}
/// <summary>
@@ -252,7 +183,7 @@ namespace Timeline.Services
/// <param name="name">The name of the timeline.</param>
/// <returns>The timeline info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
+ /// <exception cref="ArgumentException">
/// Thrown when timeline name is invalid. Currently it means it is an empty string.
/// </exception>
/// <exception cref="TimelineNotExistException">
@@ -264,20 +195,12 @@ namespace Timeline.Services
/// Create a timeline.
/// </summary>
/// <param name="name">The name of the timeline.</param>
- /// <param name="owner">The owner of the timeline.</param>
+ /// <param name="owner">The id of owner of the timeline.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="owner"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is invalid. Currently it means it is an empty string.
- /// </exception>
- /// <exception cref="TimelineAlreadyExistException">
- /// Thrown when the timeline already exists.
- /// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when the username of the owner is not valid.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when the owner user does not exist.</exception>
- Task CreateTimeline(string name, string owner);
+ /// <exception cref="ArgumentException">Thrown when timeline name is invalid. Currently it means it is an empty string.</exception>
+ /// <exception cref="ConflictException">Thrown when the timeline already exists.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
+ Task CreateTimeline(string name, long owner);
}
public interface IPersonalTimelineService : IBaseTimelineService
@@ -290,21 +213,40 @@ namespace Timeline.Services
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="username"/> is null.
/// </exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format. Inner exception MUST be <see cref="UsernameBadFormatException"/>.
+ /// <exception cref="ArgumentException">
+ /// Thrown when <paramref name="username"/> is of bad format.
/// </exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
/// </exception>
Task<BaseTimelineInfo> GetTimeline(string username);
+
+ /// <summary>
+ /// Set the properties of a timeline.
+ /// </summary>
+ /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="newProperties">The new properties. Null member means not to change.</param>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown when <paramref name="username"/> is null.
+ /// </exception>
+ /// <exception cref="ArgumentException">
+ /// Thrown when <paramref name="username"/> is of bad format.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task ChangeProperty(string name, TimelinePatchRequest newProperties);
+
}
public abstract class BaseTimelineService : IBaseTimelineService
{
- protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
+ protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
{
Clock = clock;
Database = database;
+ UserService = userService;
+ Mapper = mapper;
}
protected IClock Clock { get; }
@@ -313,6 +255,10 @@ namespace Timeline.Services
protected DatabaseContext Database { get; }
+ protected IUserService UserService { get; }
+
+ protected IMapper Mapper { get; }
+
/// <summary>
/// Find the timeline id by the name.
/// For details, see remarks.
@@ -320,12 +266,7 @@ namespace Timeline.Services
/// <param name="name">The username or the timeline name. See remarks.</param>
/// <returns>The id of the timeline entity.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -347,66 +288,60 @@ namespace Timeline.Services
if (name == null)
throw new ArgumentNullException(nameof(name));
+
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
+
var posts = new List<TimelinePostInfo>();
foreach (var entity in postEntities)
{
- posts.Add(new TimelinePostInfo
+ if (entity.Content != null) // otherwise it is deleted
{
- Id = entity.Id,
- Content = entity.Content,
- Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
- Time = entity.Time
- });
+ var author = Mapper.Map<UserInfo>(await UserService.GetUserById(entity.AuthorId));
+ posts.Add(new TimelinePostInfo
+ {
+ Id = entity.Id,
+ Content = entity.Content,
+ Author = author,
+ Time = entity.Time,
+ LastUpdated = entity.LastUpdated
+ });
+ }
}
return posts;
}
- public async Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time)
+ public async Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (author == null)
- throw new ArgumentNullException(nameof(author));
if (content == null)
throw new ArgumentNullException(nameof(content));
- {
- var (result, message) = UsernameValidator.Validate(author);
- if (!result)
- {
- throw new UsernameBadFormatException(author, message);
- }
- }
-
var timelineId = await FindTimelineId(name);
-
- var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
- if (authorEntity == null)
- {
- throw new UserNotExistException(author);
- }
- var authorId = authorEntity.Id;
+ var author = Mapper.Map<UserInfo>(await UserService.GetUserById(authorId));
var currentTime = Clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
var postEntity = new TimelinePostEntity
{
Content = content,
AuthorId = authorId,
TimelineId = timelineId,
- Time = time ?? currentTime,
+ Time = finalTime,
LastUpdated = currentTime
};
-
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
- return new TimelinePostCreateResponse
+ return new TimelinePostInfo
{
Id = postEntity.Id,
- Time = postEntity.Time
+ Content = content,
+ Author = author,
+ Time = finalTime,
+ LastUpdated = currentTime
};
}
@@ -415,6 +350,9 @@ namespace Timeline.Services
if (name == null)
throw new ArgumentNullException(nameof(name));
+ // Currently we don't use the result. But we need to check the timeline.
+ var _ = await FindTimelineId(name);
+
var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync();
if (post == null)
@@ -426,7 +364,7 @@ namespace Timeline.Services
await Database.SaveChangesAsync();
}
- public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
+ public async Task ChangeProperty(string name, TimelinePatchRequest newProperties)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -455,27 +393,23 @@ namespace Timeline.Services
if (name == null)
throw new ArgumentNullException(nameof(name));
- // remove duplication and check the format of each username.
- // Return a username->index map.
- Dictionary<string, int>? RemoveDuplicateAndCheckFormat(IList<string>? list, TimelineMemberOperationUserException.MemberOperation operation)
+ List<string>? RemoveDuplicateAndCheckFormat(IList<string>? list, string paramName)
{
if (list != null)
{
- Dictionary<string, int> result = new Dictionary<string, int>();
+ List<string> result = new List<string>();
var count = list.Count;
for (var index = 0; index < count; index++)
{
var username = list[index];
- if (result.ContainsKey(username))
+ if (result.Contains(username))
{
continue;
}
var (validationResult, message) = UsernameValidator.Validate(username);
if (!validationResult)
- throw new TimelineMemberOperationUserException(
- index, operation, username,
- new UsernameBadFormatException(username, message));
- result.Add(username, index);
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName));
+ result.Add(username);
}
return result;
}
@@ -484,13 +418,13 @@ namespace Timeline.Services
return null;
}
}
- var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
- var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
+ var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add));
+ var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove));
// remove those both in add and remove
if (simplifiedAdd != null && simplifiedRemove != null)
{
- var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
+ var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList();
foreach (var u in usersToClean)
{
simplifiedAdd.Remove(u);
@@ -500,26 +434,20 @@ namespace Timeline.Services
var timelineId = await FindTimelineId(name);
- async Task<List<long>?> CheckExistenceAndGetId(Dictionary<string, int>? map, TimelineMemberOperationUserException.MemberOperation operation)
+ async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
{
- if (map == null)
+ if (list == null)
return null;
List<long> result = new List<long>();
- foreach (var (username, index) in map)
+ foreach (var username in list)
{
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
- if (user == null)
- {
- throw new TimelineMemberOperationUserException(index, operation, username,
- new UserNotExistException(username));
- }
- result.Add(user.Id);
+ result.Add(await UserService.GetUserIdByUsername(username));
}
return result;
}
- var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
- var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
+ var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd);
+ var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove);
if (userIdsAdd != null)
{
@@ -536,30 +464,11 @@ namespace Timeline.Services
await Database.SaveChangesAsync();
}
- public async Task<bool> HasReadPermission(string name, string? username)
+ public async Task<bool> HasReadPermission(string name, long? visitorId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- long? userId = null;
- if (username != null)
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
-
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- userId = user.Id;
- }
-
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
@@ -567,43 +476,24 @@ namespace Timeline.Services
if (timelineEntity.Visibility == TimelineVisibility.Public)
return true;
- if (timelineEntity.Visibility == TimelineVisibility.Register && username != null)
+ if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null)
return true;
- if (userId == null)
+ if (visitorId == null)
{
return false;
}
else
{
- var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
+ var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync();
return memberEntity != null;
}
}
- public async Task<bool> HasPostModifyPermission(string name, long id, string username)
+ public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
- }
-
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- var userId = user.Id;
var timelineId = await FindTimelineId(name);
@@ -614,32 +504,13 @@ namespace Timeline.Services
if (postEntity == null)
throw new TimelinePostNotExistException(id);
- return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId;
+ return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
}
- public async Task<bool> IsMemberOf(string name, string username)
+ public async Task<bool> IsMemberOf(string name, long userId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
- }
-
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- var userId = user.Id;
var timelineId = await FindTimelineId(name);
@@ -648,38 +519,33 @@ namespace Timeline.Services
if (userId == timelineEntity.OwnerId)
return true;
- var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync();
-
- return timelineMemberEntity != null;
+ return await Database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
}
}
public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
{
- public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
- : base(loggerFactory, database, clock)
+ public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
+ : base(loggerFactory, database, userService, mapper, clock)
{
}
protected override async Task<long> FindTimelineId(string name)
{
+ long userId;
+ try
{
- var (result, message) = UsernameValidator.Validate(name);
- if (!result)
- {
- throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message));
- }
+ userId = await UserService.GetUserIdByUsername(name);
}
-
- var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (userEntity == null)
+ catch (ArgumentException e)
{
- throw new TimelineNotExistException(name, new UserNotExistException(name));
+ throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(name, e);
}
-
- var userId = userEntity.Id;
var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
@@ -715,16 +581,20 @@ namespace Timeline.Services
var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
- var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray();
+ var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
- var memberUsernames = await Task.WhenAll(memberUsernameTasks);
+ var members = new List<UserInfo>();
+ foreach (var memberEntity in timelineMemberEntities)
+ {
+ members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
+ }
return new BaseTimelineInfo
{
Description = timelineEntity.Description ?? "",
- Owner = username,
+ Owner = owner,
Visibility = timelineEntity.Visibility,
- Members = memberUsernames.ToList()
+ Members = members
};
}
diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs
new file mode 100644
index 00000000..09a472e5
--- /dev/null
+++ b/Timeline/Services/User.cs
@@ -0,0 +1,18 @@
+namespace Timeline.Services
+{
+ public class User
+ {
+ public string? Username { get; set; }
+ public string? Nickname { get; set; }
+
+ #region adminsecret
+ public bool? Administrator { get; set; }
+ #endregion adminsecret
+
+ #region secret
+ public long? Id { get; set; }
+ public string? Password { get; set; }
+ public long? Version { get; set; }
+ #endregion secret
+ }
+}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index 01201864..39b408e6 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -11,7 +11,6 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
-using Timeline.Models.Validation;
namespace Timeline.Services
{
@@ -61,36 +60,27 @@ namespace Timeline.Services
public interface IUserAvatarService
{
/// <summary>
- /// Get the etag of a user's avatar.
+ /// Get the etag of a user's avatar. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to get avatar etag of.</param>
+ /// <param name="id">The id of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- Task<string> GetAvatarETag(string username);
+ Task<string> GetAvatarETag(long id);
/// <summary>
- /// Get avatar of a user. If the user has no avatar set, a default one is returned.
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to get avatar of.</param>
+ /// <param name="id">The id of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- Task<AvatarInfo> GetAvatar(string username);
+ Task<AvatarInfo> GetAvatar(long id);
/// <summary>
- /// Set avatar for a user.
+ /// Set avatar for a user. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to set avatar for.</param>
+ /// <param name="id">The id of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentNullException">Throw if <paramref name="username"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
/// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar? avatar);
+ Task SetAvatar(long id, Avatar? avatar);
}
// TODO! : Make this configurable.
@@ -104,7 +94,6 @@ namespace Timeline.Services
private DateTime _cacheLastModified;
private string _cacheETag = default!;
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")]
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
_avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
@@ -195,22 +184,18 @@ namespace Timeline.Services
_clock = clock;
}
- public async Task<string> GetAvatarETag(string username)
+ public async Task<string> GetAvatarETag(long id)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
return eTag;
}
- public async Task<AvatarInfo> GetAvatar(string username)
+ public async Task<AvatarInfo> GetAvatar(long id)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
if (avatarEntity != null)
{
@@ -240,7 +225,7 @@ namespace Timeline.Services
return defaultAvatar;
}
- public async Task SetAvatar(string username, Avatar? avatar)
+ public async Task SetAvatar(long id, Avatar? avatar)
{
if (avatar != null)
{
@@ -250,8 +235,7 @@ namespace Timeline.Services
throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar));
}
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync();
if (avatar == null)
{
@@ -275,13 +259,13 @@ namespace Timeline.Services
var create = avatarEntity == null;
if (create)
{
- avatarEntity = new UserAvatar();
+ avatarEntity = new UserAvatarEntity();
}
avatarEntity!.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data);
avatarEntity.LastModified = _clock.GetCurrentTime();
- avatarEntity.UserId = userId;
+ avatarEntity.UserId = id;
if (create)
{
_database.UserAvatars.Add(avatarEntity);
diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs
deleted file mode 100644
index 0b24e4e2..00000000
--- a/Timeline/Services/UserDetailService.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using Microsoft.Extensions.Logging;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using static Timeline.Resources.Services.UserDetailService;
-
-namespace Timeline.Services
-{
- public interface IUserDetailService
- {
- /// <summary>
- /// Get the nickname of the user with given username.
- /// If the user does not set a nickname, the username is returned as the nickname.
- /// </summary>
- /// <param name="username">The username of the user to get nickname of.</param>
- /// <returns>The nickname of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user does not exist.</exception>
- Task<string> GetNickname(string username);
-
- /// <summary>
- /// Set the nickname of the user with given username.
- /// </summary>
- /// <param name="username">The username of the user to set nickname of.</param>
- /// <param name="nickname">The nickname. Pass null to unset.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="nickname"/> is not null but its length is bigger than 10.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user does not exist.</exception>
- Task SetNickname(string username, string? nickname);
- }
-
- public class UserDetailService : IUserDetailService
- {
- private readonly DatabaseContext _database;
-
- private readonly ILogger<UserDetailService> _logger;
-
- public UserDetailService(DatabaseContext database, ILogger<UserDetailService> logger)
- {
- _database = database;
- _logger = logger;
- }
-
- public async Task<string> GetNickname(string username)
- {
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var nickname = _database.UserDetails.Where(d => d.UserId == userId).Select(d => new { d.Nickname }).SingleOrDefault()?.Nickname;
- return nickname ?? username;
- }
-
- public async Task SetNickname(string username, string? nickname)
- {
- if (nickname != null && nickname.Length > 10)
- {
- throw new ArgumentException(ExceptionNicknameTooLong, nameof(nickname));
- }
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var userDetail = _database.UserDetails.Where(d => d.UserId == userId).SingleOrDefault();
- if (nickname == null)
- {
- if (userDetail == null || userDetail.Nickname == null)
- {
- return;
- }
- else
- {
- userDetail.Nickname = null;
- await _database.SaveChangesAsync();
- _logger.LogInformation(LogEntityNicknameSetToNull, userId);
- }
- }
- else
- {
- var create = userDetail == null;
- if (create)
- {
- userDetail = new UserDetail
- {
- UserId = userId
- };
- }
- userDetail!.Nickname = nickname;
- if (create)
- {
- _database.UserDetails.Add(userDetail);
- }
- await _database.SaveChangesAsync();
- if (create)
- {
- _logger.LogInformation(LogEntityNicknameCreate, userId, nickname);
- }
- else
- {
- _logger.LogInformation(LogEntityNicknameSetNotNull, userId, nickname);
- }
- }
- }
- }
-}
diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs
index c7317f56..fd0b5ecf 100644
--- a/Timeline/Services/UserNotExistException.cs
+++ b/Timeline/Services/UserNotExistException.cs
@@ -31,11 +31,11 @@ namespace Timeline.Services
/// <summary>
/// The username of the user that does not exist.
/// </summary>
- public string? Username { get; set; }
+ public string Username { get; set; } = "";
/// <summary>
/// The id of the user that does not exist.
/// </summary>
- public long? Id { get; set; }
+ public long Id { get; set; }
}
}
diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs
new file mode 100644
index 00000000..f27ee1bb
--- /dev/null
+++ b/Timeline/Services/UserRoleConvert.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ public static class UserRoleConvert
+ {
+ public const string UserRole = UserRoles.User;
+ public const string AdminRole = UserRoles.Admin;
+
+ public static string[] ToArray(bool administrator)
+ {
+ return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole };
+ }
+
+ public static string[] ToArray(string s)
+ {
+ return s.Split(',').ToArray();
+ }
+
+ public static bool ToBool(IReadOnlyCollection<string> roles)
+ {
+ return roles.Contains(AdminRole);
+ }
+
+ public static string ToString(IReadOnlyCollection<string> roles)
+ {
+ return string.Join(',', roles);
+ }
+
+ public static string ToString(bool administrator)
+ {
+ return administrator ? UserRole + "," + AdminRole : UserRole;
+ }
+
+ public static bool ToBool(string s)
+ {
+ return s.Contains("admin", StringComparison.InvariantCulture);
+ }
+ }
+}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 4012539f..7dc7159d 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -1,396 +1,462 @@
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
-using Timeline.Models;
using Timeline.Models.Validation;
+using static Timeline.Resources.Services.UserService;
namespace Timeline.Services
{
- public class CreateTokenResult
- {
- public string Token { get; set; } = default!;
- public UserInfo User { get; set; } = default!;
- }
-
public interface IUserService
{
/// <summary>
- /// Try to anthenticate with the given username and password.
- /// If success, create a token and return the user info.
+ /// Try to verify the given username and password.
/// </summary>
- /// <param name="username">The username of the user to anthenticate.</param>
- /// <param name="password">The password of the user to anthenticate.</param>
- /// <param name="expires">The expired time point. Null then use default. See <see cref="JwtService.GenerateJwtToken(TokenInfo, DateTime?)"/> for what is default.</param>
- /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
+ /// <param name="username">The username of the user to verify.</param>
+ /// <param name="password">The password of the user to verify.</param>
+ /// <returns>The user info and auth info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when username is of bad format.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or <paramref name="password"/> is empty.</exception>
/// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires = null);
+ Task<User> VerifyCredential(string username, string password);
/// <summary>
- /// Verify the given token.
- /// If success, return the user info.
+ /// Try to get a user by id.
/// </summary>
- /// <param name="token">The token to verify.</param>
- /// <returns>The user info specified by the token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
- Task<UserInfo> VerifyToken(string token);
+ /// <param name="id">The id of the user.</param>
+ /// <returns>The user info.</returns>
+ /// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
+ Task<User> GetUserById(long id);
/// <summary>
/// Get the user info of given username.
/// </summary>
/// <param name="username">Username of the user.</param>
- /// <returns>The info of the user. Null if the user of given username does not exists.</returns>
+ /// <returns>The info of the user.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- Task<UserInfo> GetUser(string username);
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<User> GetUserByUsername(string username);
+
+ /// <summary>
+ /// Get the user id of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The id of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsername(string username);
/// <summary>
/// List all users.
/// </summary>
/// <returns>The user info of users.</returns>
- Task<UserInfo[]> ListUsers();
+ Task<User[]> GetUsers();
/// <summary>
- /// Create or modify a user with given username.
- /// Username must be match with [a-zA-z0-9-_].
+ /// Create a user with given info.
/// </summary>
- /// <param name="username">Username of user.</param>
- /// <param name="password">Password of user.</param>
- /// <param name="administrator">Whether the user is administrator.</param>
- /// <returns>
- /// Return <see cref="PutResult.Create"/> if a new user is created.
- /// Return <see cref="PutResult.Modify"/> if a existing user is modified.
- /// </returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- Task<PutResult> PutUser(string username, string password, bool administrator);
+ /// <param name="info">The info of new user.</param>
+ /// <param name="password">The password, can't be null or empty.</param>
+ /// <returns>The the new user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="info"/>is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="ConflictException">Thrown when a user with given username already exists.</exception>
+ /// <remarks>
+ /// <see cref="User.Username"/> must not be null and must be a valid username.
+ /// <see cref="User.Password"/> must not be null or empty.
+ /// <see cref="User.Administrator"/> is false by default (null).
+ /// <see cref="User.Nickname"/> must be a valid nickname if set. It is empty by default.
+ /// Other fields are ignored.
+ /// </remarks>
+ Task<User> CreateUser(User info);
/// <summary>
- /// Partially modify a user of given username.
+ /// Modify a user's info.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <param name="info">The new info. May be null.</param>
+ /// <returns>The new user info.</returns>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <remarks>
+ /// Only <see cref="User.Username"/>, <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
+ /// If null, then not change.
+ /// Other fields are ignored.
+ /// Version will increase if password is changed.
+ ///
+ /// <see cref="User.Username"/> must be a valid username if set.
+ /// <see cref="User.Password"/> can't be empty if set.
+ /// <see cref="User.Nickname"/> must be a valid nickname if set.
///
- /// Note that whether actually modified or not, Version of the user will always increase.
+ /// </remarks>
+ /// <seealso cref="ModifyUser(string, User)"/>
+ Task<User> ModifyUser(long id, User? info);
+
+ /// <summary>
+ /// Modify a user's info.
/// </summary>
- /// <param name="username">Username of the user to modify. Can't be null.</param>
- /// <param name="password">New password. Null if not modify.</param>
- /// <param name="administrator">Whether the user is administrator. Null if not modify.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- Task PatchUser(string username, string? password, bool? administrator);
+ /// <param name="username">The username of the user.</param>
+ /// <param name="info">The new info. May be null.</param>
+ /// <returns>The new user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <exception cref="ConflictException">Thrown when user with the newusername already exist.</exception>
+ /// <remarks>
+ /// Only <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
+ /// If null, then not change.
+ /// Other fields are ignored.
+ /// After modified, even if nothing is changed, version will increase.
+ ///
+ /// <see cref="User.Username"/> must be a valid username if set.
+ /// <see cref="User.Password"/> can't be empty if set.
+ /// <see cref="User.Nickname"/> must be a valid nickname if set.
+ ///
+ /// Note: Whether <see cref="User.Version"/> is set or not, version will increase and not set to the specified value if there is one.
+ /// </remarks>
+ /// <seealso cref="ModifyUser(long, User)"/>
+ Task<User> ModifyUser(string username, User? info);
+
+ /// <summary>
+ /// Delete a user of given id.
+ /// </summary>
+ /// <param name="id">Id of the user to delete.</param>
+ /// <returns>True if user is deleted, false if user not exist.</returns>
+ Task<bool> DeleteUser(long id);
/// <summary>
/// Delete a user of given username.
/// </summary>
- /// <param name="username">Username of thet user to delete. Can't be null.</param>
+ /// <param name="username">Username of the user to delete. Can't be null.</param>
+ /// <returns>True if user is deleted, false if user not exist.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- Task DeleteUser(string username);
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ Task<bool> DeleteUser(string username);
/// <summary>
/// Try to change a user's password with old password.
/// </summary>
- /// <param name="username">The name of user to change password of.</param>
- /// <param name="oldPassword">The user's old password.</param>
- /// <param name="newPassword">The user's new password.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> or <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <param name="id">The id of user to change password of.</param>
+ /// <param name="oldPassword">Old password.</param>
+ /// <param name="newPassword">New password.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is empty.</exception>
/// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
- Task ChangePassword(string username, string oldPassword, string newPassword);
-
- /// <summary>
- /// Change a user's username.
- /// </summary>
- /// <param name="oldUsername">The user's old username.</param>
- /// <param name="newUsername">The new username.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldUsername"/> or <paramref name="newUsername"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with old username does not exist.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="oldUsername"/> or <paramref name="newUsername"/> is of bad format.</exception>
- /// <exception cref="UsernameConfictException">Thrown if user with the new username already exists.</exception>
- Task ChangeUsername(string oldUsername, string newUsername);
- }
-
- internal class UserCache
- {
- public string Username { get; set; } = default!;
- public bool Administrator { get; set; }
- public long Version { get; set; }
-
- public UserInfo ToUserInfo()
- {
- return new UserInfo(Username, Administrator);
- }
+ Task ChangePassword(long id, string oldPassword, string newPassword);
}
public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
- private readonly IMemoryCache _memoryCache;
private readonly DatabaseContext _databaseContext;
- private readonly IJwtService _jwtService;
private readonly IPasswordService _passwordService;
- private readonly UsernameValidator _usernameValidator;
-
- public UserService(ILogger<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService)
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+ private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService)
{
_logger = logger;
- _memoryCache = memoryCache;
_databaseContext = databaseContext;
- _jwtService = jwtService;
_passwordService = passwordService;
-
- _usernameValidator = new UsernameValidator();
}
- private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
+ private void CheckUsernameFormat(string username, string? paramName)
+ {
+ if (!_usernameValidator.Validate(username, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName);
+ }
+ }
- private void RemoveCache(long id)
+ private static void CheckPasswordFormat(string password, string? paramName)
{
- var key = GenerateCacheKeyByUserId(id);
- _memoryCache.Remove(key);
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key)));
+ if (password.Length == 0)
+ {
+ throw new ArgumentException(ExceptionPasswordEmpty, paramName);
+ }
}
- private void CheckUsernameFormat(string username, string? additionalMessage = null)
+ private void CheckNicknameFormat(string nickname, string? paramName)
{
- var (result, message) = _usernameValidator.Validate(username);
- if (!result)
+ if (!_nicknameValidator.Validate(nickname, out var message))
{
- if (additionalMessage == null)
- throw new UsernameBadFormatException(username, message);
- else
- throw new UsernameBadFormatException(username, additionalMessage + message);
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName);
}
}
- public async Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires)
+ private static void ThrowUsernameConflict()
+ {
+ throw new ConflictException(ExceptionUsernameConflict);
+ }
+
+ private static User CreateUserFromEntity(UserEntity entity)
+ {
+ return new User
+ {
+ Username = entity.Username,
+ Administrator = UserRoleConvert.ToBool(entity.Roles),
+ Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
+ Id = entity.Id,
+ Version = entity.Version
+ };
+ }
+
+ public async Task<User> VerifyCredential(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
- CheckUsernameFormat(username);
- // We need password info, so always check the database.
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ CheckUsernameFormat(username, nameof(username));
+ CheckPasswordFormat(password, nameof(password));
- if (user == null)
+ var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
+
+ if (entity == null)
throw new UserNotExistException(username);
- if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
+ if (!_passwordService.VerifyPassword(entity.Password, password))
throw new BadPasswordException(password);
- var token = _jwtService.GenerateJwtToken(new TokenInfo
- {
- Id = user.Id,
- Version = user.Version
- }, expires);
-
- return new CreateTokenResult
- {
- Token = token,
- User = UserConvert.CreateUserInfo(user)
- };
+ return CreateUserFromEntity(entity);
}
- public async Task<UserInfo> VerifyToken(string token)
+ public async Task<User> GetUserById(long id)
{
- if (token == null)
- throw new ArgumentNullException(nameof(token));
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
- TokenInfo tokenInfo;
- tokenInfo = _jwtService.VerifyJwtToken(token);
+ if (user == null)
+ throw new UserNotExistException(id);
- var id = tokenInfo.Id;
- var key = GenerateCacheKeyByUserId(id);
- if (!_memoryCache.TryGetValue<UserCache>(key, out var cache))
- {
- // no cache, check the database
- var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ return CreateUserFromEntity(user);
+ }
- if (user == null)
- throw new UserNotExistException(id);
+ public async Task<User> GetUserByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
- // create cache
- cache = UserConvert.CreateUserCache(user);
- _memoryCache.CreateEntry(key).SetValue(cache);
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
- }
+ CheckUsernameFormat(username, nameof(username));
- if (tokenInfo.Version != cache.Version)
- throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion);
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync();
- return cache.ToUserInfo();
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return CreateUserFromEntity(entity);
}
- public async Task<UserInfo> GetUser(string username)
+ public async Task<long> GetUserIdByUsername(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username);
- return await _databaseContext.Users
- .Where(user => user.Name == username)
- .Select(user => UserConvert.CreateUserInfo(user))
- .SingleOrDefaultAsync();
+ CheckUsernameFormat(username, nameof(username));
+
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
}
- public async Task<UserInfo[]> ListUsers()
+ public async Task<User[]> GetUsers()
{
- return await _databaseContext.Users
- .Select(user => UserConvert.CreateUserInfo(user))
- .ToArrayAsync();
+ var entities = await _databaseContext.Users.ToArrayAsync();
+ return entities.Select(user => CreateUserFromEntity(user)).ToArray();
}
- public async Task<PutResult> PutUser(string username, string password, bool administrator)
+ public async Task<User> CreateUser(User info)
{
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- if (password == null)
- throw new ArgumentNullException(nameof(password));
- CheckUsernameFormat(username);
+ if (info == null)
+ throw new ArgumentNullException(nameof(info));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ if (info.Username == null)
+ throw new ArgumentException(ExceptionUsernameNull, nameof(info));
+ CheckUsernameFormat(info.Username, nameof(info));
- if (user == null)
- {
- var newUser = new User
- {
- Name = username,
- EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = UserRoleConvert.ToString(administrator),
- Avatar = null
- };
- await _databaseContext.AddAsync(newUser);
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate,
- ("Id", newUser.Id), ("Username", username), ("Administrator", administrator)));
- return PutResult.Create;
- }
+ if (info.Password == null)
+ throw new ArgumentException(ExceptionPasswordNull, nameof(info));
+ CheckPasswordFormat(info.Password, nameof(info));
+
+ if (info.Nickname != null)
+ CheckNicknameFormat(info.Nickname, nameof(info));
+
+ var username = info.Username;
+
+ var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+ if (conflict)
+ ThrowUsernameConflict();
- user.EncryptedPassword = _passwordService.HashPassword(password);
- user.RoleString = UserRoleConvert.ToString(administrator);
- user.Version += 1;
+ var administrator = info.Administrator ?? false;
+ var password = info.Password;
+ var nickname = info.Nickname;
+
+ var newEntity = new UserEntity
+ {
+ Username = username,
+ Password = _passwordService.HashPassword(password),
+ Roles = UserRoleConvert.ToString(administrator),
+ Nickname = nickname,
+ Version = 1
+ };
+ _databaseContext.Users.Add(newEntity);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
- ("Id", user.Id), ("Username", username), ("Administrator", administrator)));
- //clear cache
- RemoveCache(user.Id);
+ _logger.LogInformation(Log.Format(LogDatabaseCreate,
+ ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator)));
- return PutResult.Modify;
+ return CreateUserFromEntity(newEntity);
}
- public async Task PatchUser(string username, string? password, bool? administrator)
+ private void ValidateModifyUserInfo(User? info)
{
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username);
+ if (info != null)
+ {
+ if (info.Username != null)
+ CheckUsernameFormat(info.Username, nameof(info));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
- if (user == null)
- throw new UserNotExistException(username);
+ if (info.Password != null)
+ CheckPasswordFormat(info.Password, nameof(info));
- if (password != null)
- {
- user.EncryptedPassword = _passwordService.HashPassword(password);
+ if (info.Nickname != null)
+ CheckNicknameFormat(info.Nickname, nameof(info));
}
+ }
- if (administrator != null)
+ private async Task UpdateUserEntity(UserEntity entity, User? info)
+ {
+ if (info != null)
{
- user.RoleString = UserRoleConvert.ToString(administrator.Value);
+ var username = info.Username;
+ if (username != null)
+ {
+ var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+ if (conflict)
+ ThrowUsernameConflict();
+
+ entity.Username = username;
+ }
+
+ var password = info.Password;
+ if (password != null)
+ {
+ entity.Password = _passwordService.HashPassword(password);
+ entity.Version += 1;
+ }
+
+ var administrator = info.Administrator;
+ if (administrator.HasValue)
+ {
+ entity.Roles = UserRoleConvert.ToString(administrator.Value);
+ }
+
+ var nickname = info.Nickname;
+ if (nickname != null)
+ {
+ entity.Nickname = nickname;
+ }
}
+ }
+
+
+ public async Task<User> ModifyUser(long id, User? info)
+ {
+ ValidateModifyUserInfo(info);
+
+ var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ if (entity == null)
+ throw new UserNotExistException(id);
+
+ await UpdateUserEntity(entity, info);
- user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id));
+ _logger.LogInformation(LogDatabaseUpdate, ("Id", id));
- //clear cache
- RemoveCache(user.Id);
+ return CreateUserFromEntity(entity);
}
- public async Task DeleteUser(string username)
+ public async Task<User> ModifyUser(string username, User? info)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username);
+ CheckUsernameFormat(username, nameof(username));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
- if (user == null)
+ ValidateModifyUserInfo(info);
+
+ var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
+ if (entity == null)
throw new UserNotExistException(username);
- _databaseContext.Users.Remove(user);
+ await UpdateUserEntity(entity, info);
+
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove,
- ("Id", user.Id)));
+ _logger.LogInformation(LogDatabaseUpdate, ("Username", username));
+
+ return CreateUserFromEntity(entity);
+ }
+
+ public async Task<bool> DeleteUser(long id)
+ {
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ if (user == null)
+ return false;
- //clear cache
- RemoveCache(user.Id);
+ _databaseContext.Users.Remove(user);
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username)));
+ return true;
}
- public async Task ChangePassword(string username, string oldPassword, string newPassword)
+ public async Task<bool> DeleteUser(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
- if (oldPassword == null)
- throw new ArgumentNullException(nameof(oldPassword));
- if (newPassword == null)
- throw new ArgumentNullException(nameof(newPassword));
- CheckUsernameFormat(username);
+ CheckUsernameFormat(username, nameof(username));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
if (user == null)
- throw new UserNotExistException(username);
-
- var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword);
- if (!verifyResult)
- throw new BadPasswordException(oldPassword);
+ return false;
- user.EncryptedPassword = _passwordService.HashPassword(newPassword);
- user.Version += 1;
+ _databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
- ("Id", user.Id), ("Operation", "Change password")));
- //clear cache
- RemoveCache(user.Id);
+ _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username)));
+ return true;
}
- public async Task ChangeUsername(string oldUsername, string newUsername)
+ public async Task ChangePassword(long id, string oldPassword, string newPassword)
{
- if (oldUsername == null)
- throw new ArgumentNullException(nameof(oldUsername));
- if (newUsername == null)
- throw new ArgumentNullException(nameof(newUsername));
- CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat);
- CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat);
-
- var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync();
- if (user == null)
- throw new UserNotExistException(oldUsername);
+ if (oldPassword == null)
+ throw new ArgumentNullException(nameof(oldPassword));
+ if (newPassword == null)
+ throw new ArgumentNullException(nameof(newPassword));
+ CheckPasswordFormat(oldPassword, nameof(oldPassword));
+ CheckPasswordFormat(newPassword, nameof(newPassword));
+
+ var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
- var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync();
- if (conflictUser != null)
- throw new UsernameConfictException(newUsername);
+ if (entity == null)
+ throw new UserNotExistException(id);
+
+ if (!_passwordService.VerifyPassword(entity.Password, oldPassword))
+ throw new BadPasswordException(oldPassword);
- user.Name = newUsername;
- user.Version += 1;
+ entity.Password = _passwordService.HashPassword(newPassword);
+ entity.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
- ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername)));
- RemoveCache(user.Id);
+ _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
}
}
}
diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs
new file mode 100644
index 00000000..ed0bae1a
--- /dev/null
+++ b/Timeline/Services/UserTokenException.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class UserTokenException : Exception
+ {
+ public UserTokenException() { }
+ public UserTokenException(string message) : base(message) { }
+ public UserTokenException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenException(string token, string message) : base(message) { Token = token; }
+ public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; }
+ protected UserTokenException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Token { get; private set; } = "";
+ }
+
+
+ [Serializable]
+ public class UserTokenTimeExpireException : UserTokenException
+ {
+ public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { }
+ public UserTokenTimeExpireException(string message) : base(message) { }
+ public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ protected UserTokenTimeExpireException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public DateTime ExpireTime { get; private set; } = default;
+
+ public DateTime VerifyTime { get; private set; } = default;
+ }
+
+ [Serializable]
+ public class UserTokenBadVersionException : UserTokenException
+ {
+ public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { }
+ public UserTokenBadVersionException(string message) : base(message) { }
+ public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ protected UserTokenBadVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public long TokenVersion { get; set; }
+
+ public long RequiredVersion { get; set; }
+ }
+
+ [Serializable]
+ public class UserTokenBadFormatException : UserTokenException
+ {
+ public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token, string message) : base(token, message) { }
+ public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { }
+ public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { }
+ protected UserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs
new file mode 100644
index 00000000..3e9ef3d4
--- /dev/null
+++ b/Timeline/Services/UserTokenManager.cs
@@ -0,0 +1,93 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+using Timeline.Models;
+
+namespace Timeline.Services
+{
+ public class UserTokenCreateResult
+ {
+ public string Token { get; set; } = default!;
+ public User User { get; set; } = default!;
+ }
+
+ public interface IUserTokenManager
+ {
+ /// <summary>
+ /// Try to create a token for given username and password.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <param name="password">The password.</param>
+ /// <param name="expireAt">The expire time of the token.</param>
+ /// <returns>The created token and the user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with <paramref name="username"/> does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when <paramref name="password"/> is wrong.</exception>
+ public Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null);
+
+ /// <summary>
+ /// Verify a token and get the saved user info. This also check the database for existence of the user.
+ /// </summary>
+ /// <param name="token">The token.</param>
+ /// <returns>The user stored in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="UserTokenTimeExpireException">Thrown when the token is expired.</exception>
+ /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
+ public Task<User> VerifyToken(string token);
+ }
+
+ public class UserTokenManager : IUserTokenManager
+ {
+ private readonly ILogger<UserTokenManager> _logger;
+ private readonly IUserService _userService;
+ private readonly IUserTokenService _userTokenService;
+ private readonly IClock _clock;
+
+ public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
+ {
+ _logger = logger;
+ _userService = userService;
+ _userTokenService = userTokenService;
+ _clock = clock;
+ }
+
+ public async Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ var user = await _userService.VerifyCredential(username, password);
+ var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt });
+
+ return new UserTokenCreateResult { Token = token, User = user };
+ }
+
+
+ public async Task<User> VerifyToken(string token)
+ {
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+
+ var tokenInfo = _userTokenService.VerifyToken(token);
+
+ if (tokenInfo.ExpireAt.HasValue)
+ {
+ var currentTime = _clock.GetCurrentTime();
+ if (tokenInfo.ExpireAt < currentTime)
+ throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime);
+ }
+
+ var user = await _userService.GetUserById(tokenInfo.Id);
+
+ if (tokenInfo.Version < user.Version)
+ throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value);
+
+ return user;
+ }
+ }
+}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/UserTokenService.cs
index bf92966a..cf7286f4 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/UserTokenService.cs
@@ -9,50 +9,59 @@ using Timeline.Configs;
namespace Timeline.Services
{
- public class TokenInfo
+ public class UserTokenInfo
{
public long Id { get; set; }
public long Version { get; set; }
+ public DateTime? ExpireAt { get; set; }
}
- public interface IJwtService
+ public interface IUserTokenService
{
/// <summary>
- /// Create a JWT token for a given token info.
+ /// Create a token for a given token info.
/// </summary>
/// <param name="tokenInfo">The info to generate token.</param>
- /// <param name="expires">The expire time. If null then use current time with offset in config.</param>
/// <returns>Return the generated token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
- string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null);
+ string GenerateToken(UserTokenInfo tokenInfo);
/// <summary>
- /// Verify a JWT token.
- /// Return null is <paramref name="token"/> is null.
+ /// Verify a token and get the saved info.
/// </summary>
- /// <param name="token">The token string to verify.</param>
- /// <returns>Return the saved info in token.</returns>
+ /// <param name="token">The token to verify.</param>
+ /// <returns>The saved info in token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtVerifyException">Thrown when the token is invalid.</exception>
- TokenInfo VerifyJwtToken(string token);
-
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <remarks>
+ /// If this method throw <see cref="UserTokenBadFormatException"/>, it usually means the token is not created by this service.
+ /// </remarks>
+ UserTokenInfo VerifyToken(string token);
}
- public class JwtService : IJwtService
+ public class JwtUserTokenService : IUserTokenService
{
private const string VersionClaimType = "timeline_version";
private readonly IOptionsMonitor<JwtConfig> _jwtConfig;
- private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
private readonly IClock _clock;
- public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
+ private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private SymmetricSecurityKey _tokenSecurityKey;
+
+ public JwtUserTokenService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
{
_jwtConfig = jwtConfig;
_clock = clock;
+
+ _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.CurrentValue.SigningKey));
+ jwtConfig.OnChange(config =>
+ {
+ _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey));
+ });
}
- public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
+ public string GenerateToken(UserTokenInfo tokenInfo)
{
if (tokenInfo == null)
throw new ArgumentNullException(nameof(tokenInfo));
@@ -71,7 +80,7 @@ namespace Timeline.Services
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384),
IssuedAt = _clock.GetCurrentTime(),
- Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
+ Expires = tokenInfo.ExpireAt.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass.
};
@@ -82,7 +91,7 @@ namespace Timeline.Services
}
- public TokenInfo VerifyJwtToken(string token)
+ public UserTokenInfo VerifyToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
@@ -95,37 +104,42 @@ namespace Timeline.Services
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
- ValidateLifetime = true,
+ ValidateLifetime = false,
ValidIssuer = config.Issuer,
ValidAudience = config.Audience,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
- }, out _);
+ IssuerSigningKey = _tokenSecurityKey
+ }, out var t);
var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (idClaim == null)
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim);
if (!long.TryParse(idClaim, out var id))
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat);
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim);
if (!long.TryParse(versionClaim, out var version))
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat);
+
+ var decodedToken = (JwtSecurityToken)t;
+ var exp = decodedToken.Payload.Exp;
+ DateTime? expireAt = null;
+ if (exp.HasValue)
+ {
+ expireAt = EpochTime.DateTime(exp.Value);
+ }
- return new TokenInfo
+ return new UserTokenInfo
{
Id = id,
- Version = version
+ Version = version,
+ ExpireAt = expireAt
};
}
- catch (SecurityTokenExpiredException e)
- {
- throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired);
- }
- catch (Exception e)
+ catch (Exception e) when (e is SecurityTokenException || e is ArgumentException)
{
- throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e);
}
}
}
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs
deleted file mode 100644
index d82bf962..00000000
--- a/Timeline/Services/UsernameBadFormatException.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Thrown when username is of bad format.
- /// </summary>
- [Serializable]
- public class UsernameBadFormatException : Exception
- {
- public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { }
- public UsernameBadFormatException(string username) : this() { Username = username; }
- public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; }
-
- public UsernameBadFormatException(string username, string message) : base(message) { Username = username; }
- public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; }
-
- protected UsernameBadFormatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// Username of bad format.
- /// </summary>
- public string? Username { get; private set; }
- }
-}
diff --git a/Timeline/Services/UsernameConfictException.cs b/Timeline/Services/UsernameConfictException.cs
deleted file mode 100644
index fde1eda6..00000000
--- a/Timeline/Services/UsernameConfictException.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using Timeline.Helpers;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Thrown when the user already exists.
- /// </summary>
- [Serializable]
- public class UsernameConfictException : Exception
- {
- public UsernameConfictException() : base(Resources.Services.Exception.UsernameConfictException) { }
- public UsernameConfictException(string username) : base(Log.Format(Resources.Services.Exception.UsernameConfictException, ("Username", username))) { Username = username; }
- public UsernameConfictException(string username, string message) : base(message) { Username = username; }
- public UsernameConfictException(string message, Exception inner) : base(message, inner) { }
- protected UsernameConfictException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// The username that already exists.
- /// </summary>
- public string? Username { get; set; }
- }
-}