From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- BackEnd/Timeline/Models/ByteData.cs | 33 +++ .../Models/Converters/JsonDateTimeConverter.cs | 23 ++ .../Models/Converters/MyDateTimeConverter.cs | 51 ++++ .../Models/Http/ActionContextAccessorExtensions.cs | 14 ++ BackEnd/Timeline/Models/Http/Common.cs | 120 ++++++++++ BackEnd/Timeline/Models/Http/ErrorResponse.cs | 261 +++++++++++++++++++++ BackEnd/Timeline/Models/Http/Timeline.cs | 219 +++++++++++++++++ BackEnd/Timeline/Models/Http/TimelineController.cs | 93 ++++++++ BackEnd/Timeline/Models/Http/TokenController.cs | 62 +++++ BackEnd/Timeline/Models/Http/UserController.cs | 93 ++++++++ BackEnd/Timeline/Models/Http/UserInfo.cs | 90 +++++++ BackEnd/Timeline/Models/Timeline.cs | 98 ++++++++ BackEnd/Timeline/Models/User.cs | 21 ++ .../Validation/GeneralTimelineNameValidator.cs | 33 +++ .../Timeline/Models/Validation/NameValidator.cs | 42 ++++ .../Models/Validation/NicknameValidator.cs | 25 ++ .../Models/Validation/TimelineNameValidator.cs | 19 ++ .../Models/Validation/UsernameValidator.cs | 19 ++ BackEnd/Timeline/Models/Validation/Validator.cs | 127 ++++++++++ 19 files changed, 1443 insertions(+) create mode 100644 BackEnd/Timeline/Models/ByteData.cs create mode 100644 BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs create mode 100644 BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs create mode 100644 BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs create mode 100644 BackEnd/Timeline/Models/Http/Common.cs create mode 100644 BackEnd/Timeline/Models/Http/ErrorResponse.cs create mode 100644 BackEnd/Timeline/Models/Http/Timeline.cs create mode 100644 BackEnd/Timeline/Models/Http/TimelineController.cs create mode 100644 BackEnd/Timeline/Models/Http/TokenController.cs create mode 100644 BackEnd/Timeline/Models/Http/UserController.cs create mode 100644 BackEnd/Timeline/Models/Http/UserInfo.cs create mode 100644 BackEnd/Timeline/Models/Timeline.cs create mode 100644 BackEnd/Timeline/Models/User.cs create mode 100644 BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/NameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/NicknameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/UsernameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/Validator.cs (limited to 'BackEnd/Timeline/Models') diff --git a/BackEnd/Timeline/Models/ByteData.cs b/BackEnd/Timeline/Models/ByteData.cs new file mode 100644 index 00000000..7b832eb5 --- /dev/null +++ b/BackEnd/Timeline/Models/ByteData.cs @@ -0,0 +1,33 @@ +using NSwag.Annotations; + +namespace Timeline.Models +{ + /// + /// Model for reading http body as bytes. + /// + [OpenApiFile] + public class ByteData + { + /// + /// + /// The data. + /// The content type. + public ByteData(byte[] data, string contentType) + { + Data = data; + ContentType = contentType; + } + + /// + /// Data. + /// +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; } +#pragma warning restore CA1819 // Properties should not return arrays + + /// + /// Content type. + /// + public string ContentType { get; } + } +} diff --git a/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs b/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs new file mode 100644 index 00000000..94b5cab0 --- /dev/null +++ b/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Timeline.Helpers; + +namespace Timeline.Models.Converters +{ + public class JsonDateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert == typeof(DateTime)); + return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.MyToUtc().ToString("s", CultureInfo.InvariantCulture) + "Z"); + } + } +} diff --git a/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs b/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs new file mode 100644 index 00000000..f125cd5c --- /dev/null +++ b/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Timeline.Models.Converters +{ + public class MyDateTimeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return base.CanConvertTo(context, destinationType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string text) + { + text = text.Trim(); + if (text.Length == 0) + { + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + } + + return DateTime.Parse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(string) && value is DateTime) + { + DateTime dt = (DateTime)value; + if (dt == DateTime.MinValue) + { + return string.Empty; + } + + return dt.ToString("s", CultureInfo.InvariantCulture) + "Z"; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs b/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs new file mode 100644 index 00000000..bcc55c5a --- /dev/null +++ b/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using System; + +namespace Timeline.Models.Http +{ + public static class ActionContextAccessorExtensions + { + public static ActionContext AssertActionContextForUrlFill(this IActionContextAccessor accessor) + { + return accessor.ActionContext ?? throw new InvalidOperationException(Resources.Models.Http.Exception.ActionContextNull); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/Common.cs b/BackEnd/Timeline/Models/Http/Common.cs new file mode 100644 index 00000000..5fa22c9e --- /dev/null +++ b/BackEnd/Timeline/Models/Http/Common.cs @@ -0,0 +1,120 @@ +using static Timeline.Resources.Models.Http.Common; + +namespace Timeline.Models.Http +{ + public class CommonResponse + { + public CommonResponse() + { + + } + + public CommonResponse(int code, string message) + { + Code = code; + Message = message; + } + + public int Code { get; set; } + public string? Message { get; set; } + } + + public class CommonDataResponse : CommonResponse + { + public CommonDataResponse() + { + + } + + public CommonDataResponse(int code, string message, T data) + : base(code, message) + { + Data = data; + } + + public T Data { get; set; } = default!; + } + + public class CommonPutResponse : CommonDataResponse + { + public class ResponseData + { + public ResponseData() { } + + public ResponseData(bool create) + { + Create = create; + } + + public bool Create { get; set; } + } + + public CommonPutResponse() + { + + } + + public CommonPutResponse(int code, string message, bool create) + : base(code, message, new ResponseData(create)) + { + + } + + internal static CommonPutResponse Create() + { + return new CommonPutResponse(0, MessagePutCreate, true); + } + + internal static CommonPutResponse Modify() + { + return new CommonPutResponse(0, MessagePutModify, false); + } + } + + /// + /// Common response for delete method. + /// + public class CommonDeleteResponse : CommonDataResponse + { + /// + public class ResponseData + { + /// + public ResponseData() { } + + /// + public ResponseData(bool delete) + { + Delete = delete; + } + + /// + /// True if the entry is deleted. False if the entry does not exist. + /// + public bool Delete { get; set; } + } + + /// + public CommonDeleteResponse() + { + + } + + /// + public CommonDeleteResponse(int code, string message, bool delete) + : base(code, message, new ResponseData(delete)) + { + + } + + internal static CommonDeleteResponse Delete() + { + return new CommonDeleteResponse(0, MessageDeleteDelete, true); + } + + internal static CommonDeleteResponse NotExist() + { + return new CommonDeleteResponse(0, MessageDeleteNotExist, false); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/ErrorResponse.cs b/BackEnd/Timeline/Models/Http/ErrorResponse.cs new file mode 100644 index 00000000..ac86481f --- /dev/null +++ b/BackEnd/Timeline/Models/Http/ErrorResponse.cs @@ -0,0 +1,261 @@ +using static Timeline.Resources.Messages; + +namespace Timeline.Models.Http +{ + public static class ErrorResponse + { + public static class Common + { + public static CommonResponse InvalidModel(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(Common_InvalidModel, formatArgs)); + } + + public static CommonResponse CustomMessage_InvalidModel(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(message, formatArgs)); + } + + public static CommonResponse Forbid(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(Common_Forbid, formatArgs)); + } + + public static CommonResponse CustomMessage_Forbid(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(message, formatArgs)); + } + + public static CommonResponse UnknownEndpoint(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(Common_UnknownEndpoint, formatArgs)); + } + + public static CommonResponse CustomMessage_UnknownEndpoint(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(message, formatArgs)); + } + + public static class Header + { + public static CommonResponse IfNonMatch_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(Common_Header_IfNonMatch_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_IfNonMatch_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(message, formatArgs)); + } + + } + + public static class Content + { + public static CommonResponse TooBig(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(Common_Content_TooBig, formatArgs)); + } + + public static CommonResponse CustomMessage_TooBig(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(message, formatArgs)); + } + + } + + } + + public static class UserCommon + { + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(UserCommon_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(message, formatArgs)); + } + + } + + public static class TokenController + { + public static CommonResponse Create_BadCredential(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(TokenController_Create_BadCredential, formatArgs)); + } + + public static CommonResponse CustomMessage_Create_BadCredential(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(TokenController_Verify_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_UserNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(TokenController_Verify_UserNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_UserNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_OldVersion(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(TokenController_Verify_OldVersion, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_OldVersion(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_TimeExpired(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(TokenController_Verify_TimeExpired, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_TimeExpired(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(message, formatArgs)); + } + + } + + public static class UserController + { + public static CommonResponse UsernameConflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs)); + } + + public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs)); + } + + public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(UserController_ChangePassword_BadOldPassword, formatArgs)); + } + + public static CommonResponse CustomMessage_ChangePassword_BadOldPassword(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(message, formatArgs)); + } + + } + + public static class UserAvatar + { + public static CommonResponse BadFormat_CantDecode(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(UserAvatar_BadFormat_CantDecode, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_CantDecode(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_UnmatchedFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(UserAvatar_BadFormat_UnmatchedFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_UnmatchedFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_BadSize(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(UserAvatar_BadFormat_BadSize, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_BadSize(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(message, formatArgs)); + } + + } + + public static class TimelineController + { + public static CommonResponse NameConflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(TimelineController_NameConflict, formatArgs)); + } + + public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(message, formatArgs)); + } + + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(TimelineController_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse QueryRelateNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(TimelineController_QueryRelateNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_QueryRelateNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse PostNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(TimelineController_PostNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_PostNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse PostNoData(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(TimelineController_PostNoData, formatArgs)); + } + + public static CommonResponse CustomMessage_PostNoData(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(message, formatArgs)); + } + + } + + } + +} diff --git a/BackEnd/Timeline/Models/Http/Timeline.cs b/BackEnd/Timeline/Models/Http/Timeline.cs new file mode 100644 index 00000000..a81b33f5 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/Timeline.cs @@ -0,0 +1,219 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using System; +using System.Collections.Generic; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Info of post content. + /// + public class TimelinePostContentInfo + { + /// + /// Type of the post content. + /// + public string Type { get; set; } = default!; + /// + /// If post is of text type. This is the text. + /// + public string? Text { get; set; } + /// + /// If post is of image type. This is the image url. + /// + public string? Url { get; set; } + /// + /// If post has data (currently it means it's a image post), this is the data etag. + /// + public string? ETag { get; set; } + } + + /// + /// Info of a post. + /// + public class TimelinePostInfo + { + /// + /// Post id. + /// + public long Id { get; set; } + /// + /// Content of the post. May be null if post is deleted. + /// + public TimelinePostContentInfo? Content { get; set; } + /// + /// True if post is deleted. + /// + public bool Deleted { get; set; } + /// + /// Post time. + /// + public DateTime Time { get; set; } + /// + /// The author. May be null if the user has been deleted. + /// + public UserInfo? Author { get; set; } = default!; + /// + /// Last updated time. + /// + public DateTime LastUpdated { get; set; } = default!; + } + + /// + /// Info of a timeline. + /// + public class TimelineInfo + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Title. + /// + public string Title { get; set; } = default!; + /// + /// Name of timeline. + /// + public string Name { get; set; } = default!; + /// + /// Last modified time of timeline name. + /// + public DateTime NameLastModifed { get; set; } = default!; + /// + /// Timeline description. + /// + public string Description { get; set; } = default!; + /// + /// Owner of the timeline. + /// + public UserInfo Owner { get; set; } = default!; + /// + /// Visibility of the timeline. + /// + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + /// + /// Members of timeline. + /// + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + /// + /// Create time of timeline. + /// + public DateTime CreateTime { get; set; } = default!; + /// + /// Last modified time of timeline. + /// + public DateTime LastModified { get; set; } = default!; + +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public TimelineInfoLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for timeline. + /// + public class TimelineInfoLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Posts url. + /// + public string Posts { get; set; } = default!; + } + + public class TimelineInfoLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + return new TimelineInfoLinks + { + Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }), + Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) + }; + } + } + + public class TimelinePostContentResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var sourceContent = source.Content; + + if (sourceContent == null) + { + return null; + } + + if (sourceContent is TextTimelinePostContent textContent) + { + return new TimelinePostContentInfo + { + Type = TimelinePostContentTypes.Text, + Text = textContent.Text + }; + } + else if (sourceContent is ImageTimelinePostContent imageContent) + { + return new TimelinePostContentInfo + { + Type = TimelinePostContentTypes.Image, + Url = urlHelper.ActionLink( + action: nameof(TimelineController.PostDataGet), + controller: nameof(TimelineController)[0..^nameof(Controller).Length], + values: new { Name = source.TimelineName, Id = source.Id }), + ETag = $"\"{imageContent.DataTag}\"" + }; + } + else + { + throw new InvalidOperationException(Resources.Models.Http.Exception.UnknownPostContentType); + } + } + } + + public class TimelineInfoAutoMapperProfile : Profile + { + public TimelineInfoAutoMapperProfile() + { + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); + CreateMap(); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs new file mode 100644 index 00000000..7bd141ed --- /dev/null +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -0,0 +1,93 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Content of post create request. + /// + public class TimelinePostCreateRequestContent + { + /// + /// Type of post content. + /// + [Required] + public string Type { get; set; } = default!; + /// + /// If post is of text type, this is the text. + /// + public string? Text { get; set; } + /// + /// If post is of image type, this is base64 of image data. + /// + public string? Data { get; set; } + } + + public class TimelinePostCreateRequest + { + /// + /// Content of the new post. + /// + [Required] + public TimelinePostCreateRequestContent Content { get; set; } = default!; + + /// + /// Time of the post. If not set, current time will be used. + /// + public DateTime? Time { get; set; } + } + + /// + /// Create timeline request model. + /// + public class TimelineCreateRequest + { + /// + /// Name of the new timeline. Must be a valid name. + /// + [Required] + [TimelineName] + public string Name { get; set; } = default!; + } + + /// + /// Patch timeline request model. + /// + public class TimelinePatchRequest + { + /// + /// New title. Null for not change. + /// + public string? Title { get; set; } + + /// + /// New description. Null for not change. + /// + public string? Description { get; set; } + + /// + /// New visibility. Null for not change. + /// + public TimelineVisibility? Visibility { get; set; } + } + + /// + /// Change timeline name request model. + /// + public class TimelineChangeNameRequest + { + /// + /// Old name of timeline. + /// + [Required] + [TimelineName] + public string OldName { get; set; } = default!; + /// + /// New name of timeline. + /// + [Required] + [TimelineName] + public string NewName { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Models/Http/TokenController.cs b/BackEnd/Timeline/Models/Http/TokenController.cs new file mode 100644 index 00000000..a42c44e5 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/TokenController.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Request model for . + /// + public class CreateTokenRequest + { + /// + /// The username. + /// + public string Username { get; set; } = default!; + /// + /// The password. + /// + public string Password { get; set; } = default!; + /// + /// Optional token validation period. In days. If not specified, server will use a default one. + /// + [Range(1, 365)] + public int? Expire { get; set; } + } + + /// + /// Response model for . + /// + public class CreateTokenResponse + { + /// + /// The token created. + /// + public string Token { get; set; } = default!; + /// + /// The user owning the token. + /// + public UserInfo User { get; set; } = default!; + } + + /// + /// Request model for . + /// + public class VerifyTokenRequest + { + /// + /// The token to verify. + /// + public string Token { get; set; } = default!; + } + + /// + /// Response model for . + /// + public class VerifyTokenResponse + { + /// + /// The user owning the token. + /// + public UserInfo User { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Models/Http/UserController.cs b/BackEnd/Timeline/Models/Http/UserController.cs new file mode 100644 index 00000000..6bc5a66e --- /dev/null +++ b/BackEnd/Timeline/Models/Http/UserController.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using System.ComponentModel.DataAnnotations; +using Timeline.Controllers; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Request model for . + /// + public class UserPatchRequest + { + /// + /// New username. Null if not change. Need to be administrator. + /// + [Username] + public string? Username { get; set; } + + /// + /// New password. Null if not change. Need to be administrator. + /// + [MinLength(1)] + public string? Password { get; set; } + + /// + /// New nickname. Null if not change. Need to be administrator to change other's. + /// + [Nickname] + public string? Nickname { get; set; } + + /// + /// Whether to be administrator. Null if not change. Need to be administrator. + /// + public bool? Administrator { get; set; } + } + + /// + /// Request model for . + /// + public class CreateUserRequest + { + /// + /// Username of the new user. + /// + [Required, Username] + public string Username { get; set; } = default!; + + /// + /// Password of the new user. + /// + [Required, MinLength(1)] + public string Password { get; set; } = default!; + + /// + /// Whether the new user is administrator. + /// + [Required] + public bool? Administrator { get; set; } + + /// + /// Nickname of the new user. + /// + [Nickname] + public string? Nickname { get; set; } + } + + /// + /// Request model for . + /// + public class ChangePasswordRequest + { + /// + /// Old password. + /// + [Required(AllowEmptyStrings = false)] + public string OldPassword { get; set; } = default!; + + /// + /// New password. + /// + [Required(AllowEmptyStrings = false)] + public string NewPassword { get; set; } = default!; + } + + public class UserControllerAutoMapperProfile : Profile + { + public UserControllerAutoMapperProfile() + { + CreateMap(MemberList.Source); + CreateMap(MemberList.Source); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/UserInfo.cs new file mode 100644 index 00000000..d92a12c4 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/UserInfo.cs @@ -0,0 +1,90 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Info of a user. + /// + public class UserInfo + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Username. + /// + public string Username { get; set; } = default!; + /// + /// Nickname. + /// + public string Nickname { get; set; } = default!; + /// + /// True if the user is a administrator. + /// + public bool? Administrator { get; set; } = default!; +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public UserInfoLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for user. + /// + public class UserInfoLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Avatar url. + /// + public string Avatar { get; set; } = default!; + /// + /// Personal timeline url. + /// + public string Timeline { get; set; } = default!; + } + + public class UserInfoLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var result = new UserInfoLinks + { + Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) + }; + return result; + } + } + + public class UserInfoAutoMapperProfile : Profile + { + public UserInfoAutoMapperProfile() + { + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + } + } +} diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/Timeline.cs new file mode 100644 index 00000000..a5987577 --- /dev/null +++ b/BackEnd/Timeline/Models/Timeline.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + + public static class TimelinePostContentTypes + { + public const string Text = "text"; + public const string Image = "image"; + } + + public interface ITimelinePostContent + { + public string Type { get; } + } + + public class TextTimelinePostContent : ITimelinePostContent + { + public TextTimelinePostContent(string text) { Text = text; } + + public string Type { get; } = TimelinePostContentTypes.Text; + public string Text { get; set; } + } + + public class ImageTimelinePostContent : ITimelinePostContent + { + public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } + + public string Type { get; } = TimelinePostContentTypes.Image; + + /// + /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). + /// + public string DataTag { get; set; } + } + + public class TimelinePost + { + public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) + { + Id = id; + Content = content; + Time = time; + Author = author; + LastUpdated = lastUpdated; + TimelineName = timelineName; + } + + public long Id { get; set; } + public ITimelinePostContent? Content { get; set; } + public bool Deleted => Content == null; + public DateTime Time { get; set; } + public User? Author { get; set; } + public DateTime LastUpdated { get; set; } + public string TimelineName { get; set; } + } + +#pragma warning disable CA1724 // Type names should not match namespaces + public class Timeline +#pragma warning restore CA1724 // Type names should not match namespaces + { + public string UniqueID { get; set; } = default!; + public string Name { get; set; } = default!; + public DateTime NameLastModified { get; set; } = default!; + public string Title { get; set; } = default!; + public string Description { get; set; } = default!; + public User Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + public DateTime CreateTime { get; set; } = default!; + public DateTime LastModified { get; set; } = default!; + } + + public class TimelineChangePropertyRequest + { + public string? Title { get; set; } + public string? Description { get; set; } + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs new file mode 100644 index 00000000..f08a62db --- /dev/null +++ b/BackEnd/Timeline/Models/User.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Models +{ + public class User + { + public string? UniqueId { get; set; } + public string? Username { get; set; } + public string? Nickname { get; set; } + public bool? Administrator { get; set; } + + #region secret + public long? Id { get; set; } + public string? Password { get; set; } + public long? Version { get; set; } + public DateTime? UsernameChangeTime { get; set; } + public DateTime? CreateTime { get; set; } + public DateTime? LastModified { get; set; } + #endregion secret + } +} diff --git a/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs b/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs new file mode 100644 index 00000000..e1c96fbd --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs @@ -0,0 +1,33 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class GeneralTimelineNameValidator : Validator + { + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); + + protected override (bool, string) DoValidate(string value) + { + if (value.StartsWith('@')) + { + return _usernameValidator.Validate(value.Substring(1)); + } + else + { + return _timelineNameValidator.Validate(value); + } + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class GeneralTimelineNameAttribute : ValidateWithAttribute + { + public GeneralTimelineNameAttribute() + : base(typeof(GeneralTimelineNameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/NameValidator.cs b/BackEnd/Timeline/Models/Validation/NameValidator.cs new file mode 100644 index 00000000..b74c40b7 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/NameValidator.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Text.RegularExpressions; +using static Timeline.Resources.Models.Validation.NameValidator; + +namespace Timeline.Models.Validation +{ + public class NameValidator : Validator + { + private static Regex UniqueIdRegex { get; } = new Regex(@"^[a-zA-Z0-9]{32}$"); + + public const int MaxLength = 26; + + protected override (bool, string) DoValidate(string value) + { + if (value.Length == 0) + { + return (false, MessageEmptyString); + } + + if (value.Length > MaxLength) + { + return (false, MessageTooLong); + } + + foreach ((char c, int i) in value.Select((c, i) => (c, i))) + { + if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_')) + { + return (false, MessageInvalidChar); + } + } + + // Currently name can't be longer than 26. So this is not needed. But reserve it for future use. + if (UniqueIdRegex.IsMatch(value)) + { + return (false, MessageUnqiueId); + } + + return (true, GetSuccessMessage()); + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/NicknameValidator.cs b/BackEnd/Timeline/Models/Validation/NicknameValidator.cs new file mode 100644 index 00000000..1d6ab163 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/NicknameValidator.cs @@ -0,0 +1,25 @@ +using System; +using static Timeline.Resources.Models.Validation.NicknameValidator; + +namespace Timeline.Models.Validation +{ + public class NicknameValidator : Validator + { + protected override (bool, string) DoValidate(string value) + { + if (value.Length > 25) + return (false, MessageTooLong); + + return (true, GetSuccessMessage()); + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class NicknameAttribute : ValidateWithAttribute + { + public NicknameAttribute() : base(typeof(NicknameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs new file mode 100644 index 00000000..f1ab54e8 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs @@ -0,0 +1,19 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class TimelineNameValidator : NameValidator + { + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class TimelineNameAttribute : ValidateWithAttribute + { + public TimelineNameAttribute() + : base(typeof(TimelineNameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/UsernameValidator.cs b/BackEnd/Timeline/Models/Validation/UsernameValidator.cs new file mode 100644 index 00000000..87bbf85f --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/UsernameValidator.cs @@ -0,0 +1,19 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class UsernameValidator : NameValidator + { + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class UsernameAttribute : ValidateWithAttribute + { + public UsernameAttribute() + : base(typeof(UsernameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/Validator.cs b/BackEnd/Timeline/Models/Validation/Validator.cs new file mode 100644 index 00000000..aef7891c --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/Validator.cs @@ -0,0 +1,127 @@ +using System; +using System.ComponentModel.DataAnnotations; +using static Timeline.Resources.Models.Validation.Validator; + +namespace Timeline.Models.Validation +{ + /// + /// A validator to validate value. + /// + public interface IValidator + { + /// + /// Validate given value. + /// + /// The value to validate. + /// Validation success or not and message. + (bool, string) Validate(object? value); + } + + public static class ValidatorExtensions + { + public static bool Validate(this IValidator validator, object? value, out string message) + { + if (validator == null) + throw new ArgumentNullException(nameof(validator)); + + var (r, m) = validator.Validate(value); + message = m; + return r; + } + } + + /// + /// Convenient base class for validator. + /// + /// The type of accepted value. + /// + /// Subclass should override to do the real validation. + /// This class will check the nullity and type of value. + /// If value is null, it will pass or fail depending on . + /// If value is not null and not of type + /// it will fail and not call . + /// + /// is true by default. + /// + /// If you want some other behaviours, write the validator from scratch. + /// + public abstract class Validator : IValidator + { + protected bool PermitNull { get; set; } = true; + + public (bool, string) Validate(object? value) + { + if (value == null) + { + if (PermitNull) + return (true, GetSuccessMessage()); + else + return (false, ValidatorMessageNull); + } + + if (value is T v) + { + return DoValidate(v); + } + else + { + return (false, ValidatorMessageBadType); + } + } + + protected static string GetSuccessMessage() => ValidatorMessageSuccess; + + protected abstract (bool, string) DoValidate(T value); + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class ValidateWithAttribute : ValidationAttribute + { + private readonly IValidator _validator; + + /// + /// Create with a given validator. + /// + /// The validator used to validate. + public ValidateWithAttribute(IValidator validator) + { + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + } + + /// + /// Create the validator with default constructor. + /// + /// The type of the validator. + public ValidateWithAttribute(Type validatorType) + { + if (validatorType == null) + throw new ArgumentNullException(nameof(validatorType)); + + if (!typeof(IValidator).IsAssignableFrom(validatorType)) + throw new ArgumentException(ValidateWithAttributeExceptionNotValidator, nameof(validatorType)); + + try + { + _validator = (Activator.CreateInstance(validatorType) as IValidator)!; + } + catch (Exception e) + { + throw new ArgumentException(ValidateWithAttributeExceptionCreateFail, e); + } + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var (result, message) = _validator.Validate(value); + if (result) + { + return ValidationResult.Success; + } + else + { + return new ValidationResult(message); + } + } + } +} -- cgit v1.2.3