From 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- Timeline/Services/BadPasswordException.cs | 27 - Timeline/Services/Clock.cs | 29 - Timeline/Services/DataManager.cs | 122 -- Timeline/Services/DatabaseBackupService.cs | 35 - Timeline/Services/DatabaseCorruptedException.cs | 15 - Timeline/Services/ETagGenerator.cs | 45 - Timeline/Services/EntityNames.cs | 14 - .../Services/Exceptions/EntityAlreadyExistError.cs | 63 -- .../Services/Exceptions/EntityNotExistError.cs | 55 - .../Services/Exceptions/ExceptionMessageHelper.cs | 13 - Timeline/Services/Exceptions/ImageException.cs | 57 - .../Exceptions/TimelineNotExistException.cs | 21 - .../Exceptions/TimelinePostNoDataException.cs | 15 - .../Exceptions/TimelinePostNotExistException.cs | 33 - .../Services/Exceptions/UserNotExistException.cs | 40 - Timeline/Services/ImageValidator.cs | 54 - .../Services/JwtUserTokenBadFormatException.cs | 48 - Timeline/Services/PasswordBadFormatException.cs | 27 - Timeline/Services/PasswordService.cs | 224 ---- Timeline/Services/PathProvider.cs | 42 - Timeline/Services/TimelineService.cs | 1166 -------------------- Timeline/Services/UserAvatarService.cs | 265 ----- Timeline/Services/UserDeleteService.cs | 69 -- Timeline/Services/UserRoleConvert.cs | 43 - Timeline/Services/UserService.cs | 437 -------- Timeline/Services/UserTokenException.cs | 68 -- Timeline/Services/UserTokenManager.cs | 97 -- Timeline/Services/UserTokenService.cs | 149 --- 28 files changed, 3273 deletions(-) delete mode 100644 Timeline/Services/BadPasswordException.cs delete mode 100644 Timeline/Services/Clock.cs delete mode 100644 Timeline/Services/DataManager.cs delete mode 100644 Timeline/Services/DatabaseBackupService.cs delete mode 100644 Timeline/Services/DatabaseCorruptedException.cs delete mode 100644 Timeline/Services/ETagGenerator.cs delete mode 100644 Timeline/Services/EntityNames.cs delete mode 100644 Timeline/Services/Exceptions/EntityAlreadyExistError.cs delete mode 100644 Timeline/Services/Exceptions/EntityNotExistError.cs delete mode 100644 Timeline/Services/Exceptions/ExceptionMessageHelper.cs delete mode 100644 Timeline/Services/Exceptions/ImageException.cs delete mode 100644 Timeline/Services/Exceptions/TimelineNotExistException.cs delete mode 100644 Timeline/Services/Exceptions/TimelinePostNoDataException.cs delete mode 100644 Timeline/Services/Exceptions/TimelinePostNotExistException.cs delete mode 100644 Timeline/Services/Exceptions/UserNotExistException.cs delete mode 100644 Timeline/Services/ImageValidator.cs delete mode 100644 Timeline/Services/JwtUserTokenBadFormatException.cs delete mode 100644 Timeline/Services/PasswordBadFormatException.cs delete mode 100644 Timeline/Services/PasswordService.cs delete mode 100644 Timeline/Services/PathProvider.cs delete mode 100644 Timeline/Services/TimelineService.cs delete mode 100644 Timeline/Services/UserAvatarService.cs delete mode 100644 Timeline/Services/UserDeleteService.cs delete mode 100644 Timeline/Services/UserRoleConvert.cs delete mode 100644 Timeline/Services/UserService.cs delete mode 100644 Timeline/Services/UserTokenException.cs delete mode 100644 Timeline/Services/UserTokenManager.cs delete mode 100644 Timeline/Services/UserTokenService.cs (limited to 'Timeline/Services') diff --git a/Timeline/Services/BadPasswordException.cs b/Timeline/Services/BadPasswordException.cs deleted file mode 100644 index f609371d..00000000 --- a/Timeline/Services/BadPasswordException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Timeline.Helpers; - -namespace Timeline.Services -{ - [Serializable] - public class BadPasswordException : Exception - { - public BadPasswordException() : base(Resources.Services.Exception.BadPasswordException) { } - public BadPasswordException(string message, Exception inner) : base(message, inner) { } - - public BadPasswordException(string badPassword) - : base(Log.Format(Resources.Services.Exception.BadPasswordException, ("Bad Password", badPassword))) - { - Password = badPassword; - } - - protected BadPasswordException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The wrong password. - /// - public string? Password { get; set; } - } -} diff --git a/Timeline/Services/Clock.cs b/Timeline/Services/Clock.cs deleted file mode 100644 index 4395edcd..00000000 --- a/Timeline/Services/Clock.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Convenient for unit test. - /// - public interface IClock - { - /// - /// Get current time. - /// - /// Current time. - DateTime GetCurrentTime(); - } - - public class Clock : IClock - { - public Clock() - { - - } - - public DateTime GetCurrentTime() - { - return DateTime.UtcNow; - } - } -} diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs deleted file mode 100644 index d447b0d5..00000000 --- a/Timeline/Services/DataManager.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; - -namespace Timeline.Services -{ - /// - /// A data manager controlling data. - /// - /// - /// Identical data will be saved as one copy and return the same tag. - /// Every data has a ref count. When data is retained, ref count increase. - /// When data is freed, ref count decease. If ref count is decreased - /// to 0, the data entry will be destroyed and no longer occupy space. - /// - public interface IDataManager - { - /// - /// Saves the data to a new entry if it does not exist, - /// increases its ref count and returns a tag to the entry. - /// - /// The data. Can't be null. - /// The tag of the created entry. - /// Thrown when is null. - public Task RetainEntry(byte[] data); - - /// - /// Decrease the the ref count of the entry. - /// Remove it if ref count is zero. - /// - /// The tag of the entry. - /// Thrown when is null. - /// - /// It's no-op if entry with tag does not exist. - /// - public Task FreeEntry(string tag); - - /// - /// Retrieve the entry with given tag. - /// - /// The tag of the entry. - /// The data of the entry. - /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); - } - - public class DataManager : IDataManager - { - private readonly DatabaseContext _database; - private readonly IETagGenerator _eTagGenerator; - - public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) - { - _database = database; - _eTagGenerator = eTagGenerator; - } - - public async Task RetainEntry(byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var tag = await _eTagGenerator.Generate(data); - - var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); - - if (entity == null) - { - entity = new DataEntity - { - Tag = tag, - Data = data, - Ref = 1 - }; - _database.Data.Add(entity); - } - else - { - entity.Ref += 1; - } - await _database.SaveChangesAsync(); - return tag; - } - - public async Task FreeEntry(string tag) - { - if (tag == null) - throw new ArgumentNullException(nameof(tag)); - - var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); - - if (entity != null) - { - if (entity.Ref == 1) - { - _database.Data.Remove(entity); - } - else - { - entity.Ref -= 1; - } - await _database.SaveChangesAsync(); - } - } - - public async Task GetEntry(string tag) - { - if (tag == null) - throw new ArgumentNullException(nameof(tag)); - - var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); - - if (entity == null) - throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); - - return entity.Data; - } - } -} diff --git a/Timeline/Services/DatabaseBackupService.cs b/Timeline/Services/DatabaseBackupService.cs deleted file mode 100644 index a76b2a0d..00000000 --- a/Timeline/Services/DatabaseBackupService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Globalization; -using System.IO; - -namespace Timeline.Services -{ - public interface IDatabaseBackupService - { - void BackupNow(); - } - - public class DatabaseBackupService : IDatabaseBackupService - { - private readonly IPathProvider _pathProvider; - private readonly IClock _clock; - - public DatabaseBackupService(IPathProvider pathProvider, IClock clock) - { - _pathProvider = pathProvider; - _clock = clock; - } - - public void BackupNow() - { - var databasePath = _pathProvider.GetDatabaseFilePath(); - if (File.Exists(databasePath)) - { - var backupDirPath = _pathProvider.GetDatabaseBackupDirectory(); - Directory.CreateDirectory(backupDirPath); - var fileName = _clock.GetCurrentTime().ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture); - var path = Path.Combine(backupDirPath, fileName); - File.Copy(databasePath, path); - } - } - } -} diff --git a/Timeline/Services/DatabaseCorruptedException.cs b/Timeline/Services/DatabaseCorruptedException.cs deleted file mode 100644 index 9988e0ad..00000000 --- a/Timeline/Services/DatabaseCorruptedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class DatabaseCorruptedException : Exception - { - public DatabaseCorruptedException() { } - public DatabaseCorruptedException(string message) : base(message) { } - public DatabaseCorruptedException(string message, Exception inner) : base(message, inner) { } - protected DatabaseCorruptedException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs deleted file mode 100644 index 4493e903..00000000 --- a/Timeline/Services/ETagGenerator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Threading.Tasks; - -namespace Timeline.Services -{ - public interface IETagGenerator - { - /// - /// Generate a etag for given source. - /// - /// The source data. - /// The generated etag. - /// Thrown if is null. - Task Generate(byte[] source); - } - - 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 Task Generate(byte[] source) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source))); - } - - private bool _disposed; // To detect redundant calls - - public void Dispose() - { - if (_disposed) return; - _sha1.Dispose(); - _disposed = true; - } - } -} diff --git a/Timeline/Services/EntityNames.cs b/Timeline/Services/EntityNames.cs deleted file mode 100644 index 0ce1de3b..00000000 --- a/Timeline/Services/EntityNames.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Timeline.Services -{ - public static class EntityNames - { - public const string User = "User"; - public const string Timeline = "Timeline"; - public const string TimelinePost = "TimelinePost"; - } -} diff --git a/Timeline/Services/Exceptions/EntityAlreadyExistError.cs b/Timeline/Services/Exceptions/EntityAlreadyExistError.cs deleted file mode 100644 index 7db2e860..00000000 --- a/Timeline/Services/Exceptions/EntityAlreadyExistError.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace Timeline.Services.Exceptions -{ - /// - /// Thrown when an entity is already exists. - /// - /// - /// For example, want to create a timeline but a timeline with the same name already exists. - /// - [Serializable] - public class EntityAlreadyExistException : Exception - { - private readonly string? _entityName; - - public EntityAlreadyExistException() : this(null, null, null, null) { } - - public EntityAlreadyExistException(string? entityName) : this(entityName, null) { } - - public EntityAlreadyExistException(string? entityName, Exception? inner) : this(entityName, null, null, null, inner) { } - - public EntityAlreadyExistException(string? entityName, object? entity = null) : this(entityName, null, entity, null, null) { } - public EntityAlreadyExistException(Type? entityType, object? entity = null) : this(null, entityType, entity, null, null) { } - public EntityAlreadyExistException(string? entityName, Type? entityType, object? entity = null, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) - { - _entityName = entityName; - EntityType = entityType; - Entity = entity; - } - - private static string MakeMessage(string? entityName, Type? entityType, string? message) - { - string? name = entityName ?? (entityType?.Name); - - var result = new StringBuilder(); - - if (name == null) - result.Append(Resources.Services.Exceptions.EntityAlreadyExistErrorDefault); - else - result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityAlreadyExistError, name); - - if (message != null) - { - result.Append(' '); - result.Append(message); - } - - return result.ToString(); - } - - protected EntityAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? EntityName => _entityName ?? (EntityType?.Name); - - public Type? EntityType { get; } - - public object? Entity { get; } - } -} diff --git a/Timeline/Services/Exceptions/EntityNotExistError.cs b/Timeline/Services/Exceptions/EntityNotExistError.cs deleted file mode 100644 index e79496d3..00000000 --- a/Timeline/Services/Exceptions/EntityNotExistError.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace Timeline.Services.Exceptions -{ - /// - /// Thrown when you want to get an entity that does not exist. - /// - /// - /// For example, you want to get a timeline with given name but it does not exist. - /// - [Serializable] - public class EntityNotExistException : Exception - { - public EntityNotExistException() : this(null, null, null, null) { } - public EntityNotExistException(string? entityName) : this(entityName, null, null, null) { } - public EntityNotExistException(Type? entityType) : this(null, entityType, null, null) { } - public EntityNotExistException(string? entityName, Exception? inner) : this(entityName, null, null, inner) { } - public EntityNotExistException(Type? entityType, Exception? inner) : this(null, entityType, null, inner) { } - public EntityNotExistException(string? entityName, Type? entityType, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) - { - EntityName = entityName; - EntityType = entityType; - } - - private static string MakeMessage(string? entityName, Type? entityType, string? message) - { - string? name = entityName ?? (entityType?.Name); - - var result = new StringBuilder(); - - if (name == null) - result.Append(Resources.Services.Exceptions.EntityNotExistErrorDefault); - else - result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityNotExistError, name); - - if (message != null) - { - result.Append(' '); - result.Append(message); - } - - return result.ToString(); - } - - protected EntityNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? EntityName { get; } - - public Type? EntityType { get; } - } -} diff --git a/Timeline/Services/Exceptions/ExceptionMessageHelper.cs b/Timeline/Services/Exceptions/ExceptionMessageHelper.cs deleted file mode 100644 index be3c42a4..00000000 --- a/Timeline/Services/Exceptions/ExceptionMessageHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Timeline.Services.Exceptions -{ - public static class ExceptionMessageHelper - { - public static string AppendAdditionalMessage(this string origin, string? message) - { - if (message == null) - return origin; - else - return origin + " " + message; - } - } -} diff --git a/Timeline/Services/Exceptions/ImageException.cs b/Timeline/Services/Exceptions/ImageException.cs deleted file mode 100644 index 20dd48ae..00000000 --- a/Timeline/Services/Exceptions/ImageException.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class ImageException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not of required size. - /// - NotSquare, - /// - /// Other unknown errer. - /// - Unknown - } - - public ImageException() : this(null) { } - public ImageException(string? message) : this(message, null) { } - public ImageException(string? message, Exception? inner) : this(ErrorReason.Unknown, null, null, null, message, inner) { } - - public ImageException(ErrorReason error, byte[]? data, string? requestType = null, string? realType = null, string? message = null, Exception? inner = null) : base(MakeMessage(error).AppendAdditionalMessage(message), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } - - protected ImageException( - 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.Exceptions.ImageException, reason switch - { - ErrorReason.CantDecode => Resources.Services.Exceptions.ImageExceptionCantDecode, - ErrorReason.UnmatchedFormat => Resources.Services.Exceptions.ImageExceptionUnmatchedFormat, - ErrorReason.NotSquare => Resources.Services.Exceptions.ImageExceptionBadSize, - _ => Resources.Services.Exceptions.ImageExceptionUnknownError - }); - - public ErrorReason Error { get; } -#pragma warning disable CA1819 // Properties should not return arrays - public byte[]? ImageData { get; } -#pragma warning restore CA1819 // Properties should not return arrays - public string? RequestType { get; } - - // This field will be null if decoding failed. - public string? RealType { get; } - } -} diff --git a/Timeline/Services/Exceptions/TimelineNotExistException.cs b/Timeline/Services/Exceptions/TimelineNotExistException.cs deleted file mode 100644 index 70970b24..00000000 --- a/Timeline/Services/Exceptions/TimelineNotExistException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelineNotExistException : EntityNotExistException - { - public TimelineNotExistException() : this(null, null) { } - public TimelineNotExistException(string? timelineName) : this(timelineName, null) { } - public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { } - public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null) - : base(EntityNames.Timeline, null, string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.TimelineNotExistException, timelineName ?? "").AppendAdditionalMessage(message), inner) { TimelineName = timelineName; } - - protected TimelineNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? TimelineName { get; set; } - } -} diff --git a/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/Timeline/Services/Exceptions/TimelinePostNoDataException.cs deleted file mode 100644 index c4b6bf62..00000000 --- a/Timeline/Services/Exceptions/TimelinePostNoDataException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelinePostNoDataException : Exception - { - public TimelinePostNoDataException() : this(null, null) { } - public TimelinePostNoDataException(string? message) : this(message, null) { } - public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { } - protected TimelinePostNoDataException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs b/Timeline/Services/Exceptions/TimelinePostNotExistException.cs deleted file mode 100644 index f95dd410..00000000 --- a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelinePostNotExistException : EntityNotExistException - { - public TimelinePostNotExistException() : this(null, null, false, null, null) { } - [Obsolete("This has no meaning.")] - public TimelinePostNotExistException(string? message) : this(message, null) { } - [Obsolete("This has no meaning.")] - public TimelinePostNotExistException(string? message, Exception? inner) : this(null, null, false, message, inner) { } - protected TimelinePostNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public TimelinePostNotExistException(string? timelineName, long? id, bool isDelete, string? message = null, Exception? inner = null) : base(EntityNames.TimelinePost, null, MakeMessage(timelineName, id, isDelete).AppendAdditionalMessage(message), inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; } - - private static string MakeMessage(string? timelineName, long? id, bool isDelete) - { - return string.Format(CultureInfo.InvariantCulture, isDelete ? Resources.Services.Exceptions.TimelinePostNotExistExceptionDeleted : Resources.Services.Exceptions.TimelinePostNotExistException, timelineName ?? "", id); - } - - public string? TimelineName { get; set; } - public long? Id { get; set; } - - /// - /// True if the post is deleted. False if the post does not exist at all. - /// - public bool IsDelete { get; set; } - } -} diff --git a/Timeline/Services/Exceptions/UserNotExistException.cs b/Timeline/Services/Exceptions/UserNotExistException.cs deleted file mode 100644 index 7ef714df..00000000 --- a/Timeline/Services/Exceptions/UserNotExistException.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - /// - /// The user requested does not exist. - /// - [Serializable] - public class UserNotExistException : EntityNotExistException - { - public UserNotExistException() : this(null, null, null, null) { } - public UserNotExistException(string? username, Exception? inner) : this(username, null, null, inner) { } - - public UserNotExistException(string? username) : this(username, null, null, null) { } - - public UserNotExistException(long id) : this(null, id, null, null) { } - - public UserNotExistException(string? username, long? id, string? message, Exception? inner) : base(EntityNames.User, null, - string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.UserNotExistException, username ?? "", id).AppendAdditionalMessage(message), inner) - { - Username = username; - Id = id; - } - - protected UserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username of the user that does not exist. - /// - public string? Username { get; set; } - - /// - /// The id of the user that does not exist. - /// - public long? Id { get; set; } - } -} diff --git a/Timeline/Services/ImageValidator.cs b/Timeline/Services/ImageValidator.cs deleted file mode 100644 index 59424a7c..00000000 --- a/Timeline/Services/ImageValidator.cs +++ /dev/null @@ -1,54 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public interface IImageValidator - { - /// - /// Validate a image data. - /// - /// The data of the image. Can't be null. - /// If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check. - /// If true, image must be square. - /// The format. - /// Thrown when is null. - /// Thrown when image data can't be decoded or real type does not match request type or image is not square when required. - Task Validate(byte[] data, string? requestType = null, bool square = false); - } - - public class ImageValidator : IImageValidator - { - public ImageValidator() - { - } - - public async Task Validate(byte[] data, string? requestType = null, bool square = false) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var format = await Task.Run(() => - { - try - { - using var image = Image.Load(data, out IImageFormat format); - if (requestType != null && !format.MimeTypes.Contains(requestType)) - throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType); - if (square && image.Width != image.Height) - throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType); - return format; - } - catch (UnknownImageFormatException e) - { - throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e); - } - }); - return format; - } - } -} diff --git a/Timeline/Services/JwtUserTokenBadFormatException.cs b/Timeline/Services/JwtUserTokenBadFormatException.cs deleted file mode 100644 index c528c3e3..00000000 --- a/Timeline/Services/JwtUserTokenBadFormatException.cs +++ /dev/null @@ -1,48 +0,0 @@ -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/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs deleted file mode 100644 index 2029ebb4..00000000 --- a/Timeline/Services/PasswordBadFormatException.cs +++ /dev/null @@ -1,27 +0,0 @@ -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/PasswordService.cs b/Timeline/Services/PasswordService.cs deleted file mode 100644 index 8114a520..00000000 --- a/Timeline/Services/PasswordService.cs +++ /dev/null @@ -1,224 +0,0 @@ -using Microsoft.AspNetCore.Cryptography.KeyDerivation; -using System; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -namespace Timeline.Services -{ - /// - /// Hashed password is of bad format. - /// - /// - [Serializable] - public class HashedPasswordBadFromatException : Exception - { - 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; set; } - } - - public interface IPasswordService - { - /// - /// Hash a password. - /// - /// The password to hash. - /// A hashed representation of the supplied . - /// Thrown when is null. - string HashPassword(string password); - - /// - /// Verify whether the password fits into the hashed one. - /// - /// Usually you only need to check the returned bool value. - /// Catching usually is not necessary. - /// Because if your program logic is right and always call - /// and in pair, this exception will never be thrown. - /// A thrown one usually means the data you saved is corupted, which is a critical problem. - /// - /// The hashed password. - /// The password supplied for comparison. - /// True indicating password is right. Otherwise false. - /// Thrown when or is null. - /// Thrown when the hashed password is of bad format. - bool VerifyPassword(string hashedPassword, string providedPassword); - } - - /// - /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs - /// Remove V2 format and unnecessary format version check. - /// Remove configuration options. - /// Remove user related parts. - /// Change the exceptions. - /// - public class PasswordService : IPasswordService - { - /* ======================= - * HASHED PASSWORD FORMATS - * ======================= - * - * Version 3: - * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. - * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } - * (All UInt32s are stored big-endian.) - */ - - private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - - public PasswordService() - { - } - - // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private static bool ByteArraysEqual(byte[] a, byte[] b) - { - if (a == null && b == null) - { - return true; - } - if (a == null || b == null || a.Length != b.Length) - { - return false; - } - var areSame = true; - for (var i = 0; i < a.Length; i++) - { - areSame &= (a[i] == b[i]); - } - return areSame; - } - - public string HashPassword(string password) - { - if (password == null) - throw new ArgumentNullException(nameof(password)); - return Convert.ToBase64String(HashPasswordV3(password, _rng)); - } - - private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng) - { - return HashPasswordV3(password, rng, - prf: KeyDerivationPrf.HMACSHA256, - iterCount: 10000, - saltSize: 128 / 8, - numBytesRequested: 256 / 8); - } - - private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) - { - // Produce a version 3 (see comment above) text hash. - byte[] salt = new byte[saltSize]; - rng.GetBytes(salt); - byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); - - var outputBytes = new byte[13 + salt.Length + subkey.Length]; - outputBytes[0] = 0x01; // format marker - WriteNetworkByteOrder(outputBytes, 1, (uint)prf); - WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); - WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); - Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); - Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); - return outputBytes; - } - - public bool VerifyPassword(string hashedPassword, string providedPassword) - { - if (hashedPassword == null) - throw new ArgumentNullException(nameof(hashedPassword)); - if (providedPassword == null) - throw new ArgumentNullException(nameof(providedPassword)); - - byte[] decodedHashedPassword; - try - { - decodedHashedPassword = Convert.FromBase64String(hashedPassword); - } - catch (FormatException 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, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0); - } - - return (decodedHashedPassword[0]) switch - { - 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword), - _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker), - }; - } - - private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString) - { - try - { - // Read header information - KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); - int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); - int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); - - // Read the salt: must be >= 128 bits - if (saltLength < 128 / 8) - { - throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort); - } - byte[] salt = new byte[saltLength]; - Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); - - // Read the subkey (the rest of the payload): must be >= 128 bits - int subkeyLength = hashedPassword.Length - 13 - salt.Length; - if (subkeyLength < 128 / 8) - { - throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort); - } - byte[] expectedSubkey = new byte[subkeyLength]; - Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); - - // Hash the incoming password and verify it - byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); - return ByteArraysEqual(actualSubkey, expectedSubkey); - } - catch (Exception e) - { - // 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, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e); - } - } - - private static uint ReadNetworkByteOrder(byte[] buffer, int offset) - { - return ((uint)(buffer[offset + 0]) << 24) - | ((uint)(buffer[offset + 1]) << 16) - | ((uint)(buffer[offset + 2]) << 8) - | ((uint)(buffer[offset + 3])); - } - - private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) - { - buffer[offset + 0] = (byte)(value >> 24); - buffer[offset + 1] = (byte)(value >> 16); - buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)(value >> 0); - } - } -} diff --git a/Timeline/Services/PathProvider.cs b/Timeline/Services/PathProvider.cs deleted file mode 100644 index 1baba5c0..00000000 --- a/Timeline/Services/PathProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Configuration; -using System.IO; -using Timeline.Configs; - -namespace Timeline.Services -{ - public interface IPathProvider - { - public string GetWorkingDirectory(); - public string GetDatabaseFilePath(); - public string GetDatabaseBackupDirectory(); - } - - public class PathProvider : IPathProvider - { - private readonly IConfiguration _configuration; - - private readonly string _workingDirectory; - - - public PathProvider(IConfiguration configuration) - { - _configuration = configuration; - _workingDirectory = configuration.GetValue(ApplicationConfiguration.WorkDirKey) ?? ApplicationConfiguration.DefaultWorkDir; - } - - public string GetWorkingDirectory() - { - return _workingDirectory; - } - - public string GetDatabaseFilePath() - { - return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseFileName); - } - - public string GetDatabaseBackupDirectory() - { - return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseBackupDirectoryName); - } - } -} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs deleted file mode 100644 index 4bcae596..00000000 --- a/Timeline/Services/TimelineService.cs +++ /dev/null @@ -1,1166 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Validation; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Services.TimelineService; - -namespace Timeline.Services -{ - public static class TimelineHelper - { - public static string ExtractTimelineName(string name, out bool isPersonal) - { - if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) - { - isPersonal = true; - return name.Substring(1); - } - else - { - isPersonal = false; - return name; - } - } - } - - public enum TimelineUserRelationshipType - { - Own = 0b1, - Join = 0b10, - Default = Own | Join - } - - public class TimelineUserRelationship - { - public TimelineUserRelationship(TimelineUserRelationshipType type, long userId) - { - Type = type; - UserId = userId; - } - - public TimelineUserRelationshipType Type { get; set; } - public long UserId { get; set; } - } - - public class PostData : ICacheableData - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public string Type { get; set; } = default!; - public string ETag { get; set; } = default!; - public DateTime? LastModified { get; set; } // TODO: Why nullable? - } - - /// - /// This define the interface of both personal timeline and ordinary timeline. - /// - public interface ITimelineService - { - /// - /// Get the timeline last modified time (not include name change). - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineLastModifiedTime(string timelineName); - - /// - /// Get the timeline unique id. - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineUniqueId(string timelineName); - - /// - /// Get the timeline info. - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimeline(string timelineName); - - /// - /// Set the properties of a timeline. - /// - /// The name of the timeline. - /// The new properties. Null member means not to change. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - - /// - /// Get all the posts in the timeline. - /// - /// The name of the timeline. - /// The time that posts have been modified since. - /// Whether include deleted posts. - /// A list of all posts. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); - - /// - /// Get the etag of data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostDataETag(string timelineName, long postId); - - /// - /// Get the data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostData(string timelineName, long postId); - - /// - /// Create a new text post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The content text. - /// The time of the post. If null, then current time is used. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); - - /// - /// Create a new image post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The image data. - /// The time of the post. If null, then use current time. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); - - /// - /// Delete a post. - /// - /// The name of the timeline to delete post against. - /// The id of the post to delete. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already. - /// - /// First use to check the permission. - /// - Task DeletePost(string timelineName, long postId); - - /// - /// Delete all posts of the given user. Used when delete a user. - /// - /// The id of the user. - Task DeleteAllPostsOfUser(long userId); - - /// - /// Change member of timeline. - /// - /// The name of the timeline. - /// A list of usernames of members to add. May be null. - /// A list of usernames of members to remove. May be null. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when names in or is not a valid username. - /// Thrown when one of the user to change does not exist. - /// - /// Operating on a username that is of bad format or does not exist always throws. - /// Add a user that already is a member has no effects. - /// Remove a user that is not a member also has not effects. - /// Add and remove an identical user results in no effects. - /// More than one same usernames are regarded as one. - /// - Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove); - - /// - /// Check whether a user can manage(change timeline info, member, ...) a timeline. - /// - /// The name of the timeline. - /// The id of the user to check on. - /// True if the user can manage the timeline, otherwise false. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// This method does not check whether visitor is administrator. - /// Return false if user with user id does not exist. - /// - Task HasManagePermission(string timelineName, long userId); - - /// - /// Verify whether a visitor has the permission to read a timeline. - /// - /// The name of the timeline. - /// The id of the user to check on. Null means visitor without account. - /// True if can read, false if can't read. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// This method does not check whether visitor is administrator. - /// Return false if user with visitor id does not exist. - /// - Task HasReadPermission(string timelineName, long? visitorId); - - /// - /// Verify whether a user has the permission to modify a post. - /// - /// The name of the timeline. - /// The id of the post. - /// The id of the user to check on. - /// True if you want it to throw . Default false. - /// True if can modify, false if can't modify. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already and is true. - /// - /// Unless is true, this method should return true if the post does not exist. - /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. - /// 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. - /// - Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); - - /// - /// Verify whether a user is member of a timeline. - /// - /// The name of the timeline. - /// The id of user to check on. - /// True if it is a member, false if not. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// Timeline owner is also considered as a member. - /// Return false when user with user id does not exist. - /// - Task IsMemberOf(string timelineName, long userId); - - /// - /// Get all timelines including personal and ordinary timelines. - /// - /// Filter timelines related (own or is a member) to specific user. - /// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored. - /// The list of timelines. - /// - /// If user with related user id does not exist, empty list will be returned. - /// - Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); - - /// - /// Create a timeline. - /// - /// The name of the timeline. - /// The id of owner of the timeline. - /// The info of the new timeline. - /// Thrown when is null. - /// Thrown when timeline name is invalid. - /// Thrown when the timeline already exists. - /// Thrown when the owner user does not exist. - Task CreateTimeline(string timelineName, long ownerId); - - /// - /// Delete a timeline. - /// - /// The name of the timeline to delete. - /// Thrown when is null. - /// Thrown when timeline name is invalid. - /// Thrown when the timeline does not exist. - Task DeleteTimeline(string timelineName); - - /// - /// Change name of a timeline. - /// - /// The old timeline name. - /// The new timeline name. - /// The new timeline info. - /// Thrown when or is null. - /// Thrown when or is of invalid format. - /// Thrown when timeline does not exist. - /// Thrown when a timeline with new name already exists. - /// - /// You can only change name of general timeline. - /// - Task ChangeTimelineName(string oldTimelineName, string newTimelineName); - } - - public class TimelineService : ITimelineService - { - public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) - { - _logger = logger; - _database = database; - _dataManager = dataManager; - _userService = userService; - _imageValidator = imageValidator; - _clock = clock; - } - - private readonly ILogger _logger; - - private readonly DatabaseContext _database; - - private readonly IDataManager _dataManager; - - private readonly IUserService _userService; - - private readonly IImageValidator _imageValidator; - - private readonly IClock _clock; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - - private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); - - private void ValidateTimelineName(string name, string paramName) - { - if (!_timelineNameValidator.Validate(name, out var message)) - { - throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName); - } - } - - /// Remember to include Members when query. - private async Task MapTimelineFromEntity(TimelineEntity entity) - { - var owner = await _userService.GetUserById(entity.OwnerId); - - var members = new List(); - foreach (var memberEntity in entity.Members) - { - members.Add(await _userService.GetUserById(memberEntity.UserId)); - } - - var name = entity.Name ?? ("@" + owner.Username); - - return new Models.Timeline - { - UniqueID = entity.UniqueId, - Name = name, - NameLastModified = entity.NameLastModified, - Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, - Description = entity.Description ?? "", - Owner = owner, - Visibility = entity.Visibility, - Members = members, - CreateTime = entity.CreateTime, - LastModified = entity.LastModified - }; - } - - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) - { - User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(entity.AuthorId.Value) : null; - - ITimelinePostContent? content = null; - - if (entity.Content != null) - { - var type = entity.ContentType; - - content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; - } - - return new TimelinePost( - id: entity.LocalId, - author: author, - content: content, - time: entity.Time, - lastUpdated: entity.LastUpdated, - timelineName: timelineName - ); - } - - private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) - { - var currentTime = _clock.GetCurrentTime(); - - return new TimelineEntity - { - Name = name, - NameLastModified = currentTime, - OwnerId = ownerId, - Visibility = TimelineVisibility.Register, - CreateTime = currentTime, - LastModified = currentTime, - CurrentPostLocalId = 0, - Members = new List() - }; - } - - - - // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. - // - // This method will check the name format and if it is invalid, ArgumentException is thrown. - // - // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. - // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. - // - // It follows all timeline-related function common interface contracts. - private async Task FindTimelineId(string timelineName) - { - timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - - if (isPersonal) - { - long userId; - try - { - userId = await _userService.GetUserIdByUsername(timelineName); - } - catch (ArgumentException e) - { - throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); - } - catch (UserNotExistException e) - { - throw new TimelineNotExistException(timelineName, e); - } - - var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity != null) - { - return timelineEntity.Id; - } - else - { - var newTimelineEntity = CreateNewTimelineEntity(null, userId); - _database.Timelines.Add(newTimelineEntity); - await _database.SaveChangesAsync(); - - return newTimelineEntity.Id; - } - } - else - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - ValidateTimelineName(timelineName, nameof(timelineName)); - - var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity == null) - { - throw new TimelineNotExistException(timelineName); - } - else - { - return timelineEntity.Id; - } - } - } - - public async Task GetTimelineLastModifiedTime(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); - - return timelineEntity.LastModified; - } - - public async Task GetTimelineUniqueId(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); - - return timelineEntity.UniqueId; - } - - public async Task GetTimeline(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); - - return await MapTimelineFromEntity(timelineEntity); - } - - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) - { - modifiedSince = modifiedSince?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); - - if (!includeDeleted) - { - query = query.Where(p => p.Content != null); - } - - if (modifiedSince.HasValue) - { - query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); - } - - query = query.OrderBy(p => p.Time); - - var postEntities = await query.ToListAsync(); - - var posts = new List(); - foreach (var entity in postEntities) - { - posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); - } - return posts; - } - - public async Task GetPostDataETag(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - return tag; - } - - public async Task GetPostData(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - byte[] data; - - try - { - data = await _dataManager.GetEntry(tag); - } - catch (InvalidOperationException e) - { - throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); - } - - if (postEntity.ExtraContent == null) - { - _logger.LogWarning(LogGetDataNoFormat); - var format = Image.DetectFormat(data); - postEntity.ExtraContent = format.DefaultMimeType; - await _database.SaveChangesAsync(); - } - - return new PostData - { - Data = data, - Type = postEntity.ExtraContent, - ETag = tag, - LastModified = postEntity.LastUpdated - }; - } - - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUserById(authorId); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Text, - Content = text, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - - return new TimelinePost( - id: postEntity.LocalId, - content: new TextTimelinePostContent(text), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUserById(authorId); - - var imageFormat = await _imageValidator.Validate(data); - - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Image, - Content = tag, - ExtraContent = imageFormatText, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - return new TimelinePost( - id: postEntity.LocalId, - content: new ImageTimelinePostContent(tag), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task DeletePost(string timelineName, long id) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); - - if (post == null) - throw new TimelinePostNotExistException(timelineName, id, false); - - if (post.Content == null) - throw new TimelinePostNotExistException(timelineName, id, true); - - string? dataTag = null; - - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTag = post.Content; - } - - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task DeleteAllPostsOfUser(long userId) - { - var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); - - var now = _clock.GetCurrentTime(); - - var dataTags = new List(); - - foreach (var post in posts) - { - if (post.Content != null) - { - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTags.Add(post.Content); - } - post.Content = null; - } - post.LastUpdated = now; - } - - await _database.SaveChangesAsync(); - - foreach (var dataTag in dataTags) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (newProperties == null) - throw new ArgumentNullException(nameof(newProperties)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var changed = false; - - if (newProperties.Title != null) - { - changed = true; - timelineEntity.Title = newProperties.Title; - } - - if (newProperties.Description != null) - { - changed = true; - timelineEntity.Description = newProperties.Description; - } - - if (newProperties.Visibility.HasValue) - { - changed = true; - timelineEntity.Visibility = newProperties.Visibility.Value; - } - - if (changed) - { - var currentTime = _clock.GetCurrentTime(); - timelineEntity.LastModified = currentTime; - } - - await _database.SaveChangesAsync(); - } - - public async Task ChangeMember(string timelineName, IList? add, IList? remove) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) - { - if (list != null) - { - List result = new List(); - var count = list.Count; - for (var index = 0; index < count; index++) - { - var username = list[index]; - if (result.Contains(username)) - { - continue; - } - var (validationResult, message) = _usernameValidator.Validate(username); - if (!validationResult) - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); - result.Add(username); - } - return result; - } - else - { - return null; - } - } - 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.Where(u => simplifiedAdd.Contains(u)).ToList(); - foreach (var u in usersToClean) - { - simplifiedAdd.Remove(u); - simplifiedRemove.Remove(u); - } - - if (simplifiedAdd.Count == 0) - simplifiedAdd = null; - - if (simplifiedRemove.Count == 0) - simplifiedRemove = null; - } - - if (simplifiedAdd == null && simplifiedRemove == null) - return; - - var timelineId = await FindTimelineId(timelineName); - - async Task?> CheckExistenceAndGetId(List? list) - { - if (list == null) - return null; - - List result = new List(); - foreach (var username in list) - { - result.Add(await _userService.GetUserIdByUsername(username)); - } - return result; - } - var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); - var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); - - if (userIdsAdd != null) - { - var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); - _database.TimelineMembers.AddRange(membersToAdd); - } - - if (userIdsRemove != null) - { - var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); - _database.TimelineMembers.RemoveRange(membersToRemove); - } - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - timelineEntity.LastModified = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - } - - public async Task HasManagePermission(string timelineName, long userId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - return userId == timelineEntity.OwnerId; - } - - public async Task HasReadPermission(string timelineName, long? visitorId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); - - if (timelineEntity.Visibility == TimelineVisibility.Public) - return true; - - if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) - return true; - - if (visitorId == null) - { - return false; - } - else - { - var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); - return memberEntity != null; - } - } - - public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); - - if (postEntity == null) - { - if (throwOnPostNotExist) - throw new TimelinePostNotExistException(timelineName, postId, false); - else - return true; - } - - if (postEntity.Content == null && throwOnPostNotExist) - { - throw new TimelinePostNotExistException(timelineName, postId, true); - } - - return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; - } - - public async Task IsMemberOf(string timelineName, long userId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - if (userId == timelineEntity.OwnerId) - return true; - - return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); - } - - public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) - { - List entities; - - IQueryable ApplyTimelineVisibilityFilter(IQueryable query) - { - if (visibility != null && visibility.Count != 0) - { - return query.Where(t => visibility.Contains(t.Visibility)); - } - return query; - } - - bool allVisibilities = visibility == null || visibility.Count == 0; - - if (relate == null) - { - entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync(); - } - else - { - entities = new List(); - - if ((relate.Type & TimelineUserRelationshipType.Own) != 0) - { - entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync()); - } - - if ((relate.Type & TimelineUserRelationshipType.Join) != 0) - { - entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync()); - } - } - - var result = new List(); - - foreach (var entity in entities) - { - result.Add(await MapTimelineFromEntity(entity)); - } - - return result; - } - - public async Task CreateTimeline(string name, long owner) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - ValidateTimelineName(name, nameof(name)); - - var user = await _userService.GetUserById(owner); - - var conflict = await _database.Timelines.AnyAsync(t => t.Name == name); - - if (conflict) - throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - - var newEntity = CreateNewTimelineEntity(name, user.Id!.Value); - - _database.Timelines.Add(newEntity); - await _database.SaveChangesAsync(); - - return await MapTimelineFromEntity(newEntity); - } - - public async Task DeleteTimeline(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - ValidateTimelineName(name, nameof(name)); - - var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync(); - - if (entity == null) - throw new TimelineNotExistException(name); - - _database.Timelines.Remove(entity); - await _database.SaveChangesAsync(); - } - - public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) - { - if (oldTimelineName == null) - throw new ArgumentNullException(nameof(oldTimelineName)); - if (newTimelineName == null) - throw new ArgumentNullException(nameof(newTimelineName)); - - ValidateTimelineName(oldTimelineName, nameof(oldTimelineName)); - ValidateTimelineName(newTimelineName, nameof(newTimelineName)); - - var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); - - if (entity == null) - throw new TimelineNotExistException(oldTimelineName); - - if (oldTimelineName == newTimelineName) - return await MapTimelineFromEntity(entity); - - var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName); - - if (conflict) - throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - - var now = _clock.GetCurrentTime(); - - entity.Name = newTimelineName; - entity.NameLastModified = now; - entity.LastModified = now; - - await _database.SaveChangesAsync(); - - return await MapTimelineFromEntity(entity); - } - } -} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs deleted file mode 100644 index b41c45fd..00000000 --- a/Timeline/Services/UserAvatarService.cs +++ /dev/null @@ -1,265 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public class Avatar - { - 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; } = default!; - public DateTime LastModified { get; set; } - - public CacheableData ToCacheableData() - { - return new CacheableData(Avatar.Type, Avatar.Data, LastModified); - } - } - - /// - /// Provider for default user avatar. - /// - /// - /// Mainly for unit tests. - /// - public interface IDefaultUserAvatarProvider - { - /// - /// Get the etag of default avatar. - /// - /// - Task GetDefaultAvatarETag(); - - /// - /// Get the default avatar. - /// - Task GetDefaultAvatar(); - } - - public interface IUserAvatarService - { - /// - /// Get the etag of a user's avatar. Warning: This method does not check the user existence. - /// - /// The id of the user to get avatar etag of. - /// The etag. - Task GetAvatarETag(long id); - - /// - /// 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. - /// - /// The id of the user to get avatar of. - /// The avatar info. - Task GetAvatar(long id); - - /// - /// Set avatar for a user. Warning: This method does not check the user existence. - /// - /// The id of the user to set avatar for. - /// The avatar. Can be null to delete the saved avatar. - /// The etag of the avatar. - /// Thrown if any field in is null when is not null. - /// Thrown if avatar is of bad format. - Task SetAvatar(long id, Avatar? avatar); - } - - // TODO! : Make this configurable. - public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider - { - private readonly IETagGenerator _eTagGenerator; - - private readonly string _avatarPath; - - private byte[] _cacheData = default!; - private DateTime _cacheLastModified; - private string _cacheETag = default!; - - public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) - { - _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); - _eTagGenerator = eTagGenerator; - } - - private async Task CheckAndInit() - { - 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 GetDefaultAvatarETag() - { - await CheckAndInit(); - return _cacheETag; - } - - public async Task GetDefaultAvatar() - { - await CheckAndInit(); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = "image/png", - Data = _cacheData - }, - LastModified = _cacheLastModified - }; - } - } - - public class UserAvatarService : IUserAvatarService - { - - private readonly ILogger _logger; - - private readonly DatabaseContext _database; - - private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; - - private readonly IImageValidator _imageValidator; - - private readonly IDataManager _dataManager; - - private readonly IClock _clock; - - public UserAvatarService( - ILogger logger, - DatabaseContext database, - IDefaultUserAvatarProvider defaultUserAvatarProvider, - IImageValidator imageValidator, - IDataManager dataManager, - IClock clock) - { - _logger = logger; - _database = database; - _defaultUserAvatarProvider = defaultUserAvatarProvider; - _imageValidator = imageValidator; - _dataManager = dataManager; - _clock = clock; - } - - public async Task GetAvatarETag(long id) - { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; - if (eTag == null) - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); - else - return eTag; - } - - public async Task GetAvatar(long id) - { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); - - if (avatarEntity != null) - { - if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) - { - var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; - _logger.LogCritical(message); - throw new DatabaseCorruptedException(message); - } - - - if (avatarEntity.DataTag != null) - { - var data = await _dataManager.GetEntry(avatarEntity.DataTag); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = avatarEntity.Type!, - Data = 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(long id, Avatar? avatar) - { - if (avatar != null) - { - if (avatar.Data == null) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar)); - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); - } - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); - - if (avatar == null) - { - if (avatarEntity != null && avatarEntity.DataTag != null) - { - await _dataManager.FreeEntry(avatarEntity.DataTag); - avatarEntity.DataTag = null; - avatarEntity.Type = null; - avatarEntity.LastModified = _clock.GetCurrentTime(); - await _database.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); - } - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); - } - else - { - await _imageValidator.Validate(avatar.Data, avatar.Type, true); - var tag = await _dataManager.RetainEntry(avatar.Data); - var oldTag = avatarEntity?.DataTag; - var create = avatarEntity == null; - if (avatarEntity == null) - { - avatarEntity = new UserAvatarEntity(); - _database.UserAvatars.Add(avatarEntity); - } - avatarEntity.DataTag = tag; - avatarEntity.Type = avatar.Type; - avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = id; - await _database.SaveChangesAsync(); - _logger.LogInformation(create ? - Resources.Services.UserAvatarService.LogCreateEntity - : Resources.Services.UserAvatarService.LogUpdateEntity); - if (oldTag != null) - { - await _dataManager.FreeEntry(oldTag); - } - - return avatarEntity.DataTag; - } - } - } - - public static class UserAvatarServiceCollectionExtensions - { - public static void AddUserAvatarService(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - } - } -} diff --git a/Timeline/Services/UserDeleteService.cs b/Timeline/Services/UserDeleteService.cs deleted file mode 100644 index 845de573..00000000 --- a/Timeline/Services/UserDeleteService.cs +++ /dev/null @@ -1,69 +0,0 @@ -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.Helpers; -using Timeline.Models.Validation; -using static Timeline.Resources.Services.UserService; - -namespace Timeline.Services -{ - public interface IUserDeleteService - { - /// - /// Delete a user of given username. - /// - /// Username of the user to delete. Can't be null. - /// True if user is deleted, false if user not exist. - /// Thrown if is null. - /// Thrown when is of bad format. - Task DeleteUser(string username); - } - - public class UserDeleteService : IUserDeleteService - { - private readonly ILogger _logger; - - private readonly DatabaseContext _databaseContext; - - private readonly ITimelineService _timelineService; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - - public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelineService timelineService) - { - _logger = logger; - _databaseContext = databaseContext; - _timelineService = timelineService; - } - - public async Task DeleteUser(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - if (!_usernameValidator.Validate(username, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), nameof(username)); - } - - var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) - return false; - - await _timelineService.DeleteAllPostsOfUser(user.Id); - - _databaseContext.Users.Remove(user); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", user.Username))); - - return true; - } - - } -} diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs deleted file mode 100644 index f27ee1bb..00000000 --- a/Timeline/Services/UserRoleConvert.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection 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 deleted file mode 100644 index 821bc33d..00000000 --- a/Timeline/Services/UserService.cs +++ /dev/null @@ -1,437 +0,0 @@ -using Microsoft.EntityFrameworkCore; -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 Timeline.Services.Exceptions; -using static Timeline.Resources.Services.UserService; - -namespace Timeline.Services -{ - public interface IUserService - { - /// - /// Try to verify the given username and password. - /// - /// The username of the user to verify. - /// The password of the user to verify. - /// The user info and auth info. - /// Thrown when or is null. - /// Thrown when is of bad format or is empty. - /// Thrown when the user with given username does not exist. - /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); - - /// - /// Try to get a user by id. - /// - /// The id of the user. - /// The user info. - /// Thrown when the user with given id does not exist. - Task GetUserById(long id); - - /// - /// Get the user info of given username. - /// - /// Username of the user. - /// The info of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserByUsername(string username); - - /// - /// Get the user id of given username. - /// - /// Username of the user. - /// The id of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserIdByUsername(string username); - - /// - /// List all users. - /// - /// The user info of users. - Task GetUsers(); - - /// - /// Create a user with given info. - /// - /// The info of new user. - /// The the new user. - /// Thrown when is null. - /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. - /// - /// must not be null and must be a valid username. - /// must not be null or empty. - /// is false by default (null). - /// must be a valid nickname if set. It is empty by default. - /// Other fields are ignored. - /// - Task CreateUser(User info); - - /// - /// Modify a user's info. - /// - /// The id of the user. - /// The new info. May be null. - /// The new user info. - /// Thrown when some fields in is bad. - /// Thrown when user with given id does not exist. - /// - /// Only , , and will be used. - /// If null, then not change. - /// Other fields are ignored. - /// Version will increase if password is changed. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// - /// - Task ModifyUser(long id, User? info); - - /// - /// Modify a user's info. - /// - /// The username of the user. - /// The new info. May be null. - /// The new user info. - /// Thrown when is null. - /// Thrown when is of bad format or some fields in is bad. - /// Thrown when user with given id does not exist. - /// Thrown when user with the newusername already exist. - /// - /// Only , and will be used. - /// If null, then not change. - /// Other fields are ignored. - /// After modified, even if nothing is changed, version will increase. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. - /// - /// - Task ModifyUser(string username, User? info); - - /// - /// Try to change a user's password with old password. - /// - /// The id of user to change password of. - /// Old password. - /// New password. - /// Thrown if or is null. - /// Thrown if or is empty. - /// Thrown if the user with given username does not exist. - /// Thrown if the old password is wrong. - Task ChangePassword(long id, string oldPassword, string newPassword); - } - - public class UserService : IUserService - { - private readonly ILogger _logger; - private readonly IClock _clock; - - private readonly DatabaseContext _databaseContext; - - private readonly IPasswordService _passwordService; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) - { - _logger = logger; - _clock = clock; - _databaseContext = databaseContext; - _passwordService = passwordService; - } - - 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 static void CheckPasswordFormat(string password, string? paramName) - { - if (password.Length == 0) - { - throw new ArgumentException(ExceptionPasswordEmpty, paramName); - } - } - - private void CheckNicknameFormat(string nickname, string? paramName) - { - if (!_nicknameValidator.Validate(nickname, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); - } - } - - private static void ThrowUsernameConflict() - { - throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); - } - - private static User CreateUserFromEntity(UserEntity entity) - { - return new User - { - UniqueId = entity.UniqueId, - Username = entity.Username, - Administrator = UserRoleConvert.ToBool(entity.Roles), - Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, - Id = entity.Id, - Version = entity.Version, - CreateTime = entity.CreateTime, - UsernameChangeTime = entity.UsernameChangeTime, - LastModified = entity.LastModified - }; - } - - public async Task VerifyCredential(string username, string password) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - CheckUsernameFormat(username, nameof(username)); - CheckPasswordFormat(password, nameof(password)); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - if (!_passwordService.VerifyPassword(entity.Password, password)) - throw new BadPasswordException(password); - - return CreateUserFromEntity(entity); - } - - public async Task GetUserById(long id) - { - var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (user == null) - throw new UserNotExistException(id); - - return CreateUserFromEntity(user); - } - - public async Task GetUserByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - CheckUsernameFormat(username, nameof(username)); - - var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - return CreateUserFromEntity(entity); - } - - public async Task GetUserIdByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - 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 GetUsers() - { - var entities = await _databaseContext.Users.ToArrayAsync(); - return entities.Select(user => CreateUserFromEntity(user)).ToArray(); - } - - public async Task CreateUser(User info) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - if (info.Username == null) - throw new ArgumentException(ExceptionUsernameNull, nameof(info)); - CheckUsernameFormat(info.Username, nameof(info)); - - 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(); - - 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(LogDatabaseCreate, - ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - - return CreateUserFromEntity(newEntity); - } - - private void ValidateModifyUserInfo(User? info) - { - if (info != null) - { - if (info.Username != null) - CheckUsernameFormat(info.Username, nameof(info)); - - if (info.Password != null) - CheckPasswordFormat(info.Password, nameof(info)); - - if (info.Nickname != null) - CheckNicknameFormat(info.Nickname, nameof(info)); - } - } - - private async Task UpdateUserEntity(UserEntity entity, User? info) - { - if (info != null) - { - var now = _clock.GetCurrentTime(); - bool updateLastModified = false; - - var username = info.Username; - if (username != null && username != entity.Username) - { - var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); - if (conflict) - ThrowUsernameConflict(); - - entity.Username = username; - entity.UsernameChangeTime = now; - updateLastModified = true; - } - - var password = info.Password; - if (password != null) - { - entity.Password = _passwordService.HashPassword(password); - entity.Version += 1; - } - - var administrator = info.Administrator; - if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator) - { - entity.Roles = UserRoleConvert.ToString(administrator.Value); - updateLastModified = true; - } - - var nickname = info.Nickname; - if (nickname != null && nickname != entity.Nickname) - { - entity.Nickname = nickname; - updateLastModified = true; - } - - if (updateLastModified) - { - entity.LastModified = now; - } - } - } - - - public async Task 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); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); - - return CreateUserFromEntity(entity); - } - - public async Task ModifyUser(string username, User? info) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username, nameof(username)); - - ValidateModifyUserInfo(info); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(username); - - await UpdateUserEntity(entity, info); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); - - return CreateUserFromEntity(entity); - } - - public async Task ChangePassword(long id, string oldPassword, string newPassword) - { - 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(); - - if (entity == null) - throw new UserNotExistException(id); - - if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) - throw new BadPasswordException(oldPassword); - - entity.Password = _passwordService.HashPassword(newPassword); - entity.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); - } - } -} diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs deleted file mode 100644 index d25fabb3..00000000 --- a/Timeline/Services/UserTokenException.cs +++ /dev/null @@ -1,68 +0,0 @@ -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; } - - public DateTime VerifyTime { get; private set; } - } - - [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 deleted file mode 100644 index 813dae67..00000000 --- a/Timeline/Services/UserTokenManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public class UserTokenCreateResult - { - public string Token { get; set; } = default!; - public User User { get; set; } = default!; - } - - public interface IUserTokenManager - { - /// - /// Try to create a token for given username and password. - /// - /// The username. - /// The password. - /// The expire time of the token. - /// The created token and the user info. - /// Thrown when or is null. - /// Thrown when is of bad format. - /// Thrown when the user with does not exist. - /// Thrown when is wrong. - public Task CreateToken(string username, string password, DateTime? expireAt = null); - - /// - /// Verify a token and get the saved user info. This also check the database for existence of the user. - /// - /// The token. - /// The user stored in token. - /// Thrown when is null. - /// Thrown when the token is expired. - /// Thrown when the token is of bad version. - /// Thrown when the token is of bad format. - /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); - } - - public class UserTokenManager : IUserTokenManager - { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly IUserTokenService _userTokenService; - private readonly IClock _clock; - - public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) - { - _logger = logger; - _userService = userService; - _userTokenService = userTokenService; - _clock = clock; - } - - public async Task CreateToken(string username, string password, DateTime? expireAt = null) - { - expireAt = expireAt?.MyToUtc(); - - 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 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/UserTokenService.cs b/Timeline/Services/UserTokenService.cs deleted file mode 100644 index 86f3a0f7..00000000 --- a/Timeline/Services/UserTokenService.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using Timeline.Configs; -using Timeline.Entities; - -namespace Timeline.Services -{ - public class UserTokenInfo - { - public long Id { get; set; } - public long Version { get; set; } - public DateTime? ExpireAt { get; set; } - } - - public interface IUserTokenService - { - /// - /// Create a token for a given token info. - /// - /// The info to generate token. - /// Return the generated token. - /// Thrown when is null. - string GenerateToken(UserTokenInfo tokenInfo); - - /// - /// Verify a token and get the saved info. - /// - /// The token to verify. - /// The saved info in token. - /// Thrown when is null. - /// Thrown when the token is of bad format. - /// - /// If this method throw , it usually means the token is not created by this service. - /// - UserTokenInfo VerifyToken(string token); - } - - public class JwtUserTokenService : IUserTokenService - { - private const string VersionClaimType = "timeline_version"; - - private readonly IOptionsMonitor _jwtConfig; - private readonly IClock _clock; - - private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); - private SymmetricSecurityKey _tokenSecurityKey; - - public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock, DatabaseContext database) - { - _jwtConfig = jwtConfig; - _clock = clock; - - var key = database.JwtToken.Select(t => t.Key).SingleOrDefault(); - - if (key == null) - { - throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist); - } - - _tokenSecurityKey = new SymmetricSecurityKey(key); - } - - public string GenerateToken(UserTokenInfo tokenInfo) - { - if (tokenInfo == null) - throw new ArgumentNullException(nameof(tokenInfo)); - - var config = _jwtConfig.CurrentValue; - - var identity = new ClaimsIdentity(); - 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() - { - Subject = identity, - Issuer = config.Issuer, - Audience = config.Audience, - SigningCredentials = new SigningCredentials(_tokenSecurityKey, SecurityAlgorithms.HmacSha384), - IssuedAt = _clock.GetCurrentTime(), - 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. - }; - - var token = _tokenHandler.CreateToken(tokenDescriptor); - var tokenString = _tokenHandler.WriteToken(token); - - return tokenString; - } - - - public UserTokenInfo VerifyToken(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - var config = _jwtConfig.CurrentValue; - try - { - var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = false, - ValidIssuer = config.Issuer, - ValidAudience = config.Audience, - IssuerSigningKey = _tokenSecurityKey - }, out var t); - - var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (idClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); - if (!long.TryParse(idClaim, out var id)) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); - - var versionClaim = principal.FindFirstValue(VersionClaimType); - if (versionClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); - if (!long.TryParse(versionClaim, out var version)) - 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 UserTokenInfo - { - Id = id, - Version = version, - ExpireAt = expireAt - }; - } - catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) - { - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); - } - } - } -} -- cgit v1.2.3