diff options
Diffstat (limited to 'BackEnd/Timeline/Services')
-rw-r--r-- | BackEnd/Timeline/Services/BasicTimelineService.cs | 122 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/BasicUserService.cs | 66 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs | 7 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/HighlightTimelineService.cs | 115 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/TimelinePostService.cs | 493 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/TimelineService.cs | 624 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserCredentialService.cs | 102 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserDeleteService.cs | 8 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserPermissionService.cs | 33 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserService.cs | 146 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserTokenManager.cs | 13 |
11 files changed, 1020 insertions, 709 deletions
diff --git a/BackEnd/Timeline/Services/BasicTimelineService.cs b/BackEnd/Timeline/Services/BasicTimelineService.cs new file mode 100644 index 00000000..0d9f64a9 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicTimelineService.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic timeline functions, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicTimelineService
+ {
+ /// <summary>
+ /// Get the timeline id by name.
+ /// </summary>
+ /// <param name="timelineName">Timeline name.</param>
+ /// <returns>Id of the timeline.</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>
+ /// If name is of personal timeline and the timeline does not exist, it will be created if user exists.
+ /// If the user does not exist, <see cref="TimelineNotExistException"/> will be thrown with <see cref="UserNotExistException"/> as inner exception.
+ ///</remarks>
+ Task<long> GetTimelineIdByName(string timelineName);
+ }
+
+
+ public class BasicTimelineService : IBasicTimelineService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly IBasicUserService _basicUserService;
+ private readonly IClock _clock;
+
+ private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator();
+
+ public BasicTimelineService(DatabaseContext database, IBasicUserService basicUserService, IClock clock)
+ {
+ _database = database;
+ _basicUserService = basicUserService;
+ _clock = clock;
+ }
+
+ protected 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>()
+ };
+ }
+
+ public async Task<long> GetTimelineIdByName(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ if (!_generalTimelineNameValidator.Validate(timelineName, out var message))
+ throw new ArgumentException(message);
+
+ timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+
+ if (isPersonal)
+ {
+ long userId;
+ try
+ {
+ userId = await _basicUserService.GetUserIdByUsername(timelineName);
+ }
+ 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
+ {
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs new file mode 100644 index 00000000..fbbb6677 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic user features, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicUserService
+ {
+ /// <summary>
+ /// Check if a user exists.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <returns>True if exists. Otherwise false.</returns>
+ Task<bool> CheckUserExistence(long id);
+
+ /// <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);
+ }
+
+ public class BasicUserService : IBasicUserService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public BasicUserService(DatabaseContext database)
+ {
+ _database = database;
+ }
+
+ public async Task<bool> CheckUserExistence(long id)
+ {
+ return await _database.Users.AnyAsync(u => u.Id == id);
+ }
+
+ public async Task<long> GetUserIdByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ if (!_usernameValidator.Validate(username, out var message))
+ throw new ArgumentException(message);
+
+ var entity = await _database.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs index 70970b24..ef882ffe 100644 --- a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs +++ b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs @@ -6,7 +6,11 @@ namespace Timeline.Services.Exceptions [Serializable]
public class TimelineNotExistException : EntityNotExistException
{
- public TimelineNotExistException() : this(null, null) { }
+ public TimelineNotExistException() : this((long?)null) { }
+ public TimelineNotExistException(long? id) : this(id, null) { }
+ public TimelineNotExistException(long? id, Exception? inner) : this(id, null, inner) { }
+ public TimelineNotExistException(long? id, string? message, Exception? inner) : base(EntityNames.Timeline, null, message, inner) { TimelineId = id; }
+
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)
@@ -17,5 +21,6 @@ namespace Timeline.Services.Exceptions System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public string? TimelineName { get; set; }
+ public long? TimelineId { get; set; }
}
}
diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs new file mode 100644 index 00000000..0f4e5488 --- /dev/null +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public interface IHighlightTimelineService
+ {
+ /// <summary>
+ /// Get all highlight timelines.
+ /// </summary>
+ /// <returns>A list of all highlight timelines.</returns>
+ Task<List<TimelineInfo>> GetHighlightTimelines();
+
+ /// <summary>
+ /// Add a timeline to highlight list.
+ /// </summary>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given name does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task AddHighlightTimeline(string timelineName, long? operatorId);
+
+ /// <summary>
+ /// Remove a timeline from highlight list.
+ /// </summary>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <returns>True if deletion is actually performed. Otherwise false (timeline was not in the list).</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given name does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task<bool> RemoveHighlightTimeline(string timelineName, long? operatorId);
+ }
+
+ public class HighlightTimelineService : IHighlightTimelineService
+ {
+ private readonly DatabaseContext _database;
+ private readonly IBasicUserService _userService;
+ private readonly ITimelineService _timelineService;
+ private readonly IClock _clock;
+
+ public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService, IClock clock)
+ {
+ _database = database;
+ _userService = userService;
+ _timelineService = timelineService;
+ _clock = clock;
+ }
+
+ public async Task AddHighlightTimeline(string timelineName, long? operatorId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timelineName);
+
+ if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value))
+ {
+ throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null);
+ }
+
+ var alreadyIs = await _database.HighlightTimelines.AnyAsync(t => t.TimelineId == timelineId);
+
+ if (alreadyIs) return;
+
+ _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime() });
+ await _database.SaveChangesAsync();
+ }
+
+ public async Task<List<TimelineInfo>> GetHighlightTimelines()
+ {
+ var entities = await _database.HighlightTimelines.Select(t => new { t.Id }).ToListAsync();
+
+ var result = new List<TimelineInfo>();
+
+ foreach (var entity in entities)
+ {
+ result.Add(await _timelineService.GetTimelineById(entity.Id));
+ }
+
+ return result;
+ }
+
+ public async Task<bool> RemoveHighlightTimeline(string timelineName, long? operatorId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timelineName);
+
+ if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value))
+ {
+ throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null);
+ }
+
+ var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId);
+
+ if (entity == null) return false;
+
+ _database.HighlightTimelines.Remove(entity);
+ await _database.SaveChangesAsync();
+
+ return true;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..35513a36 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +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.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Services.TimelineService;
+
+namespace Timeline.Services
+{
+ 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?
+ }
+
+ public interface ITimelinePostService
+ {
+ /// <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<TimelinePostInfo>> 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<TimelinePostInfo> 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<TimelinePostInfo> 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>
+ /// 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);
+ }
+
+ public class TimelinePostService : ITimelinePostService
+ {
+ private readonly ILogger<TimelinePostService> _logger;
+ private readonly DatabaseContext _database;
+ private readonly IBasicTimelineService _basicTimelineService;
+ private readonly IUserService _userService;
+ private readonly IDataManager _dataManager;
+ private readonly IImageValidator _imageValidator;
+ private readonly IClock _clock;
+
+ public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
+ {
+ _logger = logger;
+ _database = database;
+ _basicTimelineService = basicTimelineService;
+ _userService = userService;
+ _dataManager = dataManager;
+ _imageValidator = imageValidator;
+ _clock = clock;
+ }
+
+ private async Task<TimelinePostInfo> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
+ {
+ UserInfo? author = entity.AuthorId.HasValue ? await _userService.GetUser(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 TimelinePostInfo(
+ id: entity.LocalId,
+ author: author,
+ content: content,
+ time: entity.Time,
+ lastUpdated: entity.LastUpdated,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<List<TimelinePostInfo>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
+ {
+ modifiedSince = modifiedSince?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(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<TimelinePostInfo>();
+ 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 _basicTimelineService.GetTimelineIdByName(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 _basicTimelineService.GetTimelineIdByName(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<TimelinePostInfo> 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 _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(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 TimelinePostInfo(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<TimelinePostInfo> 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 _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(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 TimelinePostInfo(
+ 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 _basicTimelineService.GetTimelineIdByName(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<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(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;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index 769e8bed..b65b3cf4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -1,13 +1,10 @@ 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.Tasks;
using Timeline.Entities;
-using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
@@ -51,20 +48,10 @@ namespace Timeline.Services 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
+ public interface ITimelineService : IBasicTimelineService
{
/// <summary>
/// Get the timeline last modified time (not include name change).
@@ -103,7 +90,15 @@ namespace Timeline.Services /// 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);
+ Task<TimelineInfo> GetTimeline(string timelineName);
+
+ /// <summary>
+ /// Get timeline by id.
+ /// </summary>
+ /// <param name="id">Id of timeline.</param>
+ /// <returns>The timeline.</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ Task<TimelineInfo> GetTimelineById(long id);
/// <summary>
/// Set the properties of a timeline.
@@ -119,113 +114,6 @@ namespace Timeline.Services 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>
@@ -285,30 +173,6 @@ namespace Timeline.Services 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>
@@ -335,7 +199,7 @@ namespace Timeline.Services /// <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);
+ Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
/// <summary>
/// Create a timeline.
@@ -347,7 +211,7 @@ namespace Timeline.Services /// <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);
+ Task<TimelineInfo> CreateTimeline(string timelineName, long ownerId);
/// <summary>
/// Delete a timeline.
@@ -371,31 +235,23 @@ namespace Timeline.Services /// <remarks>
/// You can only change name of general timeline.
/// </remarks>
- Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName);
+ Task<TimelineInfo> ChangeTimelineName(string oldTimelineName, string newTimelineName);
}
- public class TimelineService : ITimelineService
+ public class TimelineService : BasicTimelineService, ITimelineService
{
- public TimelineService(ILogger<TimelineService> logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock)
+ public TimelineService(DatabaseContext database, IUserService userService, IClock clock)
+ : base(database, userService, 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();
@@ -411,11 +267,11 @@ namespace Timeline.Services }
/// Remember to include Members when query.
- private async Task<Models.Timeline> MapTimelineFromEntity(TimelineEntity entity)
+ private async Task<TimelineInfo> MapTimelineFromEntity(TimelineEntity entity)
{
var owner = await _userService.GetUser(entity.OwnerId);
- var members = new List<User>();
+ var members = new List<UserInfo>();
foreach (var memberEntity in entity.Members)
{
members.Add(await _userService.GetUser(memberEntity.UserId));
@@ -423,129 +279,18 @@ namespace Timeline.Services 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.GetUser(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;
- }
- }
+ return new TimelineInfo(
+ entity.UniqueId,
+ name,
+ entity.NameLastModified,
+ string.IsNullOrEmpty(entity.Title) ? name : entity.Title,
+ entity.Description ?? "",
+ owner,
+ entity.Visibility,
+ members,
+ entity.CreateTime,
+ entity.LastModified
+ );
}
public async Task<DateTime> GetTimelineLastModifiedTime(string timelineName)
@@ -553,7 +298,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
@@ -565,279 +310,33 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(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)
+ public async Task<TimelineInfo> GetTimeline(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(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)
+ public async Task<TimelineInfo> GetTimelineById(long id)
{
- 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);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync();
- if (postEntity.Content == null)
- throw new TimelinePostNotExistException(timelineName, postId, true);
+ if (timelineEntity is null)
+ throw new TimelineNotExistException(id);
- 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.GetUser(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.GetUser(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);
- }
+ return await MapTimelineFromEntity(timelineEntity);
}
public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
@@ -847,7 +346,7 @@ namespace Timeline.Services if (newProperties == null)
throw new ArgumentNullException(nameof(newProperties));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
@@ -933,7 +432,7 @@ namespace Timeline.Services if (simplifiedAdd == null && simplifiedRemove == null)
return;
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
{
@@ -973,7 +472,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
return userId == timelineEntity.OwnerId;
@@ -984,7 +483,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
if (timelineEntity.Visibility == TimelineVisibility.Public)
@@ -1004,39 +503,12 @@ namespace Timeline.Services }
}
- 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 timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
@@ -1046,7 +518,7 @@ namespace Timeline.Services 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)
+ public async Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
{
List<TimelineEntity> entities;
@@ -1080,7 +552,7 @@ namespace Timeline.Services }
}
- var result = new List<Models.Timeline>();
+ var result = new List<TimelineInfo>();
foreach (var entity in entities)
{
@@ -1090,7 +562,7 @@ namespace Timeline.Services return result;
}
- public async Task<Models.Timeline> CreateTimeline(string name, long owner)
+ public async Task<TimelineInfo> CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -1128,7 +600,7 @@ namespace Timeline.Services await _database.SaveChangesAsync();
}
- public async Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName)
+ public async Task<TimelineInfo> ChangeTimelineName(string oldTimelineName, string newTimelineName)
{
if (oldTimelineName == null)
throw new ArgumentNullException(nameof(oldTimelineName));
diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs new file mode 100644 index 00000000..8aeef9ef --- /dev/null +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public interface IUserCredentialService
+ {
+ /// <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>User id.</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<long> VerifyCredential(string username, string password);
+
+ /// <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 UserCredentialService : IUserCredentialService
+ {
+ private readonly ILogger<UserCredentialService> _logger;
+ private readonly DatabaseContext _database;
+ private readonly IPasswordService _passwordService;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public UserCredentialService(ILogger<UserCredentialService> logger, DatabaseContext database, IPasswordService passwordService)
+ {
+ _logger = logger;
+ _database = database;
+ _passwordService = passwordService;
+ }
+
+ public async Task<long> VerifyCredential(string username, string password)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+ if (!_usernameValidator.Validate(username, out var message))
+ throw new ArgumentException(message);
+ if (password.Length == 0)
+ throw new ArgumentException("Password can't be empty.");
+
+ var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ if (!_passwordService.VerifyPassword(entity.Password, password))
+ throw new BadPasswordException(password);
+
+ return entity.Id;
+ }
+
+ 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));
+ if (oldPassword.Length == 0)
+ throw new ArgumentException("Old password can't be empty.");
+ if (newPassword.Length == 0)
+ throw new ArgumentException("New password can't be empty.");
+
+ var entity = await _database.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 _database.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs index 5365313b..a4e77abc 100644 --- a/BackEnd/Timeline/Services/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -31,15 +31,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext;
- private readonly ITimelineService _timelineService;
+ private readonly ITimelinePostService _timelinePostService;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
- public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelineService timelineService)
+ public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelinePostService timelinePostService)
{
_logger = logger;
_databaseContext = databaseContext;
- _timelineService = timelineService;
+ _timelinePostService = timelinePostService;
}
public async Task<bool> DeleteUser(string username)
@@ -59,7 +59,7 @@ namespace Timeline.Services if (user.Id == 1)
throw new InvalidOperationOnRootUserException("Can't delete root user.");
- await _timelineService.DeleteAllPostsOfUser(user.Id);
+ await _timelinePostService.DeleteAllPostsOfUser(user.Id);
_databaseContext.Users.Remove(user);
diff --git a/BackEnd/Timeline/Services/UserPermissionService.cs b/BackEnd/Timeline/Services/UserPermissionService.cs index 9683000a..bd7cd6aa 100644 --- a/BackEnd/Timeline/Services/UserPermissionService.cs +++ b/BackEnd/Timeline/Services/UserPermissionService.cs @@ -28,7 +28,7 @@ namespace Timeline.Services /// <summary>
/// Represents a user's permissions.
/// </summary>
- public class UserPermissions : IEnumerable<UserPermission>
+ public class UserPermissions : IEnumerable<UserPermission>, IEquatable<UserPermissions>
{
public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues<UserPermission>());
@@ -49,10 +49,10 @@ namespace Timeline.Services public UserPermissions(IEnumerable<UserPermission> permissions)
{
if (permissions == null) throw new ArgumentNullException(nameof(permissions));
- _permissions = new HashSet<UserPermission>(permissions);
+ _permissions = new SortedSet<UserPermission>(permissions);
}
- private readonly HashSet<UserPermission> _permissions = new();
+ private readonly SortedSet<UserPermission> _permissions = new();
/// <summary>
/// Check if a permission is contained in the list.
@@ -108,6 +108,33 @@ namespace Timeline.Services {
return ((IEnumerable)_permissions).GetEnumerator();
}
+
+ public bool Equals(UserPermissions? other)
+ {
+ if (other == null)
+ return false;
+
+ return _permissions.SequenceEqual(other._permissions);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as UserPermissions);
+ }
+
+ public override int GetHashCode()
+ {
+ int result = 0;
+ foreach (var permission in Enum.GetValues<UserPermission>())
+ {
+ if (_permissions.Contains(permission))
+ {
+ result += 1;
+ }
+ result <<= 1;
+ }
+ return result;
+ }
}
public interface IUserPermissionService
diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 2c5644cd..c99e86b0 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -17,50 +17,28 @@ namespace Timeline.Services /// <summary>
/// Null means not change.
/// </summary>
- public record ModifyUserParams
+ public class ModifyUserParams
{
public string? Username { get; set; }
public string? Password { get; set; }
public string? Nickname { get; set; }
}
- public interface IUserService
+ public interface IUserService : IBasicUserService
{
/// <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> GetUser(long id);
-
- /// <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);
+ Task<UserInfo> GetUser(long id);
/// <summary>
/// List all users.
/// </summary>
/// <returns>The user info of users.</returns>
- Task<List<User>> GetUsers();
+ Task<List<UserInfo>> GetUsers();
/// <summary>
/// Create a user with given info.
@@ -71,7 +49,7 @@ namespace Timeline.Services /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="username"/> or <paramref name="password"/> is of bad format.</exception>
/// <exception cref="EntityAlreadyExistException">Thrown when a user with given username already exists.</exception>
- Task<User> CreateUser(string username, string password);
+ Task<UserInfo> CreateUser(string username, string password);
/// <summary>
/// Modify a user.
@@ -84,22 +62,10 @@ namespace Timeline.Services /// <remarks>
/// Version will increase if password is changed.
/// </remarks>
- Task<User> ModifyUser(long id, ModifyUserParams? param);
-
- /// <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);
+ Task<UserInfo> ModifyUser(long id, ModifyUserParams? param);
}
- public class UserService : IUserService
+ public class UserService : BasicUserService, IUserService
{
private readonly ILogger<UserService> _logger;
private readonly IClock _clock;
@@ -112,13 +78,13 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator();
private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService)
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserPermissionService userPermissionService, IClock clock) : base(databaseContext)
{
_logger = logger;
- _clock = clock;
_databaseContext = databaseContext;
_passwordService = passwordService;
_userPermissionService = userPermissionService;
+ _clock = clock;
}
private void CheckUsernameFormat(string username, string? paramName)
@@ -150,45 +116,23 @@ namespace Timeline.Services throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict);
}
- private async Task<User> CreateUserFromEntity(UserEntity entity)
+ private async Task<UserInfo> CreateUserFromEntity(UserEntity entity)
{
var permission = await _userPermissionService.GetPermissionsOfUserAsync(entity.Id);
- return new User
- {
- UniqueId = entity.UniqueId,
- Username = entity.Username,
- Permissions = permission,
- 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 await CreateUserFromEntity(entity);
+ return new UserInfo(
+ entity.Id,
+ entity.UniqueId,
+ entity.Username,
+ string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
+ permission,
+ entity.UsernameChangeTime,
+ entity.CreateTime,
+ entity.LastModified,
+ entity.Version
+ );
}
- public async Task<User> GetUser(long id)
+ public async Task<UserInfo> GetUser(long id)
{
var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
@@ -198,24 +142,9 @@ namespace Timeline.Services return await CreateUserFromEntity(user);
}
- 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<List<User>> GetUsers()
+ public async Task<List<UserInfo>> GetUsers()
{
- List<User> result = new();
+ List<UserInfo> result = new();
foreach (var entity in await _databaseContext.Users.ToArrayAsync())
{
result.Add(await CreateUserFromEntity(entity));
@@ -223,7 +152,7 @@ namespace Timeline.Services return result;
}
- public async Task<User> CreateUser(string username, string password)
+ public async Task<UserInfo> CreateUser(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
@@ -251,7 +180,7 @@ namespace Timeline.Services return await CreateUserFromEntity(newEntity);
}
- public async Task<User> ModifyUser(long id, ModifyUserParams? param)
+ public async Task<UserInfo> ModifyUser(long id, ModifyUserParams? param)
{
if (param != null)
{
@@ -311,28 +240,5 @@ namespace Timeline.Services return await 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/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 09ecd19c..b887b987 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -10,7 +10,7 @@ namespace Timeline.Services public class UserTokenCreateResult
{
public string Token { get; set; } = default!;
- public User User { get; set; } = default!;
+ public UserInfo User { get; set; } = default!;
}
public interface IUserTokenManager
@@ -38,20 +38,22 @@ namespace Timeline.Services /// <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 Task<UserInfo> VerifyToken(string token);
}
public class UserTokenManager : IUserTokenManager
{
private readonly ILogger<UserTokenManager> _logger;
private readonly IUserService _userService;
+ private readonly IUserCredentialService _userCredentialService;
private readonly IUserTokenService _userTokenService;
private readonly IClock _clock;
- public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
+ public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserCredentialService userCredentialService, IUserTokenService userTokenService, IClock clock)
{
_logger = logger;
_userService = userService;
+ _userCredentialService = userCredentialService;
_userTokenService = userTokenService;
_clock = clock;
}
@@ -65,14 +67,15 @@ namespace Timeline.Services if (password == null)
throw new ArgumentNullException(nameof(password));
- var user = await _userService.VerifyCredential(username, password);
+ var userId = await _userCredentialService.VerifyCredential(username, password);
+ var user = await _userService.GetUser(userId);
var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt });
return new UserTokenCreateResult { Token = token, User = user };
}
- public async Task<User> VerifyToken(string token)
+ public async Task<UserInfo> VerifyToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
|