aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-10-24 20:15:58 +0800
committerGitHub <noreply@github.com>2019-10-24 20:15:58 +0800
commit4ab69665f26aaa59bad8684e6b801b4c4cf900cd (patch)
tree7ca5010a06829cc5fadea1ea17ae72d082fc344c /Timeline/Services
parent21de8da2feab19d3fbc392e71bf0dcec25ec8d6b (diff)
parent2bc4c701f9cdff1fdd11a5736c33a5818fbae3e9 (diff)
downloadtimeline-4ab69665f26aaa59bad8684e6b801b4c4cf900cd.tar.gz
timeline-4ab69665f26aaa59bad8684e6b801b4c4cf900cd.tar.bz2
timeline-4ab69665f26aaa59bad8684e6b801b4c4cf900cd.zip
Merge pull request #50 from crupest/refactor
Refactor : A Huge Step
Diffstat (limited to 'Timeline/Services')
-rw-r--r--Timeline/Services/AvatarFormatException.cs51
-rw-r--r--Timeline/Services/BadPasswordException.cs27
-rw-r--r--Timeline/Services/DatabaseExtensions.cs15
-rw-r--r--Timeline/Services/ETagGenerator.cs24
-rw-r--r--Timeline/Services/JwtBadVersionException.cs36
-rw-r--r--Timeline/Services/JwtService.cs70
-rw-r--r--Timeline/Services/JwtVerifyException.cs59
-rw-r--r--Timeline/Services/PasswordService.cs38
-rw-r--r--Timeline/Services/UserAvatarService.cs200
-rw-r--r--Timeline/Services/UserDetailService.cs135
-rw-r--r--Timeline/Services/UserNotExistException.cs41
-rw-r--r--Timeline/Services/UserService.cs245
-rw-r--r--Timeline/Services/UsernameBadFormatException.cs27
-rw-r--r--Timeline/Services/UsernameConfictException.cs25
14 files changed, 499 insertions, 494 deletions
diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs
new file mode 100644
index 00000000..788eabb2
--- /dev/null
+++ b/Timeline/Services/AvatarFormatException.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Thrown when avatar is of bad format.
+ /// </summary>
+ [Serializable]
+ public class AvatarFormatException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ BadSize
+ }
+
+ public AvatarFormatException() : base(MakeMessage(null)) { }
+ public AvatarFormatException(string message) : base(message) { }
+ public AvatarFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; }
+ public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; }
+
+ protected AvatarFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ private static string MakeMessage(ErrorReason? reason) =>
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch
+ {
+ ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode,
+ ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat,
+ ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize,
+ _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError
+ });
+
+ public ErrorReason? Error { get; set; }
+ public Avatar? Avatar { get; set; }
+ }
+}
diff --git a/Timeline/Services/BadPasswordException.cs b/Timeline/Services/BadPasswordException.cs
new file mode 100644
index 00000000..ee8a42db
--- /dev/null
+++ b/Timeline/Services/BadPasswordException.cs
@@ -0,0 +1,27 @@
+using System;
+using Timeline.Helpers;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class BadPasswordException : Exception
+ {
+ public BadPasswordException() : base(Resources.Services.Exception.UserNotExistException) { }
+ public BadPasswordException(string message, Exception inner) : base(message, inner) { }
+
+ public BadPasswordException(string badPassword)
+ : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Bad Password", badPassword)))
+ {
+ Password = badPassword;
+ }
+
+ protected BadPasswordException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ /// <summary>
+ /// The wrong password.
+ /// </summary>
+ public string? Password { get; set; }
+ }
+}
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs
index a37cf05b..62b22f00 100644
--- a/Timeline/Services/DatabaseExtensions.cs
+++ b/Timeline/Services/DatabaseExtensions.cs
@@ -4,22 +4,27 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
- public static class DatabaseExtensions
+ internal static class DatabaseExtensions
{
/// <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="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <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>
- public static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string username)
+ internal static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, UsernameValidator validator, string username)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ var (result, messageGenerator) = validator.Validate(username);
+ if (!result)
+ throw new UsernameBadFormatException(username, messageGenerator(null));
var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
if (userId == 0)
diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs
index e2abebdc..d328ea20 100644
--- a/Timeline/Services/ETagGenerator.cs
+++ b/Timeline/Services/ETagGenerator.cs
@@ -1,33 +1,45 @@
using System;
using System.Security.Cryptography;
+using System.Threading.Tasks;
namespace Timeline.Services
{
public interface IETagGenerator
{
- string Generate(byte[] source);
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
+ Task<string> Generate(byte[] source);
}
- public class ETagGenerator : IETagGenerator, IDisposable
+ public sealed class ETagGenerator : IETagGenerator, IDisposable
{
private readonly SHA1 _sha1;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")]
public ETagGenerator()
{
_sha1 = SHA1.Create();
}
- public string Generate(byte[] source)
+ public Task<string> Generate(byte[] source)
{
- if (source == null || source.Length == 0)
- throw new ArgumentException("Source is null or empty.", nameof(source));
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
- return Convert.ToBase64String(_sha1.ComputeHash(source));
+ return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source)));
}
+ private bool _disposed = false; // To detect redundant calls
+
public void Dispose()
{
+ if (_disposed) return;
_sha1.Dispose();
+ _disposed = true;
}
}
}
diff --git a/Timeline/Services/JwtBadVersionException.cs b/Timeline/Services/JwtBadVersionException.cs
new file mode 100644
index 00000000..4ce17710
--- /dev/null
+++ b/Timeline/Services/JwtBadVersionException.cs
@@ -0,0 +1,36 @@
+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/JwtService.cs b/Timeline/Services/JwtService.cs
index 350c5e80..bf92966a 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
+using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
@@ -14,57 +15,6 @@ namespace Timeline.Services
public long Version { get; set; }
}
- [Serializable]
- public class JwtTokenVerifyException : 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 JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
- public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; }
- public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
- public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
- protected JwtTokenVerifyException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public int ErrorCode { get; private set; }
-
- private static string GetErrorMessage(int errorCode)
- {
- switch (errorCode)
- {
- case ErrorCodes.Others:
- return "Uncommon error, see inner exception for more information.";
- case ErrorCodes.NoIdClaim:
- return "Id claim does not exist.";
- case ErrorCodes.IdClaimBadFormat:
- return "Id claim is not a number.";
- case ErrorCodes.NoVersionClaim:
- return "Version claim does not exist.";
- case ErrorCodes.VersionClaimBadFormat:
- return "Version claim is not a number";
- case ErrorCodes.Expired:
- return "Token is expired.";
- default:
- return "Unknown error code.";
- }
- }
- }
-
public interface IJwtService
{
/// <summary>
@@ -83,7 +33,7 @@ namespace Timeline.Services
/// <param name="token">The token string to verify.</param>
/// <returns>Return the saved info in token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtTokenVerifyException">Thrown when the token is invalid.</exception>
+ /// <exception cref="JwtVerifyException">Thrown when the token is invalid.</exception>
TokenInfo VerifyJwtToken(string token);
}
@@ -110,8 +60,8 @@ namespace Timeline.Services
var config = _jwtConfig.CurrentValue;
var identity = new ClaimsIdentity();
- identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64));
- identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64));
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
+ identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
var tokenDescriptor = new SecurityTokenDescriptor()
{
@@ -153,15 +103,15 @@ namespace Timeline.Services
var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (idClaim == null)
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim);
+ throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim);
if (!long.TryParse(idClaim, out var id))
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat);
+ throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat);
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim);
+ throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim);
if (!long.TryParse(versionClaim, out var version))
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat);
+ throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat);
return new TokenInfo
{
@@ -171,11 +121,11 @@ namespace Timeline.Services
}
catch (SecurityTokenExpiredException e)
{
- throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired);
+ throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired);
}
catch (Exception e)
{
- throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others);
+ throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others);
}
}
}
diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs
new file mode 100644
index 00000000..a915b51a
--- /dev/null
+++ b/Timeline/Services/JwtVerifyException.cs
@@ -0,0 +1,59 @@
+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/PasswordService.cs b/Timeline/Services/PasswordService.cs
index e09a1365..e04a861b 100644
--- a/Timeline/Services/PasswordService.cs
+++ b/Timeline/Services/PasswordService.cs
@@ -12,13 +12,23 @@ namespace Timeline.Services
[Serializable]
public class HashedPasswordBadFromatException : Exception
{
- public HashedPasswordBadFromatException(string hashedPassword, string message) : base(message) { HashedPassword = hashedPassword; }
- public HashedPasswordBadFromatException(string hashedPassword, string message, Exception inner) : base(message, inner) { HashedPassword = hashedPassword; }
+ private static string MakeMessage(string reason)
+ {
+ return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason;
+ }
+
+ public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { }
+
+ public HashedPasswordBadFromatException(string message) : base(message) { }
+ public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { }
+
+ public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; }
+ public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; }
protected HashedPasswordBadFromatException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
- public string HashedPassword { get; private set; }
+ public string? HashedPassword { get; set; }
}
public interface IPasswordService
@@ -140,22 +150,20 @@ namespace Timeline.Services
}
catch (FormatException e)
{
- throw new HashedPasswordBadFromatException(hashedPassword, "Not of valid base64 format. See inner exception.", e);
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e);
}
// read the format marker from the hashed password
if (decodedHashedPassword.Length == 0)
{
- throw new HashedPasswordBadFromatException(hashedPassword, "Decoded hashed password is of length 0.");
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0);
}
- switch (decodedHashedPassword[0])
- {
- case 0x01:
- return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword);
- default:
- throw new HashedPasswordBadFromatException(hashedPassword, "Unknown format marker.");
- }
+ return (decodedHashedPassword[0]) switch
+ {
+ 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword),
+ _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker),
+ };
}
private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString)
@@ -170,7 +178,7 @@ namespace Timeline.Services
// Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Salt length < 128 bits.");
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort);
}
byte[] salt = new byte[saltLength];
Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
@@ -179,7 +187,7 @@ namespace Timeline.Services
int subkeyLength = hashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Subkey length < 128 bits.");
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort);
}
byte[] expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
@@ -193,7 +201,7 @@ namespace Timeline.Services
// This should never occur except in the case of a malformed payload, where
// we might go off the end of the array. Regardless, a malformed payload
// implies verification failed.
- throw new HashedPasswordBadFromatException(hashedPasswordString, "See inner exception.", e);
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e);
}
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index ecec5a31..ff80003c 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -10,54 +10,25 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
public class Avatar
{
- public string Type { get; set; }
- public byte[] Data { get; set; }
+ public string Type { get; set; } = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")]
+ public byte[] Data { get; set; } = default!;
}
public class AvatarInfo
{
- public Avatar Avatar { get; set; }
+ public Avatar Avatar { get; set; } = default!;
public DateTime LastModified { get; set; }
}
/// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarDataException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- BadSize
- }
-
- public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; }
- public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; }
- protected AvatarDataException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public ErrorReason Error { get; set; }
- public Avatar Avatar { get; set; }
- }
-
- /// <summary>
/// Provider for default user avatar.
/// </summary>
/// <remarks>
@@ -83,7 +54,7 @@ namespace Timeline.Services
/// Validate a avatar's format and size info.
/// </summary>
/// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarDataException">Thrown when validation failed.</exception>
+ /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
Task Validate(Avatar avatar);
}
@@ -94,16 +65,18 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <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);
/// <summary>
- /// Get avatar of a user. If the user has no avatar, a default one is returned.
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <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);
@@ -112,38 +85,41 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentException">Throw if <paramref name="username"/> is null or empty.
- /// Or thrown if <paramref name="avatar"/> is not null but <see cref="Avatar.Type"/> is null or empty or <see cref="Avatar.Data"/> is null.</exception>
+ /// <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="AvatarDataException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar avatar);
+ /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ Task SetAvatar(string username, Avatar? avatar);
}
+ // TODO! : Make this configurable.
public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
{
- private readonly IWebHostEnvironment _environment;
-
private readonly IETagGenerator _eTagGenerator;
- private byte[] _cacheData;
+ private readonly string _avatarPath;
+
+ private byte[] _cacheData = default!;
private DateTime _cacheLastModified;
- private string _cacheETag;
+ private string _cacheETag = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")]
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
- _environment = environment;
+ _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
_eTagGenerator = eTagGenerator;
}
private async Task CheckAndInit()
{
- if (_cacheData != null)
- return;
-
- var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png");
- _cacheData = await File.ReadAllBytesAsync(path);
- _cacheLastModified = File.GetLastWriteTime(path);
- _cacheETag = _eTagGenerator.Generate(_cacheData);
+ var path = _avatarPath;
+ if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified)
+ {
+ _cacheData = await File.ReadAllBytesAsync(path);
+ _cacheLastModified = File.GetLastWriteTime(path);
+ _cacheETag = await _eTagGenerator.Generate(_cacheData);
+ }
}
public async Task<string> GetDefaultAvatarETag()
@@ -175,17 +151,15 @@ namespace Timeline.Services
{
try
{
- using (var image = Image.Load(avatar.Data, out IImageFormat format))
- {
- if (!format.MimeTypes.Contains(avatar.Type))
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one.");
- if (image.Width != image.Height)
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height.");
- }
+ using var image = Image.Load(avatar.Data, out IImageFormat format);
+ if (!format.MimeTypes.Contains(avatar.Type))
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat);
+ if (image.Width != image.Height)
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize);
}
catch (UnknownImageFormatException e)
{
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e);
}
});
}
@@ -203,25 +177,32 @@ namespace Timeline.Services
private readonly IETagGenerator _eTagGenerator;
+ private readonly UsernameValidator _usernameValidator;
+
+ private readonly IClock _clock;
+
public UserAvatarService(
ILogger<UserAvatarService> logger,
DatabaseContext database,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
IUserAvatarValidator avatarValidator,
- IETagGenerator eTagGenerator)
+ IETagGenerator eTagGenerator,
+ IClock clock)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
_eTagGenerator = eTagGenerator;
+ _usernameValidator = new UsernameValidator();
+ _clock = clock;
}
public async Task<string> GetAvatarETag(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag;
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
@@ -230,73 +211,88 @@ namespace Timeline.Services
public async Task<AvatarInfo> GetAvatar(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
- if ((avatar.Type == null) != (avatar.Data == null))
+ if (avatarEntity != null)
{
- _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not.");
- throw new DatabaseCorruptedException();
- }
+ if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null))
+ {
+ var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame;
+ _logger.LogCritical(message);
+ throw new DatabaseCorruptedException(message);
+ }
- if (avatar.Data == null)
- {
- var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
- defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified;
- return defaultAvatar;
- }
- else
- {
- return new AvatarInfo
+ if (avatarEntity.Data != null)
{
- Avatar = new Avatar
+ return new AvatarInfo
{
- Type = avatar.Type,
- Data = avatar.Data
- },
- LastModified = avatar.LastModified
- };
+ Avatar = new Avatar
+ {
+ Type = avatarEntity.Type!,
+ Data = avatarEntity.Data
+ },
+ LastModified = avatarEntity.LastModified
+ };
+ }
}
+ var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
+ if (avatarEntity != null)
+ defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified;
+ return defaultAvatar;
}
- public async Task SetAvatar(string username, Avatar avatar)
+ public async Task SetAvatar(string username, Avatar? avatar)
{
if (avatar != null)
{
- if (string.IsNullOrEmpty(avatar.Type))
- throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar));
if (avatar.Data == null)
- throw new ArgumentException("Data of avatar is null.", nameof(avatar));
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar));
+ if (string.IsNullOrEmpty(avatar.Type))
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNullOrEmpty, nameof(avatar));
}
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync();
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
if (avatar == null)
{
- if (avatarEntity.Data == null)
+ if (avatarEntity == null || avatarEntity.Data == null)
+ {
return;
+ }
else
{
avatarEntity.Data = null;
avatarEntity.Type = null;
avatarEntity.ETag = null;
- avatarEntity.LastModified = DateTime.Now;
+ avatarEntity.LastModified = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
else
{
await _avatarValidator.Validate(avatar);
- avatarEntity.Type = avatar.Type;
+ var create = avatarEntity == null;
+ if (create)
+ {
+ avatarEntity = new UserAvatar();
+ }
+ avatarEntity!.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
- avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data);
- avatarEntity.LastModified = DateTime.Now;
+ avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data);
+ avatarEntity.LastModified = _clock.GetCurrentTime();
+ avatarEntity.UserId = userId;
+ if (create)
+ {
+ _database.UserAvatars.Add(avatarEntity);
+ }
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(create ?
+ Resources.Services.UserAvatarService.LogCreateEntity
+ : Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
}
@@ -308,7 +304,7 @@ namespace Timeline.Services
services.TryAddTransient<IETagGenerator, ETagGenerator>();
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddSingleton<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddSingleton<IUserAvatarValidator, UserAvatarValidator>();
+ services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs
deleted file mode 100644
index 5e049435..00000000
--- a/Timeline/Services/UserDetailService.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Models;
-
-namespace Timeline.Services
-{
- public interface IUserDetailService
- {
- /// <summary>
- /// Get the nickname of user.
- /// </summary>
- /// <param name="username">The username to get nickname of.</param>
- /// <returns>The user's nickname. Null if not set.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task<string> GetUserNickname(string username);
-
- /// <summary>
- /// Get the detail of user.
- /// </summary>
- /// <param name="username">The username to get user detail of.</param>
- /// <returns>The user detail.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task<UserDetail> GetUserDetail(string username);
-
- /// <summary>
- /// Update the detail of user. This function does not do data check.
- /// </summary>
- /// <param name="username">The username to get user detail of.</param>
- /// <param name="detail">The detail to update. Can't be null. Any null member means not set.</param>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty or <paramref name="detail"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task UpdateUserDetail(string username, UserDetail detail);
- }
-
- public class UserDetailService : IUserDetailService
- {
- private readonly ILogger<UserDetailService> _logger;
-
- private readonly DatabaseContext _databaseContext;
-
- public UserDetailService(ILogger<UserDetailService> logger, DatabaseContext databaseContext)
- {
- _logger = logger;
- _databaseContext = databaseContext;
- }
-
- private async Task<UserDetailEntity> CreateEntity(long userId)
- {
- var entity = new UserDetailEntity()
- {
- UserId = userId
- };
- _databaseContext.UserDetails.Add(entity);
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation("An entity is created in user_details.");
- return entity;
- }
-
- // Check the existence of user detail entry
- private async Task<UserDetailEntity> CheckAndInit(long userId)
- {
- var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync();
- if (detail == null)
- {
- detail = await CreateEntity(userId);
- }
- return detail;
- }
-
- public async Task<string> GetUserNickname(string username)
- {
- var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
- var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).Select(e => new { e.Nickname }).SingleOrDefaultAsync();
- if (detail == null)
- {
- var entity = await CreateEntity(userId);
- return null;
- }
- else
- {
- var nickname = detail.Nickname;
- return string.IsNullOrEmpty(nickname) ? null : nickname;
- }
- }
-
- public async Task<UserDetail> GetUserDetail(string username)
- {
- var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
- var detailEntity = await CheckAndInit(userId);
- return UserDetail.From(detailEntity);
- }
-
- public async Task UpdateUserDetail(string username, UserDetail detail)
- {
- if (detail == null)
- throw new ArgumentNullException(nameof(detail));
-
- var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
- var detailEntity = await CheckAndInit(userId);
-
- if (detail.Nickname != null)
- detailEntity.Nickname = detail.Nickname;
-
- if (detail.QQ != null)
- detailEntity.QQ = detail.QQ;
-
- if (detail.Email != null)
- detailEntity.Email = detail.Email;
-
- if (detail.PhoneNumber != null)
- detailEntity.PhoneNumber = detail.PhoneNumber;
-
- if (detail.Description != null)
- detailEntity.Description = detail.Description;
-
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation("An entity is updated in user_details.");
- }
- }
-
- public static class UserDetailServiceCollectionExtensions
- {
- public static void AddUserDetailService(this IServiceCollection services)
- {
- services.AddScoped<IUserDetailService, UserDetailService>();
- }
- }
-}
diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs
new file mode 100644
index 00000000..c7317f56
--- /dev/null
+++ b/Timeline/Services/UserNotExistException.cs
@@ -0,0 +1,41 @@
+using System;
+using Timeline.Helpers;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// The user requested does not exist.
+ /// </summary>
+ [Serializable]
+ public class UserNotExistException : Exception
+ {
+ public UserNotExistException() : base(Resources.Services.Exception.UserNotExistException) { }
+ public UserNotExistException(string message, Exception inner) : base(message, inner) { }
+
+ public UserNotExistException(string username)
+ : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Username", username)))
+ {
+ Username = username;
+ }
+
+ public UserNotExistException(long id)
+ : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Id", id)))
+ {
+ Id = id;
+ }
+
+ protected UserNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ /// <summary>
+ /// The username of the user that does not exist.
+ /// </summary>
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// The id of the user that does not exist.
+ /// </summary>
+ public long? Id { get; set; }
+ }
+}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 347b8cbb..8f354fc7 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -5,140 +5,16 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
-using static Timeline.Helpers.MyLogHelper;
-using static Timeline.Models.UserUtility;
namespace Timeline.Services
{
public class CreateTokenResult
{
- public string Token { get; set; }
- public UserInfo User { get; set; }
- }
-
- [Serializable]
- public class UserNotExistException : Exception
- {
- private const string message = "The user does not exist.";
-
- public UserNotExistException(string username)
- : base(FormatLogMessage(message, Pair("Username", username)))
- {
- Username = username;
- }
-
- public UserNotExistException(long id)
- : base(FormatLogMessage(message, Pair("Id", id)))
- {
- Id = id;
- }
-
- public UserNotExistException(string message, Exception inner) : base(message, inner) { }
-
- protected UserNotExistException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// The username that does not exist. May be null then <see cref="Id"/> is not null.
- /// </summary>
- public string Username { get; private set; }
-
- /// <summary>
- /// The id that does not exist. May be null then <see cref="Username"/> is not null.
- /// </summary>
- public long? Id { get; private set; }
- }
-
- [Serializable]
- public class BadPasswordException : Exception
- {
- public BadPasswordException(string badPassword)
- : base(FormatLogMessage("Password is wrong.", Pair("Bad Password", badPassword)))
- {
- Password = badPassword;
- }
-
- public BadPasswordException(string message, Exception inner) : base(message, inner) { }
-
- protected BadPasswordException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// The wrong password.
- /// </summary>
- public string Password { get; private set; }
- }
-
-
- [Serializable]
- public class BadTokenVersionException : Exception
- {
- public BadTokenVersionException(long tokenVersion, long requiredVersion)
- : base(FormatLogMessage("Token version is expired.",
- Pair("Token Version", tokenVersion),
- Pair("Required Version", requiredVersion)))
- {
- TokenVersion = tokenVersion;
- RequiredVersion = requiredVersion;
- }
-
- public BadTokenVersionException(string message, Exception inner) : base(message, inner) { }
-
- protected BadTokenVersionException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// The version in the token.
- /// </summary>
- public long TokenVersion { get; private set; }
-
- /// <summary>
- /// The version required.
- /// </summary>
- public long RequiredVersion { get; private set; }
- }
-
- /// <summary>
- /// Thrown when username is of bad format.
- /// </summary>
- [Serializable]
- public class UsernameBadFormatException : Exception
- {
- 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; }
- }
-
-
- /// <summary>
- /// Thrown when the user already exists.
- /// </summary>
- [Serializable]
- public class UserAlreadyExistException : Exception
- {
- public UserAlreadyExistException(string username) : base($"User {username} already exists.") { Username = username; }
- public UserAlreadyExistException(string username, string message) : base(message) { Username = username; }
- public UserAlreadyExistException(string message, Exception inner) : base(message, inner) { }
- protected UserAlreadyExistException(
- 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; }
+ public string Token { get; set; } = default!;
+ public UserInfo User { get; set; } = default!;
}
public interface IUserService
@@ -152,6 +28,7 @@ namespace Timeline.Services
/// <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>
/// <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="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);
@@ -163,9 +40,8 @@ namespace Timeline.Services
/// <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="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</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>
- /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
Task<UserInfo> VerifyToken(string token);
/// <summary>
@@ -173,6 +49,8 @@ namespace Timeline.Services
/// </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>
+ /// <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);
/// <summary>
@@ -188,10 +66,12 @@ namespace Timeline.Services
/// <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.Created"/> if a new user is created.
- /// Return <see cref="PutResult.Modified"/> if a existing user is modified.</returns>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <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);
/// <summary>
@@ -203,14 +83,16 @@ namespace Timeline.Services
/// <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);
+ Task PatchUser(string username, string? password, bool? administrator);
/// <summary>
/// Delete a user of given username.
/// </summary>
/// <param name="username">Username of thet user to delete. Can't be null.</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 DeleteUser(string username);
@@ -221,6 +103,7 @@ namespace Timeline.Services
/// <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>
/// <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);
@@ -230,16 +113,16 @@ namespace Timeline.Services
/// </summary>
/// <param name="oldUsername">The user's old username.</param>
/// <param name="newUsername">The new username.</param>
- /// <exception cref="ArgumentException">Thrown if <paramref name="oldUsername"/> or <paramref name="newUsername"/> is null or empty.</exception>
+ /// <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 new username is not accepted because of bad format.</exception>
- /// <exception cref="UserAlreadyExistException">Thrown if user with the new username already exists.</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; }
+ public string Username { get; set; } = default!;
public bool Administrator { get; set; }
public long Version { get; set; }
@@ -272,13 +155,25 @@ namespace Timeline.Services
_usernameValidator = new UsernameValidator();
}
- private string GenerateCacheKeyByUserId(long id) => $"user:{id}";
+ private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
private void RemoveCache(long id)
{
var key = GenerateCacheKeyByUserId(id);
_memoryCache.Remove(key);
- _logger.LogInformation(FormatLogMessage("A cache entry is removed.", Pair("Key", key)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key)));
+ }
+
+ private void CheckUsernameFormat(string username, string? message = null)
+ {
+ var (result, messageGenerator) = _usernameValidator.Validate(username);
+ if (!result)
+ {
+ if (message == null)
+ throw new UsernameBadFormatException(username, messageGenerator(null));
+ else
+ throw new UsernameBadFormatException(username, message + messageGenerator(null));
+ }
}
public async Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires)
@@ -287,6 +182,7 @@ namespace Timeline.Services
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();
@@ -306,7 +202,7 @@ namespace Timeline.Services
return new CreateTokenResult
{
Token = token,
- User = CreateUserInfo(user)
+ User = UserConvert.CreateUserInfo(user)
};
}
@@ -329,29 +225,33 @@ namespace Timeline.Services
throw new UserNotExistException(id);
// create cache
- cache = CreateUserCache(user);
+ cache = UserConvert.CreateUserCache(user);
_memoryCache.CreateEntry(key).SetValue(cache);
- _logger.LogInformation(FormatLogMessage("A cache entry is created.", Pair("Key", key)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
}
if (tokenInfo.Version != cache.Version)
- throw new BadTokenVersionException(tokenInfo.Version, cache.Version);
+ throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion);
return cache.ToUserInfo();
}
public async Task<UserInfo> GetUser(string username)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
+
return await _databaseContext.Users
.Where(user => user.Name == username)
- .Select(user => CreateUserInfo(user))
+ .Select(user => UserConvert.CreateUserInfo(user))
.SingleOrDefaultAsync();
}
public async Task<UserInfo[]> ListUsers()
{
return await _databaseContext.Users
- .Select(user => CreateUserInfo(user))
+ .Select(user => UserConvert.CreateUserInfo(user))
.ToArrayAsync();
}
@@ -361,11 +261,7 @@ namespace Timeline.Services
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
-
- if (!_usernameValidator.Validate(username, out var message))
- {
- throw new UsernameBadFormatException(username, message);
- }
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
@@ -375,31 +271,34 @@ namespace Timeline.Services
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = IsAdminToRoleString(administrator),
- Avatar = UserAvatar.Create(DateTime.Now)
+ RoleString = UserRoleConvert.ToString(administrator),
+ Avatar = null
};
await _databaseContext.AddAsync(newUser);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A new user entry is added to the database.", Pair("Id", newUser.Id)));
- return PutResult.Created;
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate,
+ ("Id", newUser.Id), ("Username", username), ("Administrator", administrator)));
+ return PutResult.Create;
}
user.EncryptedPassword = _passwordService.HashPassword(password);
- user.RoleString = IsAdminToRoleString(administrator);
+ user.RoleString = UserRoleConvert.ToString(administrator);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Username", username), ("Administrator", administrator)));
//clear cache
RemoveCache(user.Id);
- return PutResult.Modified;
+ return PutResult.Modify;
}
- public async Task PatchUser(string username, string password, bool? administrator)
+ public async Task PatchUser(string username, string? password, bool? administrator)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -412,12 +311,12 @@ namespace Timeline.Services
if (administrator != null)
{
- user.RoleString = IsAdminToRoleString(administrator.Value);
+ user.RoleString = UserRoleConvert.ToString(administrator.Value);
}
user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id));
//clear cache
RemoveCache(user.Id);
@@ -427,6 +326,7 @@ namespace Timeline.Services
{
if (username == null)
throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -434,7 +334,8 @@ namespace Timeline.Services
_databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is removed from the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove,
+ ("Id", user.Id)));
//clear cache
RemoveCache(user.Id);
@@ -448,6 +349,7 @@ namespace Timeline.Services
throw new ArgumentNullException(nameof(oldPassword));
if (newPassword == null)
throw new ArgumentNullException(nameof(newPassword));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -460,19 +362,20 @@ namespace Timeline.Services
user.EncryptedPassword = _passwordService.HashPassword(newPassword);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Operation", "Change password")));
//clear cache
RemoveCache(user.Id);
}
public async Task ChangeUsername(string oldUsername, string newUsername)
{
- if (string.IsNullOrEmpty(oldUsername))
- throw new ArgumentException("Old username is null or empty", nameof(oldUsername));
- if (string.IsNullOrEmpty(newUsername))
- throw new ArgumentException("New username is null or empty", nameof(newUsername));
-
- if (!_usernameValidator.Validate(newUsername, out var message))
- throw new UsernameBadFormatException(newUsername, $"New username is of bad format. {message}");
+ 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)
@@ -480,13 +383,13 @@ namespace Timeline.Services
var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync();
if (conflictUser != null)
- throw new UserAlreadyExistException(newUsername);
+ throw new UsernameConfictException(newUsername);
user.Name = newUsername;
user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry changed name field.",
- Pair("Id", user.Id), Pair("Old Username", oldUsername), Pair("New Username", newUsername)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername)));
RemoveCache(user.Id);
}
}
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs
new file mode 100644
index 00000000..04354d22
--- /dev/null
+++ b/Timeline/Services/UsernameBadFormatException.cs
@@ -0,0 +1,27 @@
+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 message) : base(message) { }
+ public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ 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
new file mode 100644
index 00000000..fde1eda6
--- /dev/null
+++ b/Timeline/Services/UsernameConfictException.cs
@@ -0,0 +1,25 @@
+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; }
+ }
+}