diff options
Diffstat (limited to 'Timeline/Services')
28 files changed, 0 insertions, 3273 deletions
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) { }
-
- /// <summary>
- /// The wrong password.
- /// </summary>
- 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
-{
- /// <summary>
- /// Convenient for unit test.
- /// </summary>
- public interface IClock
- {
- /// <summary>
- /// Get current time.
- /// </summary>
- /// <returns>Current time.</returns>
- 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
-{
- /// <summary>
- /// A data manager controlling data.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- public interface IDataManager
- {
- /// <summary>
- /// Saves the data to a new entry if it does not exist,
- /// increases its ref count and returns a tag to the entry.
- /// </summary>
- /// <param name="data">The data. Can't be null.</param>
- /// <returns>The tag of the created entry.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
- public Task<string> RetainEntry(byte[] data);
-
- /// <summary>
- /// Decrease the the ref count of the entry.
- /// Remove it if ref count is zero.
- /// </summary>
- /// <param name="tag">The tag of the entry.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
- /// <remarks>
- /// It's no-op if entry with tag does not exist.
- /// </remarks>
- public Task FreeEntry(string tag);
-
- /// <summary>
- /// Retrieve the entry with given tag.
- /// </summary>
- /// <param name="tag">The tag of the entry.</param>
- /// <returns>The data of the entry.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
- /// <exception cref="InvalidOperationException">Thrown when entry with given tag does not exist.</exception>
- public Task<byte[]> 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<string> 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<byte[]> 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
- {
- /// <summary>
- /// Generate a etag for given source.
- /// </summary>
- /// <param name="source">The source data.</param>
- /// <returns>The generated etag.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
- Task<string> Generate(byte[] source);
- }
-
- public 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<string> 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
-{
- /// <summary>
- /// Thrown when an entity is already exists.
- /// </summary>
- /// <remarks>
- /// For example, want to create a timeline but a timeline with the same name already exists.
- /// </remarks>
- [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
-{
- /// <summary>
- /// Thrown when you want to get an entity that does not exist.
- /// </summary>
- /// <example>
- /// For example, you want to get a timeline with given name but it does not exist.
- /// </example>
- [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
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not of required size.
- /// </summary>
- NotSquare,
- /// <summary>
- /// Other unknown errer.
- /// </summary>
- 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; }
-
- /// <summary>
- /// True if the post is deleted. False if the post does not exist at all.
- /// </summary>
- 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
-{
- /// <summary>
- /// The user requested does not exist.
- /// </summary>
- [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) { }
-
- /// <summary>
- /// The username of the user that does not exist.
- /// </summary>
- public string? Username { get; set; }
-
- /// <summary>
- /// The id of the user that does not exist.
- /// </summary>
- public long? Id { get; set; }
- }
-}
diff --git a/Timeline/Services/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
- {
- /// <summary>
- /// Validate a image data.
- /// </summary>
- /// <param name="data">The data of the image. Can't be null.</param>
- /// <param name="requestType">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.</param>
- /// <param name="square">If true, image must be square.</param>
- /// <returns>The format.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
- /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
- Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false);
- }
-
- public class ImageValidator : IImageValidator
- {
- public ImageValidator()
- {
- }
-
- public async Task<IImageFormat> 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
-{
- /// <summary>
- /// Hashed password is of bad format.
- /// </summary>
- /// <seealso cref="IPasswordService.VerifyPassword(string, string)"/>
- [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
- {
- /// <summary>
- /// Hash a password.
- /// </summary>
- /// <param name="password">The password to hash.</param>
- /// <returns>A hashed representation of the supplied <paramref name="password"/>.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="password"/> is null.</exception>
- string HashPassword(string password);
-
- /// <summary>
- /// Verify whether the password fits into the hashed one.
- ///
- /// Usually you only need to check the returned bool value.
- /// Catching <see cref="HashedPasswordBadFromatException"/> usually is not necessary.
- /// Because if your program logic is right and always call <see cref="HashPassword(string)"/>
- /// and <see cref="VerifyPassword(string, string)"/> in pair, this exception will never be thrown.
- /// A thrown one usually means the data you saved is corupted, which is a critical problem.
- /// </summary>
- /// <param name="hashedPassword">The hashed password.</param>
- /// <param name="providedPassword">The password supplied for comparison.</param>
- /// <returns>True indicating password is right. Otherwise false.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="hashedPassword"/> or <paramref name="providedPassword"/> is null.</exception>
- /// <exception cref="HashedPasswordBadFromatException">Thrown when the hashed password is of bad format.</exception>
- bool VerifyPassword(string hashedPassword, string providedPassword);
- }
-
- /// <summary>
- /// 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.
- /// </summary>
- 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<string?>(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?
- }
-
- /// <summary>
- /// This define the interface of both personal timeline and ordinary timeline.
- /// </summary>
- public interface ITimelineService
- {
- /// <summary>
- /// Get the timeline last modified time (not include name change).
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<DateTime> GetTimelineLastModifiedTime(string timelineName);
-
- /// <summary>
- /// Get the timeline unique id.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<string> GetTimelineUniqueId(string timelineName);
-
- /// <summary>
- /// Get the timeline info.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<Models.Timeline> GetTimeline(string timelineName);
-
- /// <summary>
- /// Set the properties of a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="newProperties">The new properties. Null member means not to change.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="newProperties"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
-
- /// <summary>
- /// Get all the posts in the timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="modifiedSince">The time that posts have been modified since.</param>
- /// <param name="includeDeleted">Whether include deleted posts.</param>
- /// <returns>A list of all posts.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
-
- /// <summary>
- /// Get the etag of data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostData(string, long)"/>
- Task<string> GetPostDataETag(string timelineName, long postId);
-
- /// <summary>
- /// Get the data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostDataETag(string, long)"/>
- Task<PostData> GetPostData(string timelineName, long postId);
-
- /// <summary>
- /// Create a new text post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="text">The content text.</param>
- /// <param name="time">The time of the post. If null, then current time is used.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
-
- /// <summary>
- /// Create a new image post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="imageData">The image data.</param>
- /// <param name="time">The time of the post. If null, then use current time.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="imageData"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
- Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time);
-
- /// <summary>
- /// Delete a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to delete post against.</param>
- /// <param name="postId">The id of the post to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
- /// <remarks>
- /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
- /// </remarks>
- Task DeletePost(string timelineName, long postId);
-
- /// <summary>
- /// Delete all posts of the given user. Used when delete a user.
- /// </summary>
- /// <param name="userId">The id of the user.</param>
- Task DeleteAllPostsOfUser(long userId);
-
- /// <summary>
- /// Change member of timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="membersToAdd">A list of usernames of members to add. May be null.</param>
- /// <param name="membersToRemove">A list of usernames of members to remove. May be null.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="ArgumentException">Thrown when names in <paramref name="membersToAdd"/> or <paramref name="membersToRemove"/> is not a valid username.</exception>
- /// <exception cref="UserNotExistException">Thrown when one of the user to change does not exist.</exception>
- /// <remarks>
- /// Operating on a username that is of bad format or does not exist always throws.
- /// 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.
- /// </remarks>
- Task ChangeMember(string timelineName, IList<string>? membersToAdd, IList<string>? membersToRemove);
-
- /// <summary>
- /// Check whether a user can manage(change timeline info, member, ...) a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="userId">The id of the user to check on.</param>
- /// <returns>True if the user can manage the timeline, otherwise false.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <remarks>
- /// This method does not check whether visitor is administrator.
- /// Return false if user with user id does not exist.
- /// </remarks>
- Task<bool> HasManagePermission(string timelineName, long userId);
-
- /// <summary>
- /// Verify whether a visitor has the permission to read a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
- /// <returns>True if can read, false if can't read.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <remarks>
- /// This method does not check whether visitor is administrator.
- /// Return false if user with visitor id does not exist.
- /// </remarks>
- Task<bool> HasReadPermission(string timelineName, long? visitorId);
-
- /// <summary>
- /// Verify whether a user has the permission to modify a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="postId">The id of the post.</param>
- /// <param name="modifierId">The id of the user to check on.</param>
- /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
- /// <returns>True if can modify, false if can't modify.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
- /// <remarks>
- /// Unless <paramref name="throwOnPostNotExist"/> 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 <paramref name="throwOnPostNotExist"/> 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.
- /// </remarks>
- Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
-
- /// <summary>
- /// Verify whether a user is member of a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="userId">The id of user to check on.</param>
- /// <returns>True if it is a member, false if not.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <remarks>
- /// Timeline owner is also considered as a member.
- /// Return false when user with user id does not exist.
- /// </remarks>
- Task<bool> IsMemberOf(string timelineName, long userId);
-
- /// <summary>
- /// Get all timelines including personal and ordinary timelines.
- /// </summary>
- /// <param name="relate">Filter timelines related (own or is a member) to specific user.</param>
- /// <param name="visibility">Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored.</param>
- /// <returns>The list of timelines.</returns>
- /// <remarks>
- /// If user with related user id does not exist, empty list will be returned.
- /// </remarks>
- Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
-
- /// <summary>
- /// Create a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="ownerId">The id of owner of the timeline.</param>
- /// <returns>The info of the new timeline.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
- /// <exception cref="EntityAlreadyExistException">Thrown when the timeline already exists.</exception>
- /// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
- Task<Models.Timeline> CreateTimeline(string timelineName, long ownerId);
-
- /// <summary>
- /// Delete a timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
- /// <exception cref="TimelineNotExistException">Thrown when the timeline does not exist.</exception>
- Task DeleteTimeline(string timelineName);
-
- /// <summary>
- /// Change name of a timeline.
- /// </summary>
- /// <param name="oldTimelineName">The old timeline name.</param>
- /// <param name="newTimelineName">The new timeline name.</param>
- /// <returns>The new timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="oldTimelineName"/> or <paramref name="newTimelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="oldTimelineName"/> or <paramref name="newTimelineName"/> is of invalid format.</exception>
- /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- /// <exception cref="EntityAlreadyExistException">Thrown when a timeline with new name already exists.</exception>
- /// <remarks>
- /// You can only change name of general timeline.
- /// </remarks>
- Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName);
- }
-
- public class TimelineService : ITimelineService
- {
- public TimelineService(ILogger<TimelineService> 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<TimelineService> _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<Models.Timeline> MapTimelineFromEntity(TimelineEntity entity)
- {
- var owner = await _userService.GetUserById(entity.OwnerId);
-
- var members = new List<User>();
- 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<TimelinePost> 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<TimelineMemberEntity>()
- };
- }
-
-
-
- // 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<long> 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<DateTime> 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<string> 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<Models.Timeline> 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<List<TimelinePost>> 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<TimelinePostEntity> 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<TimelinePost>();
- foreach (var entity in postEntities)
- {
- posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
- }
- return posts;
- }
-
- public async Task<string> 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<PostData> 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<TimelinePost> 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<TimelinePost> 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<string>();
-
- 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<string>? add, IList<string>? remove)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- List<string>? RemoveDuplicateAndCheckFormat(IList<string>? list, string paramName)
- {
- if (list != null)
- {
- List<string> result = new List<string>();
- 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<List<long>?> CheckExistenceAndGetId(List<string>? list)
- {
- if (list == null)
- return null;
-
- List<long> result = new List<long>();
- 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<bool> 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<bool> 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<bool> 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<bool> 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<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
- {
- List<TimelineEntity> entities;
-
- IQueryable<TimelineEntity> ApplyTimelineVisibilityFilter(IQueryable<TimelineEntity> 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<TimelineEntity>();
-
- 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<Models.Timeline>();
-
- foreach (var entity in entities)
- {
- result.Add(await MapTimelineFromEntity(entity));
- }
-
- return result;
- }
-
- public async Task<Models.Timeline> 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<Models.Timeline> 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);
- }
- }
-
- /// <summary>
- /// Provider for default user avatar.
- /// </summary>
- /// <remarks>
- /// Mainly for unit tests.
- /// </remarks>
- public interface IDefaultUserAvatarProvider
- {
- /// <summary>
- /// Get the etag of default avatar.
- /// </summary>
- /// <returns></returns>
- Task<string> GetDefaultAvatarETag();
-
- /// <summary>
- /// Get the default avatar.
- /// </summary>
- Task<AvatarInfo> GetDefaultAvatar();
- }
-
- public interface IUserAvatarService
- {
- /// <summary>
- /// Get the etag of a user's avatar. Warning: This method does not check the user existence.
- /// </summary>
- /// <param name="id">The id of the user to get avatar etag of.</param>
- /// <returns>The etag.</returns>
- Task<string> GetAvatarETag(long id);
-
- /// <summary>
- /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence.
- /// </summary>
- /// <param name="id">The id of the user to get avatar of.</param>
- /// <returns>The avatar info.</returns>
- Task<AvatarInfo> GetAvatar(long id);
-
- /// <summary>
- /// Set avatar for a user. Warning: This method does not check the user existence.
- /// </summary>
- /// <param name="id">The id of the user to set avatar for.</param>
- /// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <returns>The etag of the avatar.</returns>
- /// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
- /// <exception cref="ImageException">Thrown if avatar is of bad format.</exception>
- Task<string> 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<string> GetDefaultAvatarETag()
- {
- await CheckAndInit();
- return _cacheETag;
- }
-
- public async Task<AvatarInfo> GetDefaultAvatar()
- {
- await CheckAndInit();
- return new AvatarInfo
- {
- Avatar = new Avatar
- {
- Type = "image/png",
- Data = _cacheData
- },
- LastModified = _cacheLastModified
- };
- }
- }
-
- public class UserAvatarService : IUserAvatarService
- {
-
- private readonly ILogger<UserAvatarService> _logger;
-
- private readonly DatabaseContext _database;
-
- private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
-
- private readonly IImageValidator _imageValidator;
-
- private readonly IDataManager _dataManager;
-
- private readonly IClock _clock;
-
- public UserAvatarService(
- ILogger<UserAvatarService> 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<string> 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<AvatarInfo> 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<string> 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<IUserAvatarService, UserAvatarService>();
- services.AddScoped<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- }
- }
-}
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
- {
- /// <summary>
- /// Delete a user of given username.
- /// </summary>
- /// <param name="username">Username of the user to delete. Can't be null.</param>
- /// <returns>True if user is deleted, false if user not exist.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- Task<bool> DeleteUser(string username);
- }
-
- public class UserDeleteService : IUserDeleteService
- {
- private readonly ILogger<UserDeleteService> _logger;
-
- private readonly DatabaseContext _databaseContext;
-
- private readonly ITimelineService _timelineService;
-
- private readonly UsernameValidator _usernameValidator = new UsernameValidator();
-
- public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelineService timelineService)
- {
- _logger = logger;
- _databaseContext = databaseContext;
- _timelineService = timelineService;
- }
-
- public async Task<bool> 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<string> roles)
- {
- return roles.Contains(AdminRole);
- }
-
- public static string ToString(IReadOnlyCollection<string> roles)
- {
- return string.Join(',', roles);
- }
-
- public static string ToString(bool administrator)
- {
- return administrator ? UserRole + "," + AdminRole : UserRole;
- }
-
- public static bool ToBool(string s)
- {
- return s.Contains("admin", StringComparison.InvariantCulture);
- }
- }
-}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs 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
- {
- /// <summary>
- /// Try to verify the given username and password.
- /// </summary>
- /// <param name="username">The username of the user to verify.</param>
- /// <param name="password">The password of the user to verify.</param>
- /// <returns>The user info and auth info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or <paramref name="password"/> is empty.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<User> VerifyCredential(string username, string password);
-
- /// <summary>
- /// Try to get a user by id.
- /// </summary>
- /// <param name="id">The id of the user.</param>
- /// <returns>The user info.</returns>
- /// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
- Task<User> GetUserById(long id);
-
- /// <summary>
- /// Get the user info of given username.
- /// </summary>
- /// <param name="username">Username of the user.</param>
- /// <returns>The info of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<User> GetUserByUsername(string username);
-
- /// <summary>
- /// Get the user id of given username.
- /// </summary>
- /// <param name="username">Username of the user.</param>
- /// <returns>The id of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<long> GetUserIdByUsername(string username);
-
- /// <summary>
- /// List all users.
- /// </summary>
- /// <returns>The user info of users.</returns>
- Task<User[]> GetUsers();
-
- /// <summary>
- /// Create a user with given info.
- /// </summary>
- /// <param name="info">The info of new user.</param>
- /// <returns>The the new user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="info"/>is null.</exception>
- /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
- /// <exception cref="EntityAlreadyExistException">Thrown when a user with given username already exists.</exception>
- /// <remarks>
- /// <see cref="User.Username"/> must not be null and must be a valid username.
- /// <see cref="User.Password"/> must not be null or empty.
- /// <see cref="User.Administrator"/> is false by default (null).
- /// <see cref="User.Nickname"/> must be a valid nickname if set. It is empty by default.
- /// Other fields are ignored.
- /// </remarks>
- Task<User> CreateUser(User info);
-
- /// <summary>
- /// Modify a user's info.
- /// </summary>
- /// <param name="id">The id of the user.</param>
- /// <param name="info">The new info. May be null.</param>
- /// <returns>The new user info.</returns>
- /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
- /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
- /// <remarks>
- /// Only <see cref="User.Username"/>, <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
- /// If null, then not change.
- /// Other fields are ignored.
- /// Version will increase if password is changed.
- ///
- /// <see cref="User.Username"/> must be a valid username if set.
- /// <see cref="User.Password"/> can't be empty if set.
- /// <see cref="User.Nickname"/> must be a valid nickname if set.
- ///
- /// </remarks>
- /// <seealso cref="ModifyUser(string, User)"/>
- Task<User> ModifyUser(long id, User? info);
-
- /// <summary>
- /// Modify a user's info.
- /// </summary>
- /// <param name="username">The username of the user.</param>
- /// <param name="info">The new info. May be null.</param>
- /// <returns>The new user info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or some fields in <paramref name="info"/> is bad.</exception>
- /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
- /// <exception cref="EntityAlreadyExistException">Thrown when user with the newusername already exist.</exception>
- /// <remarks>
- /// Only <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
- /// If null, then not change.
- /// Other fields are ignored.
- /// After modified, even if nothing is changed, version will increase.
- ///
- /// <see cref="User.Username"/> must be a valid username if set.
- /// <see cref="User.Password"/> can't be empty if set.
- /// <see cref="User.Nickname"/> must be a valid nickname if set.
- ///
- /// Note: Whether <see cref="User.Version"/> is set or not, version will increase and not set to the specified value if there is one.
- /// </remarks>
- /// <seealso cref="ModifyUser(long, User)"/>
- Task<User> ModifyUser(string username, User? info);
-
- /// <summary>
- /// Try to change a user's password with old password.
- /// </summary>
- /// <param name="id">The id of user to change password of.</param>
- /// <param name="oldPassword">Old password.</param>
- /// <param name="newPassword">New password.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
- Task ChangePassword(long id, string oldPassword, string newPassword);
- }
-
- public class UserService : IUserService
- {
- private readonly ILogger<UserService> _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<UserService> 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<User> 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<User> 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<User> 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<long> 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<User[]> GetUsers()
- {
- var entities = await _databaseContext.Users.ToArrayAsync();
- return entities.Select(user => CreateUserFromEntity(user)).ToArray();
- }
-
- public async Task<User> 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<User> ModifyUser(long id, User? info)
- {
- ValidateModifyUserInfo(info);
-
- var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
- if (entity == null)
- throw new UserNotExistException(id);
-
- await UpdateUserEntity(entity, info);
-
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(LogDatabaseUpdate, ("Id", id));
-
- return CreateUserFromEntity(entity);
- }
-
- public async Task<User> 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
- {
- /// <summary>
- /// Try to create a token for given username and password.
- /// </summary>
- /// <param name="username">The username.</param>
- /// <param name="password">The password.</param>
- /// <param name="expireAt">The expire time of the token.</param>
- /// <returns>The created token and the user info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with <paramref name="username"/> does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown when <paramref name="password"/> is wrong.</exception>
- public Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null);
-
- /// <summary>
- /// Verify a token and get the saved user info. This also check the database for existence of the user.
- /// </summary>
- /// <param name="token">The token.</param>
- /// <returns>The user stored in token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="UserTokenTimeExpireException">Thrown when the token is expired.</exception>
- /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
- /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
- public Task<User> VerifyToken(string token);
- }
-
- public class UserTokenManager : IUserTokenManager
- {
- private readonly ILogger<UserTokenManager> _logger;
- private readonly IUserService _userService;
- private readonly IUserTokenService _userTokenService;
- private readonly IClock _clock;
-
- public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
- {
- _logger = logger;
- _userService = userService;
- _userTokenService = userTokenService;
- _clock = clock;
- }
-
- public async Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null)
- {
- 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<User> VerifyToken(string token)
- {
- if (token == null)
- throw new ArgumentNullException(nameof(token));
-
- var tokenInfo = _userTokenService.VerifyToken(token);
-
- if (tokenInfo.ExpireAt.HasValue)
- {
- var currentTime = _clock.GetCurrentTime();
- if (tokenInfo.ExpireAt < currentTime)
- throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime);
- }
-
- var user = await _userService.GetUserById(tokenInfo.Id);
-
- if (tokenInfo.Version < user.Version)
- throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value);
-
- return user;
- }
- }
-}
diff --git a/Timeline/Services/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
- {
- /// <summary>
- /// Create a token for a given token info.
- /// </summary>
- /// <param name="tokenInfo">The info to generate token.</param>
- /// <returns>Return the generated token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
- string GenerateToken(UserTokenInfo tokenInfo);
-
- /// <summary>
- /// Verify a token and get the saved info.
- /// </summary>
- /// <param name="token">The token to verify.</param>
- /// <returns>The saved info in token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
- /// <remarks>
- /// If this method throw <see cref="UserTokenBadFormatException"/>, it usually means the token is not created by this service.
- /// </remarks>
- UserTokenInfo VerifyToken(string token);
- }
-
- public class JwtUserTokenService : IUserTokenService
- {
- private const string VersionClaimType = "timeline_version";
-
- private readonly IOptionsMonitor<JwtConfiguration> _jwtConfig;
- private readonly IClock _clock;
-
- private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
- private SymmetricSecurityKey _tokenSecurityKey;
-
- public JwtUserTokenService(IOptionsMonitor<JwtConfiguration> 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);
- }
- }
- }
-}
|