diff options
author | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
commit | 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 (patch) | |
tree | 929e514de85eb82a5acb96ecffc6e6d2d95f878f /BackEnd/Timeline | |
parent | 986c6f2e3b858d6332eba0b42acc6861cd4d0227 (diff) | |
download | timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.gz timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.bz2 timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.zip |
Split front and back end.
Diffstat (limited to 'BackEnd/Timeline')
165 files changed, 20326 insertions, 0 deletions
diff --git a/BackEnd/Timeline/Auth/Attribute.cs b/BackEnd/Timeline/Auth/Attribute.cs new file mode 100644 index 00000000..86d0109b --- /dev/null +++ b/BackEnd/Timeline/Auth/Attribute.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization;
+using Timeline.Entities;
+
+namespace Timeline.Auth
+{
+ public class AdminAuthorizeAttribute : AuthorizeAttribute
+ {
+ public AdminAuthorizeAttribute()
+ {
+ Roles = UserRoles.Admin;
+ }
+ }
+
+ public class UserAuthorizeAttribute : AuthorizeAttribute
+ {
+ public UserAuthorizeAttribute()
+ {
+ Roles = UserRoles.User;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs new file mode 100644 index 00000000..3c97c329 --- /dev/null +++ b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Timeline.Services;
+using static Timeline.Resources.Authentication.AuthHandler;
+
+namespace Timeline.Auth
+{
+ public static class AuthenticationConstants
+ {
+ public const string Scheme = "Bearer";
+ public const string DisplayName = "My Jwt Auth Scheme";
+ }
+
+ public class MyAuthenticationOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// The query param key to search for token. If null then query params are not searched for token. Default to <c>"token"</c>.
+ /// </summary>
+ public string TokenQueryParamKey { get; set; } = "token";
+ }
+
+ public class MyAuthenticationHandler : AuthenticationHandler<MyAuthenticationOptions>
+ {
+ private readonly ILogger<MyAuthenticationHandler> _logger;
+ private readonly IUserTokenManager _userTokenManager;
+
+ public MyAuthenticationHandler(IOptionsMonitor<MyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager)
+ : base(options, logger, encoder, clock)
+ {
+ _logger = logger.CreateLogger<MyAuthenticationHandler>();
+ _userTokenManager = userTokenManager;
+ }
+
+ // return null if no token is found
+ private string? ExtractToken()
+ {
+ // check the authorization header
+ string header = Request.Headers[HeaderNames.Authorization];
+ if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase))
+ {
+ var token = header.Substring("Bearer ".Length).Trim();
+ _logger.LogInformation(LogTokenFoundInHeader, token);
+ return token;
+ }
+
+ // check the query params
+ var paramQueryKey = Options.TokenQueryParamKey;
+ if (!string.IsNullOrEmpty(paramQueryKey))
+ {
+ string token = Request.Query[paramQueryKey];
+ if (!string.IsNullOrEmpty(token))
+ {
+ _logger.LogInformation(LogTokenFoundInQuery, paramQueryKey, token);
+ return token;
+ }
+ }
+
+ // not found anywhere then return null
+ return null;
+ }
+
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var token = ExtractToken();
+ if (string.IsNullOrEmpty(token))
+ {
+ _logger.LogInformation(LogTokenNotFound);
+ return AuthenticateResult.NoResult();
+ }
+
+ try
+ {
+ var userInfo = await _userTokenManager.VerifyToken(token);
+
+ var identity = new ClaimsIdentity(AuthenticationConstants.Scheme);
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64));
+ identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String));
+ identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String)));
+
+ var principal = new ClaimsPrincipal();
+ principal.AddIdentity(identity);
+
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme));
+ }
+ catch (Exception e) when (!(e is ArgumentException))
+ {
+ _logger.LogInformation(e, LogTokenValidationFail);
+ return AuthenticateResult.Fail(e);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Auth/PrincipalExtensions.cs b/BackEnd/Timeline/Auth/PrincipalExtensions.cs new file mode 100644 index 00000000..ad7a887f --- /dev/null +++ b/BackEnd/Timeline/Auth/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal;
+using Timeline.Entities;
+
+namespace Timeline.Auth
+{
+ internal static class PrincipalExtensions
+ {
+ internal static bool IsAdministrator(this IPrincipal principal)
+ {
+ return principal.IsInRole(UserRoles.Admin);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Configs/ApplicationConfiguration.cs b/BackEnd/Timeline/Configs/ApplicationConfiguration.cs new file mode 100644 index 00000000..df281adb --- /dev/null +++ b/BackEnd/Timeline/Configs/ApplicationConfiguration.cs @@ -0,0 +1,13 @@ +namespace Timeline.Configs
+{
+ public static class ApplicationConfiguration
+ {
+ public const string WorkDirKey = "WorkDir";
+ public const string DefaultWorkDir = "/timeline";
+ public const string DatabaseFileName = "timeline.db";
+ public const string DatabaseBackupDirectoryName = "backup";
+ public const string DisableFrontEndKey = "DisableFrontEnd";
+ public const string UseMockFrontEndKey = "UseMockFrontEnd";
+ public const string UseProxyFrontEndKey = "UseProxyFrontEnd";
+ }
+}
diff --git a/BackEnd/Timeline/Configs/JwtConfiguration.cs b/BackEnd/Timeline/Configs/JwtConfiguration.cs new file mode 100644 index 00000000..af8052de --- /dev/null +++ b/BackEnd/Timeline/Configs/JwtConfiguration.cs @@ -0,0 +1,14 @@ +namespace Timeline.Configs
+{
+ public class JwtConfiguration
+ {
+ public string Issuer { get; set; } = default!;
+ public string Audience { get; set; } = default!;
+
+ /// <summary>
+ /// Set the default value of expire offset of jwt token.
+ /// Unit is second. Default is 3600 * 24 seconds, aka 1 day.
+ /// </summary>
+ public long DefaultExpireOffset { get; set; } = 3600 * 24;
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs b/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs new file mode 100644 index 00000000..00a65454 --- /dev/null +++ b/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Security.Claims;
+using Timeline.Auth;
+using static Timeline.Resources.Controllers.ControllerAuthExtensions;
+
+namespace Timeline.Controllers
+{
+ public static class ControllerAuthExtensions
+ {
+ public static bool IsAdministrator(this ControllerBase controller)
+ {
+ return controller.User != null && controller.User.IsAdministrator();
+ }
+
+ public static long GetUserId(this ControllerBase controller)
+ {
+ var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier);
+ if (claim == null)
+ throw new InvalidOperationException(ExceptionNoUserIdentifierClaim);
+
+ if (long.TryParse(claim.Value, out var value))
+ return value;
+
+ throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat);
+ }
+
+ public static long? GetOptionalUserId(this ControllerBase controller)
+ {
+ var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier);
+ if (claim == null)
+ return null;
+
+ if (long.TryParse(claim.Value, out var value))
+ return value;
+
+ throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs b/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs new file mode 100644 index 00000000..4d3b3ec7 --- /dev/null +++ b/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Timeline.Auth;
+
+namespace Timeline.Controllers.Testing
+{
+ [Route("testing/auth")]
+ [ApiController]
+ public class TestingAuthController : Controller
+ {
+ [HttpGet("[action]")]
+ [Authorize]
+ public ActionResult Authorize()
+ {
+ return Ok();
+ }
+
+ [HttpGet("[action]")]
+ [UserAuthorize]
+ public new ActionResult User()
+ {
+ return Ok();
+ }
+
+ [HttpGet("[action]")]
+ [AdminAuthorize]
+ public ActionResult Admin()
+ {
+ return Ok();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs new file mode 100644 index 00000000..9a3147ea --- /dev/null +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -0,0 +1,491 @@ +using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using Timeline.Filters;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Operations about timeline.
+ /// </summary>
+ [ApiController]
+ [CatchTimelineNotExistException]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class TimelineController : Controller
+ {
+ private readonly ILogger<TimelineController> _logger;
+
+ private readonly IUserService _userService;
+ private readonly ITimelineService _service;
+
+ private readonly IMapper _mapper;
+
+ /// <summary>
+ ///
+ /// </summary>
+ public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, IMapper mapper)
+ {
+ _logger = logger;
+ _userService = userService;
+ _service = service;
+ _mapper = mapper;
+ }
+
+ /// <summary>
+ /// List all timelines.
+ /// </summary>
+ /// <param name="relate">A username. If set, only timelines related to the user will return.</param>
+ /// <param name="relateType">Specify the relation type, may be 'own' or 'join'. If not set, both type will return.</param>
+ /// <param name="visibility">"Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return.</param>
+ /// <returns>The timeline list.</returns>
+ [HttpGet("timelines")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult<List<TimelineInfo>>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility)
+ {
+ List<TimelineVisibility>? visibilityFilter = null;
+ if (visibility != null)
+ {
+ visibilityFilter = new List<TimelineVisibility>();
+ var items = visibility.Split('|');
+ foreach (var item in items)
+ {
+ if (item.Equals(nameof(TimelineVisibility.Private), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!visibilityFilter.Contains(TimelineVisibility.Private))
+ visibilityFilter.Add(TimelineVisibility.Private);
+ }
+ else if (item.Equals(nameof(TimelineVisibility.Register), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!visibilityFilter.Contains(TimelineVisibility.Register))
+ visibilityFilter.Add(TimelineVisibility.Register);
+ }
+ else if (item.Equals(nameof(TimelineVisibility.Public), StringComparison.OrdinalIgnoreCase))
+ {
+ if (!visibilityFilter.Contains(TimelineVisibility.Public))
+ visibilityFilter.Add(TimelineVisibility.Public);
+ }
+ else
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_QueryVisibilityUnknown, item));
+ }
+ }
+ }
+
+ TimelineUserRelationship? relationship = null;
+ if (relate != null)
+ {
+ try
+ {
+ var relatedUserId = await _userService.GetUserIdByUsername(relate);
+
+ relationship = new TimelineUserRelationship(relateType switch
+ {
+ "own" => TimelineUserRelationshipType.Own,
+ "join" => TimelineUserRelationshipType.Join,
+ _ => TimelineUserRelationshipType.Default
+ }, relatedUserId);
+ }
+ catch (UserNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.QueryRelateNotExist());
+ }
+ }
+
+ var timelines = await _service.GetTimelines(relationship, visibilityFilter);
+ var result = _mapper.Map<List<TimelineInfo>>(timelines);
+ return result;
+ }
+
+ /// <summary>
+ /// Get info of a timeline.
+ /// </summary>
+ /// <param name="name">The timeline name.</param>
+ /// <param name="checkUniqueId">A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified.</param>
+ /// <param name="queryIfModifiedSince">Same effect as If-Modified-Since header and take precedence than it.</param>
+ /// <param name="headerIfModifiedSince">If specified, will return 304 if not modified.</param>
+ /// <returns>The timeline info.</returns>
+ [HttpGet("timelines/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status304NotModified)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince)
+ {
+ DateTime? ifModifiedSince = null;
+ if (queryIfModifiedSince.HasValue)
+ {
+ ifModifiedSince = queryIfModifiedSince.Value;
+ }
+ else if (headerIfModifiedSince != null)
+ {
+ ifModifiedSince = headerIfModifiedSince.Value;
+ }
+
+ bool returnNotModified = false;
+
+ if (ifModifiedSince.HasValue)
+ {
+ var lastModified = await _service.GetTimelineLastModifiedTime(name);
+ if (lastModified < ifModifiedSince.Value)
+ {
+ if (checkUniqueId != null)
+ {
+ var uniqueId = await _service.GetTimelineUniqueId(name);
+ if (uniqueId == checkUniqueId)
+ {
+ returnNotModified = true;
+ }
+ }
+ else
+ {
+ returnNotModified = true;
+ }
+ }
+ }
+
+ if (returnNotModified)
+ {
+ return StatusCode(StatusCodes.Status304NotModified);
+ }
+ else
+ {
+ var timeline = await _service.GetTimeline(name);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Get posts of a timeline.
+ /// </summary>
+ /// <param name="name">The name of the timeline.</param>
+ /// <param name="modifiedSince">If set, only posts modified since the time will return.</param>
+ /// <param name="includeDeleted">If set to true, deleted post will also return.</param>
+ /// <returns>The post list.</returns>
+ [HttpGet("timelines/{name}/posts")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<List<TimelinePostInfo>>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
+ {
+ if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ List<TimelinePost> posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false);
+
+ var result = _mapper.Map<List<TimelinePostInfo>>(posts);
+ return result;
+ }
+
+ /// <summary>
+ /// Get the data of a post. Usually a image post.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="id">The id of the post.</param>
+ /// <param name="ifNoneMatch">If-None-Match header.</param>
+ /// <returns>The data.</returns>
+ [HttpGet("timelines/{name}/posts/{id}/data")]
+ [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")]
+ [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ {
+ _ = ifNoneMatch;
+ if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () =>
+ {
+ var data = await _service.GetPostData(name, id);
+ return data;
+ });
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return NotFound(ErrorResponse.TimelineController.PostNotExist());
+ }
+ catch (TimelinePostNoDataException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.PostNoData());
+ }
+ }
+
+ /// <summary>
+ /// Create a new post.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="body"></param>
+ /// <returns>Info of new post.</returns>
+ [HttpPost("timelines/{name}/posts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body)
+ {
+ var id = this.GetUserId();
+ if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ var content = body.Content;
+
+ TimelinePost post;
+
+ if (content.Type == TimelinePostContentTypes.Text)
+ {
+ var text = content.Text;
+ if (text == null)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
+ }
+ post = await _service.CreateTextPost(name, id, text, body.Time);
+ }
+ else if (content.Type == TimelinePostContentTypes.Image)
+ {
+ var base64Data = content.Data;
+ if (base64Data == null)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired));
+ }
+ byte[] data;
+ try
+ {
+ data = Convert.FromBase64String(base64Data);
+ }
+ catch (FormatException)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64));
+ }
+
+ try
+ {
+ post = await _service.CreateImagePost(name, id, data, body.Time);
+ }
+ catch (ImageException)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
+ }
+ }
+ else
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
+ }
+
+ var result = _mapper.Map<TimelinePostInfo>(post);
+ return result;
+ }
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="id">Post id.</param>
+ /// <returns>Info of deletion.</returns>
+ [HttpDelete("timelines/{name}/posts/{id}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<CommonDeleteResponse>> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
+ {
+ if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+ try
+ {
+ await _service.DeletePost(name, id);
+ return CommonDeleteResponse.Delete();
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return CommonDeleteResponse.NotExist();
+ }
+ }
+
+ /// <summary>
+ /// Change properties of a timeline.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="body"></param>
+ /// <returns>The new info.</returns>
+ [HttpPatch("timelines/{name}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body)
+ {
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+ await _service.ChangeProperty(name, _mapper.Map<TimelineChangePropertyRequest>(body));
+ var timeline = await _service.GetTimeline(name);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
+ }
+
+ /// <summary>
+ /// Add a member to timeline.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="member">The new member's username.</param>
+ [HttpPut("timelines/{name}/members/{member}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> TimelineMemberPut([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
+ {
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ await _service.ChangeMember(name, new List<string> { member }, null);
+ return Ok();
+ }
+ catch (UserNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist());
+ }
+ }
+
+ /// <summary>
+ /// Remove a member from timeline.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <param name="member">The member's username.</param>
+ [HttpDelete("timelines/{name}/members/{member}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> TimelineMemberDelete([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
+ {
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ await _service.ChangeMember(name, null, new List<string> { member });
+ return Ok(CommonDeleteResponse.Delete());
+ }
+ catch (UserNotExistException)
+ {
+ return Ok(CommonDeleteResponse.NotExist());
+ }
+ }
+
+ /// <summary>
+ /// Create a timeline.
+ /// </summary>
+ /// <param name="body"></param>
+ /// <returns>Info of new timeline.</returns>
+ [HttpPost("timelines")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public async Task<ActionResult<TimelineInfo>> TimelineCreate([FromBody] TimelineCreateRequest body)
+ {
+ var userId = this.GetUserId();
+
+ try
+ {
+ var timeline = await _service.CreateTimeline(body.Name, userId);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
+ }
+ catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline)
+ {
+ return BadRequest(ErrorResponse.TimelineController.NameConflict());
+ }
+ }
+
+ /// <summary>
+ /// Delete a timeline.
+ /// </summary>
+ /// <param name="name">Timeline name.</param>
+ /// <returns>Info of deletion.</returns>
+ [HttpDelete("timelines/{name}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<CommonDeleteResponse>> TimelineDelete([FromRoute][TimelineName] string name)
+ {
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ await _service.DeleteTimeline(name);
+ return CommonDeleteResponse.Delete();
+ }
+ catch (TimelineNotExistException)
+ {
+ return CommonDeleteResponse.NotExist();
+ }
+ }
+
+ [HttpPost("timelineop/changename")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<TimelineInfo>> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body)
+ {
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(body.OldName, this.GetUserId())))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName);
+ return Ok(_mapper.Map<TimelineInfo>(timeline));
+ }
+ catch (EntityAlreadyExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.NameConflict());
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs new file mode 100644 index 00000000..8f2ca600 --- /dev/null +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -0,0 +1,142 @@ +using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Timeline.Helpers;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Controllers.TokenController;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Operation about tokens.
+ /// </summary>
+ [Route("token")]
+ [ApiController]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class TokenController : Controller
+ {
+ private readonly IUserTokenManager _userTokenManager;
+ private readonly ILogger<TokenController> _logger;
+ private readonly IClock _clock;
+
+ private readonly IMapper _mapper;
+
+ /// <summary></summary>
+ public TokenController(IUserTokenManager userTokenManager, ILogger<TokenController> logger, IClock clock, IMapper mapper)
+ {
+ _userTokenManager = userTokenManager;
+ _logger = logger;
+ _clock = clock;
+ _mapper = mapper;
+ }
+
+ /// <summary>
+ /// Create a new token for a user.
+ /// </summary>
+ /// <returns>Result of token creation.</returns>
+ [HttpPost("create")]
+ [AllowAnonymous]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult<CreateTokenResponse>> Create([FromBody] CreateTokenRequest request)
+ {
+ void LogFailure(string reason, Exception? e = null)
+ {
+ _logger.LogInformation(e, Log.Format(LogCreateFailure,
+ ("Reason", reason),
+ ("Username", request.Username),
+ ("Password", request.Password),
+ ("Expire (in days)", request.Expire)
+ ));
+ }
+
+ try
+ {
+ DateTime? expireTime = null;
+ if (request.Expire != null)
+ expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value);
+
+ var result = await _userTokenManager.CreateToken(request.Username, request.Password, expireTime);
+
+ _logger.LogInformation(Log.Format(LogCreateSuccess,
+ ("Username", request.Username),
+ ("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default")
+ ));
+ return Ok(new CreateTokenResponse
+ {
+ Token = result.Token,
+ User = _mapper.Map<UserInfo>(result.User)
+ });
+ }
+ catch (UserNotExistException e)
+ {
+ LogFailure(LogUserNotExist, e);
+ return BadRequest(ErrorResponse.TokenController.Create_BadCredential());
+ }
+ catch (BadPasswordException e)
+ {
+ LogFailure(LogBadPassword, e);
+ return BadRequest(ErrorResponse.TokenController.Create_BadCredential());
+ }
+ }
+
+ /// <summary>
+ /// Verify a token.
+ /// </summary>
+ /// <returns>Result of token verification.</returns>
+ [HttpPost("verify")]
+ [AllowAnonymous]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult<VerifyTokenResponse>> Verify([FromBody] VerifyTokenRequest request)
+ {
+ void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties)
+ {
+ var properties = new (string, object?)[2 + otherProperties.Length];
+ properties[0] = ("Reason", reason);
+ properties[1] = ("Token", request.Token);
+ otherProperties.CopyTo(properties, 2);
+ _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties));
+ }
+
+ try
+ {
+ var result = await _userTokenManager.VerifyToken(request.Token);
+ _logger.LogInformation(Log.Format(LogVerifySuccess,
+ ("Username", result.Username), ("Token", request.Token)));
+ return Ok(new VerifyTokenResponse
+ {
+ User = _mapper.Map<UserInfo>(result)
+ });
+ }
+ catch (UserTokenTimeExpireException e)
+ {
+ LogFailure(LogVerifyExpire, e, ("Expire Time", e.ExpireTime), ("Verify Time", e.VerifyTime));
+ return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired());
+ }
+ catch (UserTokenBadVersionException e)
+ {
+ LogFailure(LogVerifyOldVersion, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion));
+ return BadRequest(ErrorResponse.TokenController.Verify_OldVersion());
+
+ }
+ catch (UserTokenBadFormatException e)
+ {
+ LogFailure(LogVerifyBadFormat, e);
+ return BadRequest(ErrorResponse.TokenController.Verify_BadFormat());
+ }
+ catch (UserNotExistException e)
+ {
+ LogFailure(LogVerifyUserNotExist, e);
+ return BadRequest(ErrorResponse.TokenController.Verify_UserNotExist());
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs new file mode 100644 index 00000000..bc4afa30 --- /dev/null +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+using Timeline.Auth;
+using Timeline.Filters;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Controllers.UserAvatarController;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Operations about user avatar.
+ /// </summary>
+ [ApiController]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class UserAvatarController : Controller
+ {
+ private readonly ILogger<UserAvatarController> _logger;
+
+ private readonly IUserService _userService;
+ private readonly IUserAvatarService _service;
+
+ /// <summary>
+ ///
+ /// </summary>
+ public UserAvatarController(ILogger<UserAvatarController> logger, IUserService userService, IUserAvatarService service)
+ {
+ _logger = logger;
+ _userService = userService;
+ _service = service;
+ }
+
+ /// <summary>
+ /// Get avatar of a user.
+ /// </summary>
+ /// <param name="username">Username of the user to get avatar of.</param>
+ /// <param name="ifNoneMatch">If-None-Match header.</param>
+ /// <returns>Avatar data.</returns>
+ [HttpGet("users/{username}/avatar")]
+ [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")]
+ [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> Get([FromRoute][Username] string username, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ {
+ _ = ifNoneMatch;
+ long id;
+ try
+ {
+ id = await _userService.GetUserIdByUsername(username);
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username)));
+ return NotFound(ErrorResponse.UserCommon.NotExist());
+ }
+
+ return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarETag(id), async () =>
+ {
+ var avatar = await _service.GetAvatar(id);
+ return avatar.ToCacheableData();
+ });
+ }
+
+ /// <summary>
+ /// Set avatar of a user. You have to be administrator to change other's.
+ /// </summary>
+ /// <param name="username">Username of the user to set avatar of.</param>
+ /// <param name="body">The avatar data.</param>
+ [HttpPut("users/{username}/avatar")]
+ [Authorize]
+ [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")]
+ [MaxContentLength(1000 * 1000 * 10)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<IActionResult> Put([FromRoute][Username] string username, [FromBody] ByteData body)
+ {
+ if (!User.IsAdministrator() && User.Identity.Name != username)
+ {
+ _logger.LogInformation(Log.Format(LogPutForbid,
+ ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username)));
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ long id;
+ try
+ {
+ id = await _userService.GetUserIdByUsername(username);
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username)));
+ return BadRequest(ErrorResponse.UserCommon.NotExist());
+ }
+
+ try
+ {
+ var etag = await _service.SetAvatar(id, new Avatar
+ {
+ Data = body.Data,
+ Type = body.ContentType
+ });
+
+ _logger.LogInformation(Log.Format(LogPutSuccess,
+ ("Username", username), ("Mime Type", Request.ContentType)));
+
+ Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{etag}\"").ToString());
+
+ return Ok();
+ }
+ catch (ImageException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username)));
+ return BadRequest(e.Error switch
+ {
+ ImageException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(),
+ ImageException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(),
+ ImageException.ErrorReason.NotSquare => ErrorResponse.UserAvatar.BadFormat_BadSize(),
+ _ =>
+ throw new Exception(ExceptionUnknownAvatarFormatError)
+ });
+ }
+ }
+
+ /// <summary>
+ /// Reset the avatar to the default one. You have to be administrator to reset other's.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <response code="200">Succeeded to reset.</response>
+ /// <response code="400">Error code is 10010001 if user does not exist.</response>
+ /// <response code="401">You have not logged in.</response>
+ /// <response code="403">You are not administrator.</response>
+ [HttpDelete("users/{username}/avatar")]
+ [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [Authorize]
+ public async Task<IActionResult> Delete([FromRoute][Username] string username)
+ {
+ if (!User.IsAdministrator() && User.Identity.Name != username)
+ {
+ _logger.LogInformation(Log.Format(LogDeleteForbid,
+ ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username)));
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ long id;
+ try
+ {
+ id = await _userService.GetUserIdByUsername(username);
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username)));
+ return BadRequest(ErrorResponse.UserCommon.NotExist());
+ }
+
+ await _service.SetAvatar(id, null);
+ return Ok();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs new file mode 100644 index 00000000..02c09aab --- /dev/null +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -0,0 +1,195 @@ +using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Auth;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Controllers.UserController;
+using static Timeline.Resources.Messages;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Operations about users.
+ /// </summary>
+ [ApiController]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class UserController : Controller
+ {
+ private readonly ILogger<UserController> _logger;
+ private readonly IUserService _userService;
+ private readonly IUserDeleteService _userDeleteService;
+ private readonly IMapper _mapper;
+
+ /// <summary></summary>
+ public UserController(ILogger<UserController> logger, IUserService userService, IUserDeleteService userDeleteService, IMapper mapper)
+ {
+ _logger = logger;
+ _userService = userService;
+ _userDeleteService = userDeleteService;
+ _mapper = mapper;
+ }
+
+ private UserInfo ConvertToUserInfo(User user) => _mapper.Map<UserInfo>(user);
+
+ /// <summary>
+ /// Get all users.
+ /// </summary>
+ /// <returns>All user list.</returns>
+ [HttpGet("users")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<UserInfo[]>> List()
+ {
+ var users = await _userService.GetUsers();
+ var result = users.Select(u => ConvertToUserInfo(u)).ToArray();
+ return Ok(result);
+ }
+
+ /// <summary>
+ /// Get a user's info.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>User info.</returns>
+ [HttpGet("users/{username}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<UserInfo>> Get([FromRoute][Username] string username)
+ {
+ try
+ {
+ var user = await _userService.GetUserByUsername(username);
+ return Ok(ConvertToUserInfo(user));
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username)));
+ return NotFound(ErrorResponse.UserCommon.NotExist());
+ }
+ }
+
+ /// <summary>
+ /// Change a user's property.
+ /// </summary>
+ /// <param name="body"></param>
+ /// <param name="username">Username of the user to change.</param>
+ /// <returns>The new user info.</returns>
+ [HttpPatch("users/{username}"), Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<UserInfo>> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username)
+ {
+ if (this.IsAdministrator())
+ {
+ try
+ {
+ var user = await _userService.ModifyUser(username, _mapper.Map<User>(body));
+ return Ok(ConvertToUserInfo(user));
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username)));
+ return NotFound(ErrorResponse.UserCommon.NotExist());
+ }
+ catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User)
+ {
+ return BadRequest(ErrorResponse.UserController.UsernameConflict());
+ }
+ }
+ else
+ {
+ if (User.Identity.Name != username)
+ return StatusCode(StatusCodes.Status403Forbidden,
+ ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf));
+
+ if (body.Username != null)
+ return StatusCode(StatusCodes.Status403Forbidden,
+ ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username));
+
+ if (body.Password != null)
+ return StatusCode(StatusCodes.Status403Forbidden,
+ ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password));
+
+ if (body.Administrator != null)
+ return StatusCode(StatusCodes.Status403Forbidden,
+ ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator));
+
+ var user = await _userService.ModifyUser(this.GetUserId(), _mapper.Map<User>(body));
+ return Ok(ConvertToUserInfo(user));
+ }
+ }
+
+ /// <summary>
+ /// Delete a user and all his related data. You have to be administrator.
+ /// </summary>
+ /// <param name="username">Username of the user to delete.</param>
+ /// <returns>Info of deletion.</returns>
+ [HttpDelete("users/{username}"), AdminAuthorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<CommonDeleteResponse>> Delete([FromRoute][Username] string username)
+ {
+ var delete = await _userDeleteService.DeleteUser(username);
+ if (delete)
+ return Ok(CommonDeleteResponse.Delete());
+ else
+ return Ok(CommonDeleteResponse.NotExist());
+ }
+
+ /// <summary>
+ /// Create a new user. You have to be administrator.
+ /// </summary>
+ /// <returns>The new user's info.</returns>
+ [HttpPost("userop/createuser"), AdminAuthorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<UserInfo>> CreateUser([FromBody] CreateUserRequest body)
+ {
+ try
+ {
+ var user = await _userService.CreateUser(_mapper.Map<User>(body));
+ return Ok(ConvertToUserInfo(user));
+ }
+ catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User)
+ {
+ return BadRequest(ErrorResponse.UserController.UsernameConflict());
+ }
+ }
+
+ /// <summary>
+ /// Change password with old password.
+ /// </summary>
+ [HttpPost("userop/changepassword"), Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public async Task<ActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
+ {
+ try
+ {
+ await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword);
+ return Ok();
+ }
+ catch (BadPasswordException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword,
+ ("Username", User.Identity.Name), ("Old Password", request.OldPassword)));
+ return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword());
+ }
+ // User can't be non-existent or the token is bad.
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Entities/DataEntity.cs b/BackEnd/Timeline/Entities/DataEntity.cs new file mode 100644 index 00000000..b21e2dbf --- /dev/null +++ b/BackEnd/Timeline/Entities/DataEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("data")]
+ public class DataEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("tag"), Required]
+ public string Tag { get; set; } = default!;
+
+ [Column("data"), Required]
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; } = default!;
+#pragma warning restore CA1819 // Properties should not return arrays
+
+ [Column("ref"), Required]
+ public int Ref { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs new file mode 100644 index 00000000..ecadd703 --- /dev/null +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore;
+
+namespace Timeline.Entities
+{
+ public class DatabaseContext : DbContext
+ {
+ public DatabaseContext(DbContextOptions<DatabaseContext> options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity<UserEntity>().Property(e => e.Version).HasDefaultValue(0);
+ modelBuilder.Entity<UserEntity>().HasIndex(e => e.Username).IsUnique();
+ modelBuilder.Entity<UserEntity>().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))");
+ modelBuilder.Entity<UserEntity>().Property(e => e.UsernameChangeTime).HasDefaultValueSql("datetime('now', 'utc')");
+ modelBuilder.Entity<UserEntity>().Property(e => e.CreateTime).HasDefaultValueSql("datetime('now', 'utc')");
+ modelBuilder.Entity<UserEntity>().Property(e => e.LastModified).HasDefaultValueSql("datetime('now', 'utc')");
+ modelBuilder.Entity<DataEntity>().HasIndex(e => e.Tag).IsUnique();
+ modelBuilder.Entity<TimelineEntity>().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ modelBuilder.ApplyUtcDateTimeConverter();
+ }
+
+ public DbSet<UserEntity> Users { get; set; } = default!;
+ public DbSet<UserAvatarEntity> UserAvatars { get; set; } = default!;
+ public DbSet<TimelineEntity> Timelines { get; set; } = default!;
+ public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+ public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
+ public DbSet<JwtTokenEntity> JwtToken { get; set; } = default!;
+ public DbSet<DataEntity> Data { get; set; } = default!;
+ }
+}
diff --git a/BackEnd/Timeline/Entities/JwtTokenEntity.cs b/BackEnd/Timeline/Entities/JwtTokenEntity.cs new file mode 100644 index 00000000..40cb230a --- /dev/null +++ b/BackEnd/Timeline/Entities/JwtTokenEntity.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("jwt_token")]
+ public class JwtTokenEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Required, Column("key")]
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Key { get; set; } = default!;
+#pragma warning restore CA1819 // Properties should not return arrays
+ }
+}
diff --git a/BackEnd/Timeline/Entities/TimelineEntity.cs b/BackEnd/Timeline/Entities/TimelineEntity.cs new file mode 100644 index 00000000..3e592673 --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelineEntity.cs @@ -0,0 +1,58 @@ +using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Timeline.Models;
+
+namespace Timeline.Entities
+{
+#pragma warning disable CA2227 // Collection properties should be read only
+ // TODO: Create index for this table.
+ [Table("timelines")]
+ public class TimelineEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("unique_id"), Required]
+ public string UniqueId { get; set; } = default!;
+
+ /// <summary>
+ /// If null, then this timeline is a personal timeline.
+ /// </summary>
+ [Column("name")]
+ public string? Name { get; set; }
+
+ [Column("title")]
+ public string? Title { get; set; }
+
+ [Column("name_last_modified")]
+ public DateTime NameLastModified { get; set; }
+
+ [Column("description")]
+ public string? Description { get; set; }
+
+ [Column("owner")]
+ public long OwnerId { get; set; }
+
+ [ForeignKey(nameof(OwnerId))]
+ public UserEntity Owner { get; set; } = default!;
+
+ [Column("visibility")]
+ public TimelineVisibility Visibility { get; set; }
+
+ [Column("create_time")]
+ public DateTime CreateTime { get; set; }
+
+ [Column("last_modified")]
+ public DateTime LastModified { get; set; }
+
+ [Column("current_post_local_id")]
+ public long CurrentPostLocalId { get; set; }
+
+ public List<TimelineMemberEntity> Members { get; set; } = default!;
+
+ public List<TimelinePostEntity> Posts { get; set; } = default!;
+ }
+#pragma warning restore CA2227 // Collection properties should be read only
+}
diff --git a/BackEnd/Timeline/Entities/TimelineMemberEntity.cs b/BackEnd/Timeline/Entities/TimelineMemberEntity.cs new file mode 100644 index 00000000..e76f2099 --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelineMemberEntity.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("timeline_members")]
+ public class TimelineMemberEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("user")]
+ public long UserId { get; set; }
+
+ [ForeignKey(nameof(UserId))]
+ public UserEntity User { get; set; } = default!;
+
+ [Column("timeline")]
+ public long TimelineId { get; set; }
+
+ [ForeignKey(nameof(TimelineId))]
+ public TimelineEntity Timeline { get; set; } = default!;
+ }
+}
diff --git a/BackEnd/Timeline/Entities/TimelinePostEntity.cs b/BackEnd/Timeline/Entities/TimelinePostEntity.cs new file mode 100644 index 00000000..07367fba --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelinePostEntity.cs @@ -0,0 +1,43 @@ +using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("timeline_posts")]
+ public class TimelinePostEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("local_id")]
+ public long LocalId { get; set; }
+
+ [Column("timeline")]
+ public long TimelineId { get; set; }
+
+ [ForeignKey(nameof(TimelineId))]
+ public TimelineEntity Timeline { get; set; } = default!;
+
+ [Column("author")]
+ public long? AuthorId { get; set; }
+
+ [ForeignKey(nameof(AuthorId))]
+ public UserEntity? Author { get; set; } = default!;
+
+ [Column("content_type"), Required]
+ public string ContentType { get; set; } = default!;
+
+ [Column("content")]
+ public string? Content { get; set; }
+
+ [Column("extra_content")]
+ public string? ExtraContent { get; set; }
+
+ [Column("time")]
+ public DateTime Time { get; set; }
+
+ [Column("last_updated")]
+ public DateTime LastUpdated { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Entities/UserAvatarEntity.cs b/BackEnd/Timeline/Entities/UserAvatarEntity.cs new file mode 100644 index 00000000..3c2720f7 --- /dev/null +++ b/BackEnd/Timeline/Entities/UserAvatarEntity.cs @@ -0,0 +1,29 @@ +using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
+ [Table("user_avatars")]
+ public class UserAvatarEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("data_tag")]
+ public string? DataTag { get; set; }
+
+ [Column("type")]
+ public string? Type { get; set; }
+
+ [Column("last_modified"), Required]
+ public DateTime LastModified { get; set; }
+
+ [Column("user"), Required]
+ public long UserId { get; set; }
+
+ [ForeignKey(nameof(UserId))]
+ public UserEntity User { get; set; } = default!;
+ }
+}
diff --git a/BackEnd/Timeline/Entities/UserEntity.cs b/BackEnd/Timeline/Entities/UserEntity.cs new file mode 100644 index 00000000..0cfaa335 --- /dev/null +++ b/BackEnd/Timeline/Entities/UserEntity.cs @@ -0,0 +1,56 @@ +using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ public static class UserRoles
+ {
+ public const string Admin = "admin";
+ public const string User = "user";
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")]
+ [Table("users")]
+ public class UserEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("unique_id"), Required]
+ public string UniqueId { get; set; } = default!;
+
+ [Column("username"), Required]
+ public string Username { get; set; } = default!;
+
+ [Column("username_change_time")]
+ public DateTime UsernameChangeTime { get; set; }
+
+ [Column("password"), Required]
+ public string Password { get; set; } = default!;
+
+ [Column("roles"), Required]
+ public string Roles { get; set; } = default!;
+
+ [Column("version"), Required]
+ public long Version { get; set; }
+
+ [Column("nickname")]
+ public string? Nickname { get; set; }
+
+ [Column("create_time")]
+ public DateTime CreateTime { get; set; }
+
+ [Column("last_modified")]
+ public DateTime LastModified { get; set; }
+
+ public UserAvatarEntity? Avatar { get; set; }
+
+ public List<TimelineEntity> Timelines { get; set; } = default!;
+
+ public List<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+
+ public List<TimelineMemberEntity> TimelinesJoined { get; set; } = default!;
+ }
+}
diff --git a/BackEnd/Timeline/Entities/UtcDateAnnotation.cs b/BackEnd/Timeline/Entities/UtcDateAnnotation.cs new file mode 100644 index 00000000..6600e701 --- /dev/null +++ b/BackEnd/Timeline/Entities/UtcDateAnnotation.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using System;
+
+namespace Timeline.Entities
+{
+ // Copied from https://github.com/dotnet/efcore/issues/4711#issuecomment-589842988
+ public static class UtcDateAnnotation
+ {
+ private const string IsUtcAnnotation = "IsUtc";
+ private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
+ new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
+
+ public static PropertyBuilder<TProperty> IsUtc<TProperty>(this PropertyBuilder<TProperty> builder, bool isUtc = true) =>
+ builder.HasAnnotation(IsUtcAnnotation, isUtc);
+
+ public static bool IsUtc(this IMutableProperty property) =>
+ ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true;
+
+ /// <summary>
+ /// Make sure this is called after configuring all your entities.
+ /// </summary>
+ public static void ApplyUtcDateTimeConverter(this ModelBuilder builder)
+ {
+ foreach (var entityType in builder.Model.GetEntityTypes())
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ if (!property.IsUtc())
+ {
+ continue;
+ }
+
+ if (property.ClrType == typeof(DateTime))
+ {
+ property.SetValueConverter(UtcConverter);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Filters/Header.cs b/BackEnd/Timeline/Filters/Header.cs new file mode 100644 index 00000000..cc5ddd9f --- /dev/null +++ b/BackEnd/Timeline/Filters/Header.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Timeline.Models.Http;
+
+namespace Timeline.Filters
+{
+ /// <summary>
+ /// Restrict max content length.
+ /// </summary>
+ public class MaxContentLengthFilter : IResourceFilter
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="maxByteLength">Max length.</param>
+ public MaxContentLengthFilter(long maxByteLength)
+ {
+ MaxByteLength = maxByteLength;
+ }
+
+ /// <summary>
+ /// Max length.
+ /// </summary>
+ public long MaxByteLength { get; set; }
+
+ /// <inheritdoc/>
+ public void OnResourceExecuted(ResourceExecutedContext context)
+ {
+ }
+
+ /// <inheritdoc/>
+ public void OnResourceExecuting(ResourceExecutingContext context)
+ {
+ var contentLength = context.HttpContext.Request.ContentLength;
+ if (contentLength != null && contentLength > MaxByteLength)
+ {
+ context.Result = new BadRequestObjectResult(ErrorResponse.Common.Content.TooBig(MaxByteLength + "B"));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Restrict max content length.
+ /// </summary>
+ public class MaxContentLengthAttribute : TypeFilterAttribute
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="maxByteLength">Max length.</param>
+ public MaxContentLengthAttribute(long maxByteLength)
+ : base(typeof(MaxContentLengthFilter))
+ {
+ MaxByteLength = maxByteLength;
+ Arguments = new object[] { maxByteLength };
+ }
+
+ /// <summary>
+ /// Max length.
+ /// </summary>
+ public long MaxByteLength { get; }
+ }
+}
diff --git a/BackEnd/Timeline/Filters/Timeline.cs b/BackEnd/Timeline/Filters/Timeline.cs new file mode 100644 index 00000000..6a730ee7 --- /dev/null +++ b/BackEnd/Timeline/Filters/Timeline.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Timeline.Models.Http;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Filters
+{
+ public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute
+ {
+ public override void OnException(ExceptionContext context)
+ {
+ if (context.Exception is TimelineNotExistException e)
+ {
+ if (e.InnerException is UserNotExistException)
+ {
+ if (HttpMethods.IsGet(context.HttpContext.Request.Method))
+ context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist());
+ else
+ context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist());
+ }
+ else
+ {
+ if (HttpMethods.IsGet(context.HttpContext.Request.Method))
+ context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist());
+ else
+ context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist());
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Formatters/BytesInputFormatter.cs b/BackEnd/Timeline/Formatters/BytesInputFormatter.cs new file mode 100644 index 00000000..ac6537c9 --- /dev/null +++ b/BackEnd/Timeline/Formatters/BytesInputFormatter.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+using Timeline.Models;
+
+namespace Timeline.Formatters
+{
+ /// <summary>
+ /// Formatter that reads body as bytes.
+ /// </summary>
+ public class BytesInputFormatter : InputFormatter
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ public BytesInputFormatter()
+ {
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/webp"));
+ }
+
+ /// <inheritdoc/>
+ public override bool CanRead(InputFormatterContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ if (context.ModelType == typeof(ByteData))
+ return true;
+
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
+ {
+ var request = context.HttpContext.Request;
+ var contentLength = request.ContentLength;
+
+ var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<BytesInputFormatter>>();
+
+ if (contentLength == null)
+ {
+ logger.LogInformation("Failed to read body as bytes. Content-Length is not set.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ if (contentLength == 0)
+ {
+ logger.LogInformation("Failed to read body as bytes. Content-Length is 0.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ var bodyStream = request.Body;
+
+ var data = new byte[contentLength.Value];
+ var bytesRead = await bodyStream.ReadAsync(data);
+
+ if (bytesRead != contentLength)
+ {
+ logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ var extraByte = new byte[1];
+ if (await bodyStream.ReadAsync(extraByte) != 0)
+ {
+ logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType));
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Formatters/StringInputFormatter.cs b/BackEnd/Timeline/Formatters/StringInputFormatter.cs new file mode 100644 index 00000000..b1924268 --- /dev/null +++ b/BackEnd/Timeline/Formatters/StringInputFormatter.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+using System.IO;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Timeline.Formatters
+{
+ public class StringInputFormatter : TextInputFormatter
+ {
+ public StringInputFormatter()
+ {
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Text.Plain));
+ SupportedEncodings.Add(Encoding.UTF8);
+ }
+
+ public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
+ {
+ var request = context.HttpContext.Request;
+ using var reader = new StreamReader(request.Body, effectiveEncoding);
+ var stringContent = await reader.ReadToEndAsync();
+ return await InputFormatterResult.SuccessAsync(stringContent);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/GlobalSuppressions.cs b/BackEnd/Timeline/GlobalSuppressions.cs new file mode 100644 index 00000000..2b0da576 --- /dev/null +++ b/BackEnd/Timeline/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")]
diff --git a/BackEnd/Timeline/Helpers/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/DataCacheHelper.cs new file mode 100644 index 00000000..1ad69708 --- /dev/null +++ b/BackEnd/Timeline/Helpers/DataCacheHelper.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using static Timeline.Resources.Helper.DataCacheHelper;
+
+namespace Timeline.Helpers
+{
+ public interface ICacheableData
+ {
+ string Type { get; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ byte[] Data { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ DateTime? LastModified { get; }
+ }
+
+ public class CacheableData : ICacheableData
+ {
+ public CacheableData(string type, byte[] data, DateTime? lastModified)
+ {
+ Type = type;
+ Data = data;
+ LastModified = lastModified;
+ }
+
+ public string Type { get; set; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ public DateTime? LastModified { get; set; }
+ }
+
+ public interface ICacheableDataProvider
+ {
+ Task<string> GetDataETag();
+ Task<ICacheableData> GetData();
+ }
+
+ public class DelegateCacheableDataProvider : ICacheableDataProvider
+ {
+ private readonly Func<Task<string>> _getDataETagDelegate;
+ private readonly Func<Task<ICacheableData>> _getDataDelegate;
+
+ public DelegateCacheableDataProvider(Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> getDataDelegate)
+ {
+ _getDataETagDelegate = getDataETagDelegate;
+ _getDataDelegate = getDataDelegate;
+ }
+
+ public Task<ICacheableData> GetData()
+ {
+ return _getDataDelegate();
+ }
+
+ public Task<string> GetDataETag()
+ {
+ return _getDataETagDelegate();
+ }
+ }
+
+ public static class DataCacheHelper
+ {
+ public static async Task<ActionResult> GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null)
+ {
+ const string CacheControlHeaderKey = "Cache-Control";
+ const string IfNonMatchHeaderKey = "If-None-Match";
+ const string ETagHeaderKey = "ETag";
+
+ string GenerateCacheControlHeaderValue()
+ {
+ var cacheControlHeader = new CacheControlHeaderValue()
+ {
+ NoCache = true,
+ NoStore = false,
+ MaxAge = maxAge ?? TimeSpan.FromDays(14),
+ Private = true,
+ MustRevalidate = true
+ };
+ return cacheControlHeader.ToString();
+ }
+
+ var loggerFactory = controller.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+ var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper));
+
+ var eTagValue = await provider.GetDataETag();
+ eTagValue = '"' + eTagValue + '"';
+ var eTag = new EntityTagHeaderValue(eTagValue);
+
+
+ if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
+ {
+ if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
+ {
+ logger.LogInformation(Log.Format(LogBadIfNoneMatch, ("Header Value", value)));
+ return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat());
+ }
+
+ if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
+ {
+ logger.LogInformation(LogResultNotModified);
+ controller.Response.Headers.Add(ETagHeaderKey, eTagValue);
+ controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue());
+
+ return controller.StatusCode(StatusCodes.Status304NotModified, null);
+ }
+ }
+
+ var data = await provider.GetData();
+ logger.LogInformation(LogResultData);
+ controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue());
+ return controller.File(data.Data, data.Type, data.LastModified, eTag);
+ }
+
+ public static Task<ActionResult> GenerateActionResult(Controller controller, Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> getDataDelegate, TimeSpan? maxAge = null)
+ {
+ return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Helpers/DateTimeExtensions.cs b/BackEnd/Timeline/Helpers/DateTimeExtensions.cs new file mode 100644 index 00000000..374f3bc9 --- /dev/null +++ b/BackEnd/Timeline/Helpers/DateTimeExtensions.cs @@ -0,0 +1,14 @@ +using System;
+
+namespace Timeline.Helpers
+{
+ public static class DateTimeExtensions
+ {
+ public static DateTime MyToUtc(this DateTime dateTime)
+ {
+ if (dateTime.Kind == DateTimeKind.Utc) return dateTime;
+ if (dateTime.Kind == DateTimeKind.Local) return dateTime.ToUniversalTime();
+ return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs new file mode 100644 index 00000000..9b253e7d --- /dev/null +++ b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc;
+using System.Text;
+using Timeline.Models.Http;
+
+namespace Timeline.Helpers
+{
+ public static class InvalidModelResponseFactory
+ {
+ public static IActionResult Factory(ActionContext context)
+ {
+ var modelState = context.ModelState;
+
+ var messageBuilder = new StringBuilder();
+ foreach (var model in modelState)
+ foreach (var error in model.Value.Errors)
+ {
+ messageBuilder.Append(model.Key);
+ messageBuilder.Append(" : ");
+ messageBuilder.AppendLine(error.ErrorMessage);
+ }
+
+ return new BadRequestObjectResult(ErrorResponse.Common.CustomMessage_InvalidModel(messageBuilder.ToString()));
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Helpers/LanguageHelper.cs b/BackEnd/Timeline/Helpers/LanguageHelper.cs new file mode 100644 index 00000000..b0156b8b --- /dev/null +++ b/BackEnd/Timeline/Helpers/LanguageHelper.cs @@ -0,0 +1,12 @@ +using System.Linq;
+
+namespace Timeline.Helpers
+{
+ public static class LanguageHelper
+ {
+ public static bool AreSame(this bool firstBool, params bool[] otherBools)
+ {
+ return otherBools.All(b => b == firstBool);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Helpers/Log.cs b/BackEnd/Timeline/Helpers/Log.cs new file mode 100644 index 00000000..af0b7e13 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Log.cs @@ -0,0 +1,22 @@ +using System.Text;
+
+namespace Timeline.Helpers
+{
+ public static class Log
+ {
+ public static string Format(string summary, params (string, object?)[] properties)
+ {
+ var builder = new StringBuilder();
+ builder.Append(summary);
+ foreach (var property in properties)
+ {
+ var (key, value) = property;
+ builder.AppendLine();
+ builder.Append(key);
+ builder.Append(" : ");
+ builder.Append(value);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs b/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs new file mode 100644 index 00000000..99e4eaac --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs @@ -0,0 +1,266 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200105150407_Initialize")]
+ partial class Initialize
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.0");
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.User", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("EncryptedPassword")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("RoleString")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<string>("ETag")
+ .HasColumnName("etag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserDetail", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_details");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.User", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.User", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.User", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.HasOne("Timeline.Entities.User", null)
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserDetail", b =>
+ {
+ b.HasOne("Timeline.Entities.User", null)
+ .WithOne("Detail")
+ .HasForeignKey("Timeline.Entities.UserDetail", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs b/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs new file mode 100644 index 00000000..4e12ef83 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs @@ -0,0 +1,217 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class Initialize : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "users",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ name = table.Column<string>(nullable: false),
+ password = table.Column<string>(nullable: false),
+ roles = table.Column<string>(nullable: false),
+ version = table.Column<long>(nullable: false, defaultValue: 0L)
+ .Annotation("Sqlite:Autoincrement", true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_users", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "timelines",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ name = table.Column<string>(nullable: true),
+ description = table.Column<string>(nullable: true),
+ owner = table.Column<long>(nullable: false),
+ visibility = table.Column<int>(nullable: false),
+ create_time = table.Column<DateTime>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_timelines", x => x.id);
+ table.ForeignKey(
+ name: "FK_timelines_users_owner",
+ column: x => x.owner,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "user_avatars",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ data = table.Column<byte[]>(nullable: true),
+ type = table.Column<string>(nullable: true),
+ etag = table.Column<string>(nullable: true),
+ last_modified = table.Column<DateTime>(nullable: false),
+ UserId = table.Column<long>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_user_avatars", x => x.id);
+ table.ForeignKey(
+ name: "FK_user_avatars_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "user_details",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ nickname = table.Column<string>(nullable: true),
+ UserId = table.Column<long>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_user_details", x => x.id);
+ table.ForeignKey(
+ name: "FK_user_details_users_UserId",
+ column: x => x.UserId,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "timeline_members",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ user = table.Column<long>(nullable: false),
+ timeline = table.Column<long>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_timeline_members", x => x.id);
+ table.ForeignKey(
+ name: "FK_timeline_members_timelines_timeline",
+ column: x => x.timeline,
+ principalTable: "timelines",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_timeline_members_users_user",
+ column: x => x.user,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "timeline_posts",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ timeline = table.Column<long>(nullable: false),
+ author = table.Column<long>(nullable: false),
+ content = table.Column<string>(nullable: true),
+ time = table.Column<DateTime>(nullable: false),
+ last_updated = table.Column<DateTime>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_timeline_posts", x => x.id);
+ table.ForeignKey(
+ name: "FK_timeline_posts_users_author",
+ column: x => x.author,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_timeline_posts_timelines_timeline",
+ column: x => x.timeline,
+ principalTable: "timelines",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_timeline_members_timeline",
+ table: "timeline_members",
+ column: "timeline");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_timeline_members_user",
+ table: "timeline_members",
+ column: "user");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_timeline_posts_author",
+ table: "timeline_posts",
+ column: "author");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_timeline_posts_timeline",
+ table: "timeline_posts",
+ column: "timeline");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_timelines_owner",
+ table: "timelines",
+ column: "owner");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_avatars_UserId",
+ table: "user_avatars",
+ column: "UserId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_details_UserId",
+ table: "user_details",
+ column: "UserId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_users_name",
+ table: "users",
+ column: "name",
+ unique: true);
+
+ // Add a init user. Username is "administrator". Password is "crupest".
+ migrationBuilder.InsertData("users", new string[] { "name", "password", "roles" },
+ new object[] { "administrator", "AQAAAAEAACcQAAAAENsspZrk8Wo+UuMyg6QuWJsNvRg6gVu4K/TumVod3h9GVLX9zDVuQQds3o7V8QWJ2w==", "user,admin" });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "timeline_members");
+
+ migrationBuilder.DropTable(
+ name: "timeline_posts");
+
+ migrationBuilder.DropTable(
+ name: "user_avatars");
+
+ migrationBuilder.DropTable(
+ name: "user_details");
+
+ migrationBuilder.DropTable(
+ name: "timelines");
+
+ migrationBuilder.DropTable(
+ name: "users");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs new file mode 100644 index 00000000..9b78eb15 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs @@ -0,0 +1,240 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200131100517_RefactorUser")]
+ partial class RefactorUser
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.1");
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<string>("ETag")
+ .HasColumnName("etag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs new file mode 100644 index 00000000..8597ed50 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs @@ -0,0 +1,128 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class RefactorUser : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(name: "name", table: "users", newName: "username");
+ migrationBuilder.RenameIndex(name: "IX_users_name", table: "users", newName: "IX_users_username");
+
+ migrationBuilder.AddColumn<string>(
+ name: "nickname",
+ table: "users",
+ nullable: true);
+
+ migrationBuilder.Sql(@"
+UPDATE users
+ SET nickname = (
+ SELECT nickname
+ FROM user_details
+ WHERE user_details.UserId = users.id
+ );
+ ");
+
+ /*
+ migrationBuilder.RenameColumn(name: "UserId", table: "user_avatars", newName: "user");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_user_avatars_users_UserId",
+ table: "user_avatars");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_user_avatars_users_user",
+ table: "user_avatars",
+ column: "user",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.RenameIndex(
+ name: "IX_user_avatars_UserId",
+ table: "user_avatars",
+ newName: "IX_user_avatars_user");
+ */
+
+ migrationBuilder.Sql(@"
+CREATE TABLE user_avatars_backup (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT,
+ data BLOB,
+ type TEXT,
+ etag TEXT,
+ last_modified TEXT NOT NULL,
+ user INTEGER NOT NULL,
+ CONSTRAINT FK_user_avatars_users_user FOREIGN KEY (
+ user
+ )
+ REFERENCES users (id) ON DELETE CASCADE
+);
+
+INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, user)
+ SELECT id, data, type, etag, last_modified, UserId FROM user_avatars;
+
+DROP TABLE user_avatars;
+
+ALTER TABLE user_avatars_backup
+ RENAME TO user_avatars;
+
+CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user);
+ ");
+
+ // migrationBuilder.DropTable(name: "user_details");
+
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(@"
+CREATE TABLE user_avatars_backup (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT,
+ data BLOB,
+ type TEXT,
+ etag TEXT,
+ last_modified TEXT NOT NULL,
+ UserId INTEGER NOT NULL,
+ CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY (
+ user
+ )
+ REFERENCES users (id) ON DELETE CASCADE
+);
+
+INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, UserId)
+ SELECT id, data, type, etag, last_modified, user FROM user_avatars;
+
+DROP TABLE user_avatars;
+
+ALTER TABLE user_avatars_backup
+ RENAME TO user_avatars;
+
+CREATE UNIQUE INDEX IX_user_avatars_UserId ON user_avatars (UserId);
+ ");
+
+ migrationBuilder.Sql(@"
+CREATE TABLE users_backup (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ password TEXT NOT NULL,
+ roles TEXT NOT NULL,
+ version INTEGER NOT NULL
+ DEFAULT 0
+);
+
+INSERT INTO users_backup (id, name, password, roles, version)
+ SELECT id, username, password, roles, version FROM users;
+
+DROP TABLE users;
+
+ALTER TABLE users_backup
+ RENAME TO users;
+
+CREATE UNIQUE INDEX IX_users_name ON users (name);
+ ");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs new file mode 100644 index 00000000..eb328b52 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs @@ -0,0 +1,257 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200221064341_AddJwtToken")]
+ partial class AddJwtToken
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<string>("ETag")
+ .HasColumnName("etag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs new file mode 100644 index 00000000..628970c6 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs @@ -0,0 +1,45 @@ +using System;
+using System.Security.Cryptography;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public static class JwtTokenGenerateHelper
+ {
+ public static byte[] GenerateKey()
+ {
+ using var random = RandomNumberGenerator.Create();
+ var key = new byte[16];
+ random.GetBytes(key);
+ return key;
+ }
+ }
+
+ public partial class AddJwtToken : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "jwt_token",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ key = table.Column<byte[]>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_jwt_token", x => x.id);
+ });
+
+
+ migrationBuilder.InsertData("jwt_token", "key", JwtTokenGenerateHelper.GenerateKey());
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "jwt_token");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs new file mode 100644 index 00000000..cf6ae8a3 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs @@ -0,0 +1,265 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200229103848_AddPostLocalId")]
+ partial class AddPostLocalId
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<string>("ETag")
+ .HasColumnName("etag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs new file mode 100644 index 00000000..497b38a1 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs @@ -0,0 +1,42 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddPostLocalId : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+
+ migrationBuilder.AddColumn<long>(
+ name: "current_post_local_id",
+ table: "timelines",
+ nullable: false,
+ defaultValue: 0L);
+
+ migrationBuilder.AddColumn<long>(
+ name: "local_id",
+ table: "timeline_posts",
+ nullable: false,
+ defaultValue: 0L);
+
+ migrationBuilder.Sql(@"
+UPDATE timeline_posts
+SET local_id = (SELECT COUNT (*)
+ FROM timeline_posts AS p
+ WHERE p.timeline = timeline_posts.timeline
+ AND p.id <= timeline_posts.id);
+
+UPDATE timelines
+SET current_post_local_id = (SELECT COUNT (*)
+ FROM timeline_posts AS p
+ WHERE p.timeline = timelines.id);
+ ");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs new file mode 100644 index 00000000..336ffc18 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs @@ -0,0 +1,290 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200306110049_AddDataTable")]
+ partial class AddDataTable
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs new file mode 100644 index 00000000..e33bf4c9 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs @@ -0,0 +1,87 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddDataTable : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "data",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ tag = table.Column<string>(nullable: false),
+ data = table.Column<byte[]>(nullable: false),
+ @ref = table.Column<int>(name: "ref", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_data", x => x.id);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_data_tag",
+ table: "data",
+ column: "tag",
+ unique: true);
+
+ migrationBuilder.Sql(@"
+ALTER TABLE user_avatars
+ RENAME TO user_avatars_backup;
+
+CREATE TABLE user_avatars (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT,
+ data_tag TEXT,
+ type TEXT,
+ last_modified TEXT NOT NULL,
+ user INTEGER NOT NULL,
+ CONSTRAINT FK_user_avatars_users_user FOREIGN KEY (
+ user
+ )
+ REFERENCES users (id) ON DELETE CASCADE
+);
+
+INSERT INTO user_avatars (id, data_tag, type, last_modified, user)
+ SELECT id, etag, type, last_modified, user FROM user_avatars_backup;
+
+INSERT OR IGNORE INTO data (tag, data, ref)
+ SELECT etag, data, 0 FROM user_avatars_backup;
+
+UPDATE data
+SET ref = (SELECT COUNT (*)
+ FROM user_avatars_backup AS a
+ WHERE a.etag == data.tag);
+
+DROP TABLE user_avatars_backup;
+
+CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user);
+ ");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "data");
+
+ migrationBuilder.DropColumn(
+ name: "data_tag",
+ table: "user_avatars");
+
+ migrationBuilder.AddColumn<byte[]>(
+ name: "data",
+ table: "user_avatars",
+ type: "BLOB",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "etag",
+ table: "user_avatars",
+ type: "TEXT",
+ nullable: true);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs new file mode 100644 index 00000000..f0c4dc08 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs @@ -0,0 +1,290 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200306111553_DropUserDetails")]
+ partial class DropUserDetails
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs new file mode 100644 index 00000000..0a176461 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class DropUserDetails : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(name: "user_details");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs new file mode 100644 index 00000000..bd75a916 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs @@ -0,0 +1,299 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200312112552_AddImagePost")]
+ partial class AddImagePost
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs new file mode 100644 index 00000000..d5098ce0 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+using Timeline.Models;
+
+namespace Timeline.Migrations
+{
+ public partial class AddImagePost : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "content_type",
+ table: "timeline_posts",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn<string>(
+ name: "extra_content",
+ table: "timeline_posts",
+ nullable: true);
+
+ migrationBuilder.Sql($@"
+UPDATE timeline_posts
+SET content_type = '{TimelinePostContentTypes.Text}';
+ ");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "content_type",
+ table: "timeline_posts");
+
+ migrationBuilder.DropColumn(
+ name: "extra_content",
+ table: "timeline_posts");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs new file mode 100644 index 00000000..adcc6308 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs @@ -0,0 +1,306 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200614061237_AddTimelineUniqueId")]
+ partial class AddTimelineUniqueId
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.4");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs new file mode 100644 index 00000000..7abbed79 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddTimelineUniqueId : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(
+@"
+PRAGMA foreign_keys=OFF;
+
+BEGIN TRANSACTION;
+
+CREATE TABLE new_timelines (
+ id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT,
+ unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
+ name TEXT NULL,
+ description TEXT NULL,
+ owner INTEGER NOT NULL,
+ visibility INTEGER NOT NULL,
+ create_time TEXT NOT NULL,
+ current_post_local_id INTEGER NOT NULL DEFAULT 0,
+ CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE
+);
+
+INSERT INTO new_timelines (id, name, description, owner, visibility, create_time, current_post_local_id)
+ SELECT id, name, description, owner, visibility, create_time, current_post_local_id FROM timelines;
+
+DROP TABLE timelines;
+
+ALTER TABLE new_timelines
+ RENAME TO timelines;
+
+CREATE INDEX IX_timelines_owner ON timelines (owner);
+
+PRAGMA foreign_key_check;
+
+COMMIT TRANSACTION;
+
+PRAGMA foreign_keys=ON;
+"
+ , true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs new file mode 100644 index 00000000..fd10dfa9 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs @@ -0,0 +1,314 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200618064936_TimelineAddModifiedTime")]
+ partial class TimelineAddModifiedTime
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.5");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs new file mode 100644 index 00000000..c277fe39 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs @@ -0,0 +1,57 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Timeline.Migrations
+{
+ public partial class TimelineAddModifiedTime : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var currentTime = new DateTimeToStringConverter().ConvertToProvider(DateTime.Now);
+
+ migrationBuilder.Sql(
+@$"
+PRAGMA foreign_keys=OFF;
+
+BEGIN TRANSACTION;
+
+CREATE TABLE new_timelines (
+ id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT,
+ unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
+ name TEXT NULL,
+ name_last_modified TEXT NOT NULL,
+ description TEXT NULL,
+ owner INTEGER NOT NULL,
+ visibility INTEGER NOT NULL,
+ create_time TEXT NOT NULL,
+ last_modified TEXT NOT NULL,
+ current_post_local_id INTEGER NOT NULL DEFAULT 0,
+ CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE
+);
+
+INSERT INTO new_timelines (id, unique_id, name, name_last_modified, description, owner, visibility, create_time, last_modified, current_post_local_id)
+ SELECT id, unique_id, name, '{currentTime}', description, owner, visibility, create_time, '{currentTime}', current_post_local_id FROM timelines;
+
+DROP TABLE timelines;
+
+ALTER TABLE new_timelines
+ RENAME TO timelines;
+
+CREATE INDEX IX_timelines_owner ON timelines (owner);
+
+PRAGMA foreign_key_check;
+
+COMMIT TRANSACTION;
+
+PRAGMA foreign_keys=ON;
+"
+ , true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs new file mode 100644 index 00000000..fe2329e4 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs @@ -0,0 +1,321 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200808071611_UserAddUniqueId")]
+ partial class UserAddUniqueId
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.5");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs new file mode 100644 index 00000000..651a2b05 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class UserAddUniqueId : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(
+@"
+PRAGMA foreign_keys=OFF;
+
+BEGIN TRANSACTION;
+
+CREATE TABLE new_users (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT,
+ unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
+ username TEXT NOT NULL,
+ password TEXT NOT NULL,
+ roles TEXT NOT NULL,
+ version INTEGER NOT NULL
+ DEFAULT 0,
+ nickname TEXT
+);
+
+INSERT INTO new_users (id, username, password, roles, version, nickname)
+ SELECT id, username, password, roles, version, nickname FROM users;
+
+DROP TABLE users;
+
+ALTER TABLE new_users
+ RENAME TO users;
+
+CREATE UNIQUE INDEX IX_users_username ON users (
+ username
+);
+
+PRAGMA foreign_key_check;
+
+COMMIT TRANSACTION;
+
+PRAGMA foreign_keys=ON;
+"
+ , true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "unique_id",
+ table: "users");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs new file mode 100644 index 00000000..71cc54dc --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs @@ -0,0 +1,339 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200810155908_AddTimesToUser")]
+ partial class AddTimesToUser
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.5");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTimeOffset>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTimeOffset>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTimeOffset>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("username_change_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs new file mode 100644 index 00000000..369f85e6 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs @@ -0,0 +1,67 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddTimesToUser : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(
+@"
+PRAGMA foreign_keys=OFF;
+
+BEGIN TRANSACTION;
+
+CREATE TABLE new_users (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT,
+ unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
+ username TEXT NOT NULL,
+ password TEXT NOT NULL,
+ roles TEXT NOT NULL,
+ version INTEGER NOT NULL
+ DEFAULT 0,
+ nickname TEXT,
+ create_time TEXT NOT NULL DEFAULT (datetime('now', 'utc')),
+ last_modified TEXT NOT NULL DEFAULT (datetime('now', 'utc')),
+ username_change_time TEXT NOT NULL DEFAULT (datetime('now', 'utc'))
+);
+
+INSERT INTO new_users (id, unique_id, username, password, roles, version, nickname)
+ SELECT id, unique_id, username, password, roles, version, nickname FROM users;
+
+DROP TABLE users;
+
+ALTER TABLE new_users
+ RENAME TO users;
+
+CREATE UNIQUE INDEX IX_users_username ON users (
+ username
+);
+
+PRAGMA foreign_key_check;
+
+COMMIT TRANSACTION;
+
+PRAGMA foreign_keys=ON;
+"
+, true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "create_time",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "last_modified",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "username_change_time",
+ table: "users");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs new file mode 100644 index 00000000..80598fdf --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs @@ -0,0 +1,337 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200810170533_MakePostAuthorOptional")]
+ partial class MakePostAuthorOptional
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.5");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTimeOffset>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTimeOffset>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTimeOffset>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("username_change_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs new file mode 100644 index 00000000..b0f0bca7 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class MakePostAuthorOptional : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(@"
+PRAGMA foreign_keys = 0;
+
+BEGIN TRANSACTION;
+
+CREATE TABLE new_timeline_posts (
+ id INTEGER NOT NULL
+ CONSTRAINT PK_timeline_posts PRIMARY KEY AUTOINCREMENT,
+ timeline INTEGER NOT NULL,
+ author INTEGER,
+ content TEXT,
+ time TEXT NOT NULL,
+ last_updated TEXT NOT NULL,
+ local_id INTEGER NOT NULL
+ DEFAULT 0,
+ content_type TEXT NOT NULL
+ DEFAULT '',
+ extra_content TEXT,
+ CONSTRAINT FK_timeline_posts_users_author FOREIGN KEY (
+ author
+ )
+ REFERENCES users (id),
+ CONSTRAINT FK_timeline_posts_timelines_timeline FOREIGN KEY (
+ timeline
+ )
+ REFERENCES timelines (id) ON DELETE CASCADE
+);
+
+INSERT INTO new_timeline_posts SELECT * FROM timeline_posts;
+
+DROP TABLE timeline_posts;
+
+ALTER TABLE new_timeline_posts RENAME TO timeline_posts;
+
+CREATE INDEX IX_timeline_posts_author ON timeline_posts (author);
+
+CREATE INDEX IX_timeline_posts_timeline ON timeline_posts(timeline);
+
+PRAGMA foreign_key_check;
+
+COMMIT TRANSACTION;
+
+PRAGMA foreign_keys = 1;
+ ", true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_timeline_posts_users_author",
+ table: "timeline_posts");
+
+ migrationBuilder.AlterColumn<long>(
+ name: "author",
+ table: "timeline_posts",
+ type: "INTEGER",
+ nullable: false,
+ oldClrType: typeof(long),
+ oldNullable: true);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_timeline_posts_users_author",
+ table: "timeline_posts",
+ column: "author",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs new file mode 100644 index 00000000..58238557 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs @@ -0,0 +1,337 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200811080808_ChangeDateTimeOffsetToDateTime")]
+ partial class ChangeDateTimeOffsetToDateTime
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.5");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTime>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("username_change_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs new file mode 100644 index 00000000..eb6b44f3 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class ChangeDateTimeOffsetToDateTime : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs new file mode 100644 index 00000000..f2279f3b --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs @@ -0,0 +1,341 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200826164553_TimelineAddTitle")]
+ partial class TimelineAddTitle
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.7");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Title")
+ .HasColumnName("title")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTime>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("username_change_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs new file mode 100644 index 00000000..7e8c498b --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class TimelineAddTitle : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "title",
+ table: "timelines",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "title",
+ table: "timelines");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 00000000..65ae6c9a --- /dev/null +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,339 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ partial class DatabaseContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.7");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnName("name_last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Title")
+ .HasColumnName("title")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTime>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnName("unique_id")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("username_change_time")
+ .HasColumnType("TEXT")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/MockClientApp/index.html b/BackEnd/Timeline/MockClientApp/index.html new file mode 100644 index 00000000..03cf371e --- /dev/null +++ b/BackEnd/Timeline/MockClientApp/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Mock Client App</title>
+</head>
+<body>
+ This is a mock client app for testing.
+</body>
+</html>
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
+{
+ /// <summary>
+ /// Model for reading http body as bytes.
+ /// </summary>
+ [OpenApiFile]
+ public class ByteData
+ {
+ /// <summary>
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <param name="contentType">The content type.</param>
+ public ByteData(byte[] data, string contentType)
+ {
+ Data = data;
+ ContentType = contentType;
+ }
+
+ /// <summary>
+ /// Data.
+ /// </summary>
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+
+ /// <summary>
+ /// Content type.
+ /// </summary>
+ 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<DateTime>
+ {
+ 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<T> : 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<CommonPutResponse.ResponseData>
+ {
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Common response for delete method.
+ /// </summary>
+ public class CommonDeleteResponse : CommonDataResponse<CommonDeleteResponse.ResponseData>
+ {
+ /// <summary></summary>
+ public class ResponseData
+ {
+ /// <summary></summary>
+ public ResponseData() { }
+
+ /// <summary></summary>
+ public ResponseData(bool delete)
+ {
+ Delete = delete;
+ }
+
+ /// <summary>
+ /// True if the entry is deleted. False if the entry does not exist.
+ /// </summary>
+ public bool Delete { get; set; }
+ }
+
+ /// <summary></summary>
+ public CommonDeleteResponse()
+ {
+
+ }
+
+ /// <summary></summary>
+ 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
+{
+ /// <summary>
+ /// Info of post content.
+ /// </summary>
+ public class TimelinePostContentInfo
+ {
+ /// <summary>
+ /// Type of the post content.
+ /// </summary>
+ public string Type { get; set; } = default!;
+ /// <summary>
+ /// If post is of text type. This is the text.
+ /// </summary>
+ public string? Text { get; set; }
+ /// <summary>
+ /// If post is of image type. This is the image url.
+ /// </summary>
+ public string? Url { get; set; }
+ /// <summary>
+ /// If post has data (currently it means it's a image post), this is the data etag.
+ /// </summary>
+ public string? ETag { get; set; }
+ }
+
+ /// <summary>
+ /// Info of a post.
+ /// </summary>
+ public class TimelinePostInfo
+ {
+ /// <summary>
+ /// Post id.
+ /// </summary>
+ public long Id { get; set; }
+ /// <summary>
+ /// Content of the post. May be null if post is deleted.
+ /// </summary>
+ public TimelinePostContentInfo? Content { get; set; }
+ /// <summary>
+ /// True if post is deleted.
+ /// </summary>
+ public bool Deleted { get; set; }
+ /// <summary>
+ /// Post time.
+ /// </summary>
+ public DateTime Time { get; set; }
+ /// <summary>
+ /// The author. May be null if the user has been deleted.
+ /// </summary>
+ public UserInfo? Author { get; set; } = default!;
+ /// <summary>
+ /// Last updated time.
+ /// </summary>
+ public DateTime LastUpdated { get; set; } = default!;
+ }
+
+ /// <summary>
+ /// Info of a timeline.
+ /// </summary>
+ public class TimelineInfo
+ {
+ /// <summary>
+ /// Unique id.
+ /// </summary>
+ public string UniqueId { get; set; } = default!;
+ /// <summary>
+ /// Title.
+ /// </summary>
+ public string Title { get; set; } = default!;
+ /// <summary>
+ /// Name of timeline.
+ /// </summary>
+ public string Name { get; set; } = default!;
+ /// <summary>
+ /// Last modified time of timeline name.
+ /// </summary>
+ public DateTime NameLastModifed { get; set; } = default!;
+ /// <summary>
+ /// Timeline description.
+ /// </summary>
+ public string Description { get; set; } = default!;
+ /// <summary>
+ /// Owner of the timeline.
+ /// </summary>
+ public UserInfo Owner { get; set; } = default!;
+ /// <summary>
+ /// Visibility of the timeline.
+ /// </summary>
+ public TimelineVisibility Visibility { get; set; }
+#pragma warning disable CA2227 // Collection properties should be read only
+ /// <summary>
+ /// Members of timeline.
+ /// </summary>
+ public List<UserInfo> Members { get; set; } = default!;
+#pragma warning restore CA2227 // Collection properties should be read only
+ /// <summary>
+ /// Create time of timeline.
+ /// </summary>
+ public DateTime CreateTime { get; set; } = default!;
+ /// <summary>
+ /// Last modified time of timeline.
+ /// </summary>
+ public DateTime LastModified { get; set; } = default!;
+
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+ /// <summary>
+ /// Related links.
+ /// </summary>
+ public TimelineInfoLinks _links { get; set; } = default!;
+#pragma warning restore CA1707 // Identifiers should not contain underscores
+ }
+
+ /// <summary>
+ /// Related links for timeline.
+ /// </summary>
+ public class TimelineInfoLinks
+ {
+ /// <summary>
+ /// Self.
+ /// </summary>
+ public string Self { get; set; } = default!;
+ /// <summary>
+ /// Posts url.
+ /// </summary>
+ public string Posts { get; set; } = default!;
+ }
+
+ public class TimelineInfoLinksValueResolver : IValueResolver<Timeline, TimelineInfo, TimelineInfoLinks>
+ {
+ 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<TimelinePost, TimelinePostInfo, TimelinePostContentInfo?>
+ {
+ 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<Timeline, TimelineInfo>().ForMember(u => u._links, opt => opt.MapFrom<TimelineInfoLinksValueResolver>());
+ CreateMap<TimelinePost, TimelinePostInfo>().ForMember(p => p.Content, opt => opt.MapFrom<TimelinePostContentResolver>());
+ CreateMap<TimelinePatchRequest, TimelineChangePropertyRequest>();
+ }
+ }
+}
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
+{
+ /// <summary>
+ /// Content of post create request.
+ /// </summary>
+ public class TimelinePostCreateRequestContent
+ {
+ /// <summary>
+ /// Type of post content.
+ /// </summary>
+ [Required]
+ public string Type { get; set; } = default!;
+ /// <summary>
+ /// If post is of text type, this is the text.
+ /// </summary>
+ public string? Text { get; set; }
+ /// <summary>
+ /// If post is of image type, this is base64 of image data.
+ /// </summary>
+ public string? Data { get; set; }
+ }
+
+ public class TimelinePostCreateRequest
+ {
+ /// <summary>
+ /// Content of the new post.
+ /// </summary>
+ [Required]
+ public TimelinePostCreateRequestContent Content { get; set; } = default!;
+
+ /// <summary>
+ /// Time of the post. If not set, current time will be used.
+ /// </summary>
+ public DateTime? Time { get; set; }
+ }
+
+ /// <summary>
+ /// Create timeline request model.
+ /// </summary>
+ public class TimelineCreateRequest
+ {
+ /// <summary>
+ /// Name of the new timeline. Must be a valid name.
+ /// </summary>
+ [Required]
+ [TimelineName]
+ public string Name { get; set; } = default!;
+ }
+
+ /// <summary>
+ /// Patch timeline request model.
+ /// </summary>
+ public class TimelinePatchRequest
+ {
+ /// <summary>
+ /// New title. Null for not change.
+ /// </summary>
+ public string? Title { get; set; }
+
+ /// <summary>
+ /// New description. Null for not change.
+ /// </summary>
+ public string? Description { get; set; }
+
+ /// <summary>
+ /// New visibility. Null for not change.
+ /// </summary>
+ public TimelineVisibility? Visibility { get; set; }
+ }
+
+ /// <summary>
+ /// Change timeline name request model.
+ /// </summary>
+ public class TimelineChangeNameRequest
+ {
+ /// <summary>
+ /// Old name of timeline.
+ /// </summary>
+ [Required]
+ [TimelineName]
+ public string OldName { get; set; } = default!;
+ /// <summary>
+ /// New name of timeline.
+ /// </summary>
+ [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
+{
+ /// <summary>
+ /// Request model for <see cref="TokenController.Create(CreateTokenRequest)"/>.
+ /// </summary>
+ public class CreateTokenRequest
+ {
+ /// <summary>
+ /// The username.
+ /// </summary>
+ public string Username { get; set; } = default!;
+ /// <summary>
+ /// The password.
+ /// </summary>
+ public string Password { get; set; } = default!;
+ /// <summary>
+ /// Optional token validation period. In days. If not specified, server will use a default one.
+ /// </summary>
+ [Range(1, 365)]
+ public int? Expire { get; set; }
+ }
+
+ /// <summary>
+ /// Response model for <see cref="TokenController.Create(CreateTokenRequest)"/>.
+ /// </summary>
+ public class CreateTokenResponse
+ {
+ /// <summary>
+ /// The token created.
+ /// </summary>
+ public string Token { get; set; } = default!;
+ /// <summary>
+ /// The user owning the token.
+ /// </summary>
+ public UserInfo User { get; set; } = default!;
+ }
+
+ /// <summary>
+ /// Request model for <see cref="TokenController.Verify(VerifyTokenRequest)"/>.
+ /// </summary>
+ public class VerifyTokenRequest
+ {
+ /// <summary>
+ /// The token to verify.
+ /// </summary>
+ public string Token { get; set; } = default!;
+ }
+
+ /// <summary>
+ /// Response model for <see cref="TokenController.Verify(VerifyTokenRequest)"/>.
+ /// </summary>
+ public class VerifyTokenResponse
+ {
+ /// <summary>
+ /// The user owning the token.
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Request model for <see cref="UserController.Patch(UserPatchRequest, string)"/>.
+ /// </summary>
+ public class UserPatchRequest
+ {
+ /// <summary>
+ /// New username. Null if not change. Need to be administrator.
+ /// </summary>
+ [Username]
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// New password. Null if not change. Need to be administrator.
+ /// </summary>
+ [MinLength(1)]
+ public string? Password { get; set; }
+
+ /// <summary>
+ /// New nickname. Null if not change. Need to be administrator to change other's.
+ /// </summary>
+ [Nickname]
+ public string? Nickname { get; set; }
+
+ /// <summary>
+ /// Whether to be administrator. Null if not change. Need to be administrator.
+ /// </summary>
+ public bool? Administrator { get; set; }
+ }
+
+ /// <summary>
+ /// Request model for <see cref="UserController.CreateUser(CreateUserRequest)"/>.
+ /// </summary>
+ public class CreateUserRequest
+ {
+ /// <summary>
+ /// Username of the new user.
+ /// </summary>
+ [Required, Username]
+ public string Username { get; set; } = default!;
+
+ /// <summary>
+ /// Password of the new user.
+ /// </summary>
+ [Required, MinLength(1)]
+ public string Password { get; set; } = default!;
+
+ /// <summary>
+ /// Whether the new user is administrator.
+ /// </summary>
+ [Required]
+ public bool? Administrator { get; set; }
+
+ /// <summary>
+ /// Nickname of the new user.
+ /// </summary>
+ [Nickname]
+ public string? Nickname { get; set; }
+ }
+
+ /// <summary>
+ /// Request model for <see cref="UserController.ChangePassword(ChangePasswordRequest)"/>.
+ /// </summary>
+ public class ChangePasswordRequest
+ {
+ /// <summary>
+ /// Old password.
+ /// </summary>
+ [Required(AllowEmptyStrings = false)]
+ public string OldPassword { get; set; } = default!;
+
+ /// <summary>
+ /// New password.
+ /// </summary>
+ [Required(AllowEmptyStrings = false)]
+ public string NewPassword { get; set; } = default!;
+ }
+
+ public class UserControllerAutoMapperProfile : Profile
+ {
+ public UserControllerAutoMapperProfile()
+ {
+ CreateMap<UserPatchRequest, User>(MemberList.Source);
+ CreateMap<CreateUserRequest, User>(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
+{
+ /// <summary>
+ /// Info of a user.
+ /// </summary>
+ public class UserInfo
+ {
+ /// <summary>
+ /// Unique id.
+ /// </summary>
+ public string UniqueId { get; set; } = default!;
+ /// <summary>
+ /// Username.
+ /// </summary>
+ public string Username { get; set; } = default!;
+ /// <summary>
+ /// Nickname.
+ /// </summary>
+ public string Nickname { get; set; } = default!;
+ /// <summary>
+ /// True if the user is a administrator.
+ /// </summary>
+ public bool? Administrator { get; set; } = default!;
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+ /// <summary>
+ /// Related links.
+ /// </summary>
+ public UserInfoLinks _links { get; set; } = default!;
+#pragma warning restore CA1707 // Identifiers should not contain underscores
+ }
+
+ /// <summary>
+ /// Related links for user.
+ /// </summary>
+ public class UserInfoLinks
+ {
+ /// <summary>
+ /// Self.
+ /// </summary>
+ public string Self { get; set; } = default!;
+ /// <summary>
+ /// Avatar url.
+ /// </summary>
+ public string Avatar { get; set; } = default!;
+ /// <summary>
+ /// Personal timeline url.
+ /// </summary>
+ public string Timeline { get; set; } = default!;
+ }
+
+ public class UserInfoLinksValueResolver : IValueResolver<User, UserInfo, UserInfoLinks>
+ {
+ 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<User, UserInfo>().ForMember(u => u._links, opt => opt.MapFrom<UserInfoLinksValueResolver>());
+ }
+ }
+}
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
+ {
+ /// <summary>
+ /// All people including those without accounts.
+ /// </summary>
+ Public,
+ /// <summary>
+ /// Only people signed in.
+ /// </summary>
+ Register,
+ /// <summary>
+ /// Only member.
+ /// </summary>
+ 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;
+
+ /// <summary>
+ /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted).
+ /// </summary>
+ 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<User> 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<string>
+ {
+ 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<string>
+ {
+ 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<string>
+ {
+ 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
+{
+ /// <summary>
+ /// A validator to validate value.
+ /// </summary>
+ public interface IValidator
+ {
+ /// <summary>
+ /// Validate given value.
+ /// </summary>
+ /// <param name="value">The value to validate.</param>
+ /// <returns>Validation success or not and message.</returns>
+ (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;
+ }
+ }
+
+ /// <summary>
+ /// Convenient base class for validator.
+ /// </summary>
+ /// <typeparam name="T">The type of accepted value.</typeparam>
+ /// <remarks>
+ /// Subclass should override <see cref="DoValidate(T)"/> 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 <see cref="PermitNull"/>.
+ /// If value is not null and not of type <typeparamref name="T"/>
+ /// it will fail and not call <see cref="DoValidate(T)"/>.
+ ///
+ /// <see cref="PermitNull"/> is true by default.
+ ///
+ /// If you want some other behaviours, write the validator from scratch.
+ /// </remarks>
+ public abstract class Validator<T> : 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;
+
+ /// <summary>
+ /// Create with a given validator.
+ /// </summary>
+ /// <param name="validator">The validator used to validate.</param>
+ public ValidateWithAttribute(IValidator validator)
+ {
+ _validator = validator ?? throw new ArgumentNullException(nameof(validator));
+ }
+
+ /// <summary>
+ /// Create the validator with default constructor.
+ /// </summary>
+ /// <param name="validatorType">The type of the validator.</param>
+ 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);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Program.cs b/BackEnd/Timeline/Program.cs new file mode 100644 index 00000000..87e330a2 --- /dev/null +++ b/BackEnd/Timeline/Program.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Hosting;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System.Resources;
+using Timeline.Entities;
+using Timeline.Services;
+
+[assembly: NeutralResourcesLanguage("en")]
+
+namespace Timeline
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = CreateWebHostBuilder(args).Build();
+
+ var env = host.Services.GetRequiredService<IWebHostEnvironment>();
+
+ var databaseBackupService = host.Services.GetRequiredService<IDatabaseBackupService>();
+ databaseBackupService.BackupNow();
+
+ if (env.IsProduction())
+ {
+ using (var scope = host.Services.CreateScope())
+ {
+ var databaseContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
+ databaseContext.Database.Migrate();
+ }
+ }
+
+ host.Run();
+ }
+
+ public static IHostBuilder CreateWebHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup<Startup>();
+ });
+ }
+}
diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json new file mode 100644 index 00000000..de8186db --- /dev/null +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{
+ "profiles": {
+ "Development": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_USEPROXYFRONTEND": "true",
+ "ASPNETCORE_WORKDIR": "D:\\timeline-development"
+ }
+ },
+ "Development-Mock": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_USEMOCKFRONTEND": "true",
+ "ASPNETCORE_WORKDIR": "D:\\timeline-development"
+ }
+ },
+ "Staging": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Staging",
+ "ASPNETCORE_WORKDIR": "D:\\timeline-development"
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs b/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs new file mode 100644 index 00000000..fd4540ea --- /dev/null +++ b/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Authentication {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class AuthHandler {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal AuthHandler() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Authentication.AuthHandler", typeof(AuthHandler).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token is found in authorization header. Token is {0} ..
+ /// </summary>
+ internal static string LogTokenFoundInHeader {
+ get {
+ return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} ..
+ /// </summary>
+ internal static string LogTokenFoundInQuery {
+ get {
+ return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No jwt token is found..
+ /// </summary>
+ internal static string LogTokenNotFound {
+ get {
+ return ResourceManager.GetString("LogTokenNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A jwt token validation failed..
+ /// </summary>
+ internal static string LogTokenValidationFail {
+ get {
+ return ResourceManager.GetString("LogTokenValidationFail", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx b/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx new file mode 100644 index 00000000..4cddc8ce --- /dev/null +++ b/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogTokenFoundInHeader" xml:space="preserve">
+ <value>Token is found in authorization header. Token is {0} .</value>
+ </data>
+ <data name="LogTokenFoundInQuery" xml:space="preserve">
+ <value>Token is found in query param with key "{0}". Token is {1} .</value>
+ </data>
+ <data name="LogTokenNotFound" xml:space="preserve">
+ <value>No jwt token is found.</value>
+ </data>
+ <data name="LogTokenValidationFail" xml:space="preserve">
+ <value>A jwt token validation failed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs new file mode 100644 index 00000000..70a1d605 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ControllerAuthExtensions {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ControllerAuthExtensions() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.ControllerAuthExtensions", typeof(ControllerAuthExtensions).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to get user id because User has no NameIdentifier claim..
+ /// </summary>
+ internal static string ExceptionNoUserIdentifierClaim {
+ get {
+ return ResourceManager.GetString("ExceptionNoUserIdentifierClaim", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to get user id because NameIdentifier claim is not a number..
+ /// </summary>
+ internal static string ExceptionUserIdentifierClaimBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx new file mode 100644 index 00000000..03e6d95a --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionNoUserIdentifierClaim" xml:space="preserve">
+ <value>Failed to get user id because User has no NameIdentifier claim.</value>
+ </data>
+ <data name="ExceptionUserIdentifierClaimBadFormat" xml:space="preserve">
+ <value>Failed to get user id because NameIdentifier claim is not a number.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs new file mode 100644 index 00000000..ae6414e6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class TimelineController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TimelineController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TimelineController", typeof(TimelineController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An unknown timeline visibility value. Can't convert it..
+ /// </summary>
+ internal static string ExceptionStringToVisibility {
+ get {
+ return ResourceManager.GetString("ExceptionStringToVisibility", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown..
+ /// </summary>
+ internal static string LogUnknownTimelineMemberOperationUserException {
+ get {
+ return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Controllers/TimelineController.resx b/BackEnd/Timeline/Resources/Controllers/TimelineController.resx new file mode 100644 index 00000000..4cf3d6fb --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TimelineController.resx @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionStringToVisibility" xml:space="preserve">
+ <value>An unknown timeline visibility value. Can't convert it.</value>
+ </data>
+ <data name="LogUnknownTimelineMemberOperationUserException" xml:space="preserve">
+ <value>An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs new file mode 100644 index 00000000..a7c2864b --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class TokenController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TokenController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TokenController", typeof(TokenController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The password is wrong..
+ /// </summary>
+ internal static string LogBadPassword {
+ get {
+ return ResourceManager.GetString("LogBadPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user failed to create a token..
+ /// </summary>
+ internal static string LogCreateFailure {
+ get {
+ return ResourceManager.GetString("LogCreateFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user succeeded to create a token..
+ /// </summary>
+ internal static string LogCreateSuccess {
+ get {
+ return ResourceManager.GetString("LogCreateSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user does not exist..
+ /// </summary>
+ internal static string LogUserNotExist {
+ get {
+ return ResourceManager.GetString("LogUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad format. It might not be created by the server..
+ /// </summary>
+ internal static string LogVerifyBadFormat {
+ get {
+ return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is expired..
+ /// </summary>
+ internal static string LogVerifyExpire {
+ get {
+ return ResourceManager.GetString("LogVerifyExpire", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A token failed to be verified..
+ /// </summary>
+ internal static string LogVerifyFailure {
+ get {
+ return ResourceManager.GetString("LogVerifyFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token has an old version. User might have update some info..
+ /// </summary>
+ internal static string LogVerifyOldVersion {
+ get {
+ return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A token succeeded to be verified..
+ /// </summary>
+ internal static string LogVerifySuccess {
+ get {
+ return ResourceManager.GetString("LogVerifySuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user..
+ /// </summary>
+ internal static string LogVerifyUserNotExist {
+ get {
+ return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Controllers/TokenController.resx b/BackEnd/Timeline/Resources/Controllers/TokenController.resx new file mode 100644 index 00000000..683d6cc9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TokenController.resx @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogBadPassword" xml:space="preserve">
+ <value>The password is wrong.</value>
+ </data>
+ <data name="LogCreateFailure" xml:space="preserve">
+ <value>A user failed to create a token.</value>
+ </data>
+ <data name="LogCreateSuccess" xml:space="preserve">
+ <value>A user succeeded to create a token.</value>
+ </data>
+ <data name="LogUserNotExist" xml:space="preserve">
+ <value>The user does not exist.</value>
+ </data>
+ <data name="LogVerifyBadFormat" xml:space="preserve">
+ <value>The token is of bad format. It might not be created by the server.</value>
+ </data>
+ <data name="LogVerifyExpire" xml:space="preserve">
+ <value>The token is expired.</value>
+ </data>
+ <data name="LogVerifyFailure" xml:space="preserve">
+ <value>A token failed to be verified.</value>
+ </data>
+ <data name="LogVerifyOldVersion" xml:space="preserve">
+ <value>Token has an old version. User might have update some info.</value>
+ </data>
+ <data name="LogVerifySuccess" xml:space="preserve">
+ <value>A token succeeded to be verified.</value>
+ </data>
+ <data name="LogVerifyUserNotExist" xml:space="preserve">
+ <value>User does not exist. Administrator might have deleted this user.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs new file mode 100644 index 00000000..b0c35ff9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserAvatarController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserAvatarController", typeof(UserAvatarController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value..
+ /// </summary>
+ internal static string ExceptionUnknownAvatarFormatError {
+ get {
+ return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogDeleteForbid {
+ get {
+ return ResourceManager.GetString("LogDeleteForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogDeleteNotExist {
+ get {
+ return ResourceManager.GetString("LogDeleteNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to delete a avatar of a user..
+ /// </summary>
+ internal static string LogDeleteSuccess {
+ get {
+ return ResourceManager.GetString("LogDeleteSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogGetUserNotExist {
+ get {
+ return ResourceManager.GetString("LogGetUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogPutForbid {
+ get {
+ return ResourceManager.GetString("LogPutForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to put a avatar of a user..
+ /// </summary>
+ internal static string LogPutSuccess {
+ get {
+ return ResourceManager.GetString("LogPutSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed..
+ /// </summary>
+ internal static string LogPutUserBadFormat {
+ get {
+ return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogPutUserNotExist {
+ get {
+ return ResourceManager.GetString("LogPutUserNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx new file mode 100644 index 00000000..864d96c0 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionUnknownAvatarFormatError" xml:space="preserve">
+ <value>Unknown AvatarDataException.ErrorReason value.</value>
+ </data>
+ <data name="LogDeleteForbid" xml:space="preserve">
+ <value>Attempt to delete a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogDeleteNotExist" xml:space="preserve">
+ <value>Attempt to delete a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogDeleteSuccess" xml:space="preserve">
+ <value>Succeed to delete a avatar of a user.</value>
+ </data>
+ <data name="LogGetUserNotExist" xml:space="preserve">
+ <value>Attempt to get a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogPutForbid" xml:space="preserve">
+ <value>Attempt to put a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogPutSuccess" xml:space="preserve">
+ <value>Succeed to put a avatar of a user.</value>
+ </data>
+ <data name="LogPutUserBadFormat" xml:space="preserve">
+ <value>Attempt to put a avatar of a bad format failed.</value>
+ </data>
+ <data name="LogPutUserNotExist" xml:space="preserve">
+ <value>Attempt to put a avatar of a non-existent user failed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs new file mode 100644 index 00000000..c8067614 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserController", typeof(UserController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown PutResult..
+ /// </summary>
+ internal static string ExceptionUnknownPutResult {
+ get {
+ return ResourceManager.GetString("ExceptionUnknownPutResult", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change password with wrong old password failed..
+ /// </summary>
+ internal static string LogChangePasswordBadPassword {
+ get {
+ return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change a user's username to a existent one failed..
+ /// </summary>
+ internal static string LogChangeUsernameConflict {
+ get {
+ return ResourceManager.GetString("LogChangeUsernameConflict", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed..
+ /// </summary>
+ internal static string LogChangeUsernameNotExist {
+ get {
+ return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed..
+ /// </summary>
+ internal static string LogGetUserNotExist {
+ get {
+ return ResourceManager.GetString("LogGetUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to patch a user that does not exist failed..
+ /// </summary>
+ internal static string LogPatchUserNotExist {
+ get {
+ return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Controllers/UserController.resx b/BackEnd/Timeline/Resources/Controllers/UserController.resx new file mode 100644 index 00000000..0bdf4845 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserController.resx @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionUnknownPutResult" xml:space="preserve">
+ <value>Unknown PutResult.</value>
+ </data>
+ <data name="LogChangePasswordBadPassword" xml:space="preserve">
+ <value>Attempt to change password with wrong old password failed.</value>
+ </data>
+ <data name="LogChangeUsernameConflict" xml:space="preserve">
+ <value>Attempt to change a user's username to a existent one failed.</value>
+ </data>
+ <data name="LogChangeUsernameNotExist" xml:space="preserve">
+ <value>Attempt to change a username of a user that does not exist failed.</value>
+ </data>
+ <data name="LogGetUserNotExist" xml:space="preserve">
+ <value>Attempt to retrieve info of a user that does not exist failed.</value>
+ </data>
+ <data name="LogPatchUserNotExist" xml:space="preserve">
+ <value>Attempt to patch a user that does not exist failed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Entities.Designer.cs b/BackEnd/Timeline/Resources/Entities.Designer.cs new file mode 100644 index 00000000..5f286f23 --- /dev/null +++ b/BackEnd/Timeline/Resources/Entities.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Entities {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Entities() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Entities", typeof(Entities).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Only sqlite is supported..
+ /// </summary>
+ internal static string ExceptionOnlySqliteSupported {
+ get {
+ return ResourceManager.GetString("ExceptionOnlySqliteSupported", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Entities.resx b/BackEnd/Timeline/Resources/Entities.resx new file mode 100644 index 00000000..1538b533 --- /dev/null +++ b/BackEnd/Timeline/Resources/Entities.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionOnlySqliteSupported" xml:space="preserve">
+ <value>Only sqlite is supported.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Filters.Designer.cs b/BackEnd/Timeline/Resources/Filters.Designer.cs new file mode 100644 index 00000000..dedfe498 --- /dev/null +++ b/BackEnd/Timeline/Resources/Filters.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Filters {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Filters() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Filters", typeof(Filters).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute..
+ /// </summary>
+ internal static string LogSelfOrAdminNoUser {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminNoUser", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but it does not have a model named username..
+ /// </summary>
+ internal static string LogSelfOrAdminNoUsername {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminNoUsername", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string..
+ /// </summary>
+ internal static string LogSelfOrAdminUsernameNotString {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Filters.resx b/BackEnd/Timeline/Resources/Filters.resx new file mode 100644 index 00000000..22620889 --- /dev/null +++ b/BackEnd/Timeline/Resources/Filters.resx @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogSelfOrAdminNoUser" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute.</value>
+ </data>
+ <data name="LogSelfOrAdminNoUsername" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, but it does not have a model named username.</value>
+ </data>
+ <data name="LogSelfOrAdminUsernameNotString" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs new file mode 100644 index 00000000..acf56d13 --- /dev/null +++ b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Helper {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class DataCacheHelper {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal DataCacheHelper() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Helper.DataCacheHelper", typeof(DataCacheHelper).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header If-None-Match is of bad format..
+ /// </summary>
+ internal static string LogBadIfNoneMatch {
+ get {
+ return ResourceManager.GetString("LogBadIfNoneMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cache is invalid and data is returned..
+ /// </summary>
+ internal static string LogResultData {
+ get {
+ return ResourceManager.GetString("LogResultData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cache is valid and 304 Not Modified is returned..
+ /// </summary>
+ internal static string LogResultNotModified {
+ get {
+ return ResourceManager.GetString("LogResultNotModified", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx new file mode 100644 index 00000000..515cfa9b --- /dev/null +++ b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogBadIfNoneMatch" xml:space="preserve">
+ <value>Header If-None-Match is of bad format.</value>
+ </data>
+ <data name="LogResultData" xml:space="preserve">
+ <value>Cache is invalid and data is returned.</value>
+ </data>
+ <data name="LogResultNotModified" xml:space="preserve">
+ <value>Cache is valid and 304 Not Modified is returned.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Messages.Designer.cs b/BackEnd/Timeline/Resources/Messages.Designer.cs new file mode 100644 index 00000000..bb654ce6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Messages.Designer.cs @@ -0,0 +1,396 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Messages {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Messages() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Messages", typeof(Messages).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Body is too big. It can't be bigger than {0}..
+ /// </summary>
+ internal static string Common_Content_TooBig {
+ get {
+ return ResourceManager.GetString("Common_Content_TooBig", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Actual body length is bigger than it in header..
+ /// </summary>
+ internal static string Common_Content_UnmatchedLength_Bigger {
+ get {
+ return ResourceManager.GetString("Common_Content_UnmatchedLength_Bigger", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Actual body length is smaller than it in header..
+ /// </summary>
+ internal static string Common_Content_UnmatchedLength_Smaller {
+ get {
+ return ResourceManager.GetString("Common_Content_UnmatchedLength_Smaller", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You have no permission to do the operation..
+ /// </summary>
+ internal static string Common_Forbid {
+ get {
+ return ResourceManager.GetString("Common_Forbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You are not the resource owner..
+ /// </summary>
+ internal static string Common_Forbid_NotSelf {
+ get {
+ return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header Content-Length is missing or of bad format..
+ /// </summary>
+ internal static string Common_Header_ContentLength_Missing {
+ get {
+ return ResourceManager.GetString("Common_Header_ContentLength_Missing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header Content-Length must not be 0..
+ /// </summary>
+ internal static string Common_Header_ContentLength_Zero {
+ get {
+ return ResourceManager.GetString("Common_Header_ContentLength_Zero", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header Content-Type is missing..
+ /// </summary>
+ internal static string Common_Header_ContentType_Missing {
+ get {
+ return ResourceManager.GetString("Common_Header_ContentType_Missing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header If-Non-Match is of bad format..
+ /// </summary>
+ internal static string Common_Header_IfNonMatch_BadFormat {
+ get {
+ return ResourceManager.GetString("Common_Header_IfNonMatch_BadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Model is of bad format..
+ /// </summary>
+ internal static string Common_InvalidModel {
+ get {
+ return ResourceManager.GetString("Common_InvalidModel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The api endpoint you request is unknown. You might get the wrong api entry..
+ /// </summary>
+ internal static string Common_UnknownEndpoint {
+ get {
+ return ResourceManager.GetString("Common_UnknownEndpoint", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown type of post content..
+ /// </summary>
+ internal static string TimelineController_ContentUnknownType {
+ get {
+ return ResourceManager.GetString("TimelineController_ContentUnknownType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is not a valid base64 string in image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataNotBase64 {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataNotBase64", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is not a valid image after base64 decoding in image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataNotImage {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataNotImage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is required for image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataRequired {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user to set as member does not exist..
+ /// </summary>
+ internal static string TimelineController_MemberPut_NotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A timeline with given name already exists..
+ /// </summary>
+ internal static string TimelineController_NameConflict {
+ get {
+ return ResourceManager.GetString("TimelineController_NameConflict", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline with given name does not exist..
+ /// </summary>
+ internal static string TimelineController_NotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_NotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post of that type has no data..
+ /// </summary>
+ internal static string TimelineController_PostNoData {
+ get {
+ return ResourceManager.GetString("TimelineController_PostNoData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post to operate on does not exist..
+ /// </summary>
+ internal static string TimelineController_PostNotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_PostNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user specified by query param "relate" does not exist..
+ /// </summary>
+ internal static string TimelineController_QueryRelateNotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_QueryRelateNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to '{0}' is an unkown visibility in the query parameter 'visibility'. .
+ /// </summary>
+ internal static string TimelineController_QueryVisibilityUnknown {
+ get {
+ return ResourceManager.GetString("TimelineController_QueryVisibilityUnknown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Text field is required for text content..
+ /// </summary>
+ internal static string TimelineController_TextContentTextRequired {
+ get {
+ return ResourceManager.GetString("TimelineController_TextContentTextRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Username or password is invalid..
+ /// </summary>
+ internal static string TokenController_Create_BadCredential {
+ get {
+ return ResourceManager.GetString("TokenController_Create_BadCredential", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad format. It might not be created by the server..
+ /// </summary>
+ internal static string TokenController_Verify_BadFormat {
+ get {
+ return ResourceManager.GetString("TokenController_Verify_BadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token has an old version. User might have update some info..
+ /// </summary>
+ internal static string TokenController_Verify_OldVersion {
+ get {
+ return ResourceManager.GetString("TokenController_Verify_OldVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is expired..
+ /// </summary>
+ internal static string TokenController_Verify_TimeExpired {
+ get {
+ return ResourceManager.GetString("TokenController_Verify_TimeExpired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user..
+ /// </summary>
+ internal static string TokenController_Verify_UserNotExist {
+ get {
+ return ResourceManager.GetString("TokenController_Verify_UserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image is not a square..
+ /// </summary>
+ internal static string UserAvatar_BadFormat_BadSize {
+ get {
+ return ResourceManager.GetString("UserAvatar_BadFormat_BadSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image decode failed..
+ /// </summary>
+ internal static string UserAvatar_BadFormat_CantDecode {
+ get {
+ return ResourceManager.GetString("UserAvatar_BadFormat_CantDecode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image format does not match the one in header..
+ /// </summary>
+ internal static string UserAvatar_BadFormat_UnmatchedFormat {
+ get {
+ return ResourceManager.GetString("UserAvatar_BadFormat_UnmatchedFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user to operate on does not exist..
+ /// </summary>
+ internal static string UserCommon_NotExist {
+ get {
+ return ResourceManager.GetString("UserCommon_NotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Old password is wrong..
+ /// </summary>
+ internal static string UserController_ChangePassword_BadOldPassword {
+ get {
+ return ResourceManager.GetString("UserController_ChangePassword_BadOldPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You can't set permission unless you are administrator..
+ /// </summary>
+ internal static string UserController_Patch_Forbid_Administrator {
+ get {
+ return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword ..
+ /// </summary>
+ internal static string UserController_Patch_Forbid_Password {
+ get {
+ return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You can't set username unless you are administrator..
+ /// </summary>
+ internal static string UserController_Patch_Forbid_Username {
+ get {
+ return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user with given username already exists..
+ /// </summary>
+ internal static string UserController_UsernameConflict {
+ get {
+ return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Messages.resx b/BackEnd/Timeline/Resources/Messages.resx new file mode 100644 index 00000000..2bbf494e --- /dev/null +++ b/BackEnd/Timeline/Resources/Messages.resx @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Common_Content_TooBig" xml:space="preserve">
+ <value>Body is too big. It can't be bigger than {0}.</value>
+ </data>
+ <data name="Common_Content_UnmatchedLength_Bigger" xml:space="preserve">
+ <value>Actual body length is bigger than it in header.</value>
+ </data>
+ <data name="Common_Content_UnmatchedLength_Smaller" xml:space="preserve">
+ <value>Actual body length is smaller than it in header.</value>
+ </data>
+ <data name="Common_Forbid" xml:space="preserve">
+ <value>You have no permission to do the operation.</value>
+ </data>
+ <data name="Common_Forbid_NotSelf" xml:space="preserve">
+ <value>You are not the resource owner.</value>
+ </data>
+ <data name="Common_Header_ContentLength_Missing" xml:space="preserve">
+ <value>Header Content-Length is missing or of bad format.</value>
+ </data>
+ <data name="Common_Header_ContentLength_Zero" xml:space="preserve">
+ <value>Header Content-Length must not be 0.</value>
+ </data>
+ <data name="Common_Header_ContentType_Missing" xml:space="preserve">
+ <value>Header Content-Type is missing.</value>
+ </data>
+ <data name="Common_Header_IfNonMatch_BadFormat" xml:space="preserve">
+ <value>Header If-Non-Match is of bad format.</value>
+ </data>
+ <data name="Common_InvalidModel" xml:space="preserve">
+ <value>Model is of bad format.</value>
+ </data>
+ <data name="Common_UnknownEndpoint" xml:space="preserve">
+ <value>The api endpoint you request is unknown. You might get the wrong api entry.</value>
+ </data>
+ <data name="TimelineController_ContentUnknownType" xml:space="preserve">
+ <value>Unknown type of post content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataNotBase64" xml:space="preserve">
+ <value>Data field is not a valid base64 string in image content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataNotImage" xml:space="preserve">
+ <value>Data field is not a valid image after base64 decoding in image content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataRequired" xml:space="preserve">
+ <value>Data field is required for image content.</value>
+ </data>
+ <data name="TimelineController_MemberPut_NotExist" xml:space="preserve">
+ <value>The user to set as member does not exist.</value>
+ </data>
+ <data name="TimelineController_NameConflict" xml:space="preserve">
+ <value>A timeline with given name already exists.</value>
+ </data>
+ <data name="TimelineController_NotExist" xml:space="preserve">
+ <value>The timeline with given name does not exist.</value>
+ </data>
+ <data name="TimelineController_PostNoData" xml:space="preserve">
+ <value>The post of that type has no data.</value>
+ </data>
+ <data name="TimelineController_PostNotExist" xml:space="preserve">
+ <value>The post to operate on does not exist.</value>
+ </data>
+ <data name="TimelineController_QueryRelateNotExist" xml:space="preserve">
+ <value>The user specified by query param "relate" does not exist.</value>
+ </data>
+ <data name="TimelineController_QueryVisibilityUnknown" xml:space="preserve">
+ <value>'{0}' is an unkown visibility in the query parameter 'visibility'. </value>
+ </data>
+ <data name="TimelineController_TextContentTextRequired" xml:space="preserve">
+ <value>Text field is required for text content.</value>
+ </data>
+ <data name="TokenController_Create_BadCredential" xml:space="preserve">
+ <value>Username or password is invalid.</value>
+ </data>
+ <data name="TokenController_Verify_BadFormat" xml:space="preserve">
+ <value>The token is of bad format. It might not be created by the server.</value>
+ </data>
+ <data name="TokenController_Verify_OldVersion" xml:space="preserve">
+ <value>Token has an old version. User might have update some info.</value>
+ </data>
+ <data name="TokenController_Verify_TimeExpired" xml:space="preserve">
+ <value>The token is expired.</value>
+ </data>
+ <data name="TokenController_Verify_UserNotExist" xml:space="preserve">
+ <value>User does not exist. Administrator might have deleted this user.</value>
+ </data>
+ <data name="UserAvatar_BadFormat_BadSize" xml:space="preserve">
+ <value>Image is not a square.</value>
+ </data>
+ <data name="UserAvatar_BadFormat_CantDecode" xml:space="preserve">
+ <value>Image decode failed.</value>
+ </data>
+ <data name="UserAvatar_BadFormat_UnmatchedFormat" xml:space="preserve">
+ <value>Image format does not match the one in header.</value>
+ </data>
+ <data name="UserCommon_NotExist" xml:space="preserve">
+ <value>The user to operate on does not exist.</value>
+ </data>
+ <data name="UserController_ChangePassword_BadOldPassword" xml:space="preserve">
+ <value>Old password is wrong.</value>
+ </data>
+ <data name="UserController_Patch_Forbid_Administrator" xml:space="preserve">
+ <value>You can't set permission unless you are administrator.</value>
+ </data>
+ <data name="UserController_Patch_Forbid_Password" xml:space="preserve">
+ <value>You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .</value>
+ </data>
+ <data name="UserController_Patch_Forbid_Username" xml:space="preserve">
+ <value>You can't set username unless you are administrator.</value>
+ </data>
+ <data name="UserController_UsernameConflict" xml:space="preserve">
+ <value>A user with given username already exists.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs b/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs new file mode 100644 index 00000000..5165463e --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Http {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Common {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Common() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Common", typeof(Common).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An existent item is deleted..
+ /// </summary>
+ internal static string MessageDeleteDelete {
+ get {
+ return ResourceManager.GetString("MessageDeleteDelete", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The item does not exist, so nothing is changed..
+ /// </summary>
+ internal static string MessageDeleteNotExist {
+ get {
+ return ResourceManager.GetString("MessageDeleteNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A new item is created..
+ /// </summary>
+ internal static string MessagePutCreate {
+ get {
+ return ResourceManager.GetString("MessagePutCreate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An existent item is modified..
+ /// </summary>
+ internal static string MessagePutModify {
+ get {
+ return ResourceManager.GetString("MessagePutModify", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Models/Http/Common.resx b/BackEnd/Timeline/Resources/Models/Http/Common.resx new file mode 100644 index 00000000..85ec4d32 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Common.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageDeleteDelete" xml:space="preserve">
+ <value>An existent item is deleted.</value>
+ </data>
+ <data name="MessageDeleteNotExist" xml:space="preserve">
+ <value>The item does not exist, so nothing is changed.</value>
+ </data>
+ <data name="MessagePutCreate" xml:space="preserve">
+ <value>A new item is created.</value>
+ </data>
+ <data name="MessagePutModify" xml:space="preserve">
+ <value>An existent item is modified.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs b/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs new file mode 100644 index 00000000..19f42793 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Http {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Exception {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Exception() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Exception", typeof(Exception).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No action context currently, can't fill urls in value resolver..
+ /// </summary>
+ internal static string ActionContextNull {
+ get {
+ return ResourceManager.GetString("ActionContextNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown post content type..
+ /// </summary>
+ internal static string UnknownPostContentType {
+ get {
+ return ResourceManager.GetString("UnknownPostContentType", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Models/Http/Exception.resx b/BackEnd/Timeline/Resources/Models/Http/Exception.resx new file mode 100644 index 00000000..3f7bddb6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Exception.resx @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ActionContextNull" xml:space="preserve">
+ <value>No action context currently, can't fill urls in value resolver.</value>
+ </data>
+ <data name="UnknownPostContentType" xml:space="preserve">
+ <value>Unknown post content type.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs new file mode 100644 index 00000000..3050049e --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class NameValidator {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal NameValidator() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NameValidator", typeof(NameValidator).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An empty string is not allowed..
+ /// </summary>
+ internal static string MessageEmptyString {
+ get {
+ return ResourceManager.GetString("MessageEmptyString", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid character, only alphabet, digit, underscore and hyphen are allowed..
+ /// </summary>
+ internal static string MessageInvalidChar {
+ get {
+ return ResourceManager.GetString("MessageInvalidChar", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Too long, more than 26 characters is not premitted..
+ /// </summary>
+ internal static string MessageTooLong {
+ get {
+ return ResourceManager.GetString("MessageTooLong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Name can't be of the same format of unique id..
+ /// </summary>
+ internal static string MessageUnqiueId {
+ get {
+ return ResourceManager.GetString("MessageUnqiueId", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx new file mode 100644 index 00000000..5e7e1745 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageEmptyString" xml:space="preserve">
+ <value>An empty string is not allowed.</value>
+ </data>
+ <data name="MessageInvalidChar" xml:space="preserve">
+ <value>Invalid character, only alphabet, digit, underscore and hyphen are allowed.</value>
+ </data>
+ <data name="MessageTooLong" xml:space="preserve">
+ <value>Too long, more than 26 characters is not premitted.</value>
+ </data>
+ <data name="MessageUnqiueId" xml:space="preserve">
+ <value>Name can't be of the same format of unique id.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs new file mode 100644 index 00000000..522f305a --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class NicknameValidator {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal NicknameValidator() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NicknameValidator", typeof(NicknameValidator).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Nickname is too long..
+ /// </summary>
+ internal static string MessageTooLong {
+ get {
+ return ResourceManager.GetString("MessageTooLong", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx new file mode 100644 index 00000000..b191b505 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageTooLong" xml:space="preserve">
+ <value>Nickname is too long.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs new file mode 100644 index 00000000..74d4c169 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Validator {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Validator() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.Validator", typeof(Validator).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to create a validator instance from default constructor. See inner exception..
+ /// </summary>
+ internal static string ValidateWithAttributeExceptionCreateFail {
+ get {
+ return ResourceManager.GetString("ValidateWithAttributeExceptionCreateFail", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Given type is not assignable to IValidator..
+ /// </summary>
+ internal static string ValidateWithAttributeExceptionNotValidator {
+ get {
+ return ResourceManager.GetString("ValidateWithAttributeExceptionNotValidator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value is not of type {0}..
+ /// </summary>
+ internal static string ValidatorMessageBadType {
+ get {
+ return ResourceManager.GetString("ValidatorMessageBadType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value can't be null..
+ /// </summary>
+ internal static string ValidatorMessageNull {
+ get {
+ return ResourceManager.GetString("ValidatorMessageNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation succeeded..
+ /// </summary>
+ internal static string ValidatorMessageSuccess {
+ get {
+ return ResourceManager.GetString("ValidatorMessageSuccess", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Models/Validation/Validator.resx b/BackEnd/Timeline/Resources/Models/Validation/Validator.resx new file mode 100644 index 00000000..8317e3eb --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/Validator.resx @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ValidateWithAttributeExceptionCreateFail" xml:space="preserve">
+ <value>Failed to create a validator instance from default constructor. See inner exception.</value>
+ </data>
+ <data name="ValidateWithAttributeExceptionNotValidator" xml:space="preserve">
+ <value>Given type is not assignable to IValidator.</value>
+ </data>
+ <data name="ValidatorMessageBadType" xml:space="preserve">
+ <value>Value is not of type {0}.</value>
+ </data>
+ <data name="ValidatorMessageNull" xml:space="preserve">
+ <value>Value can't be null.</value>
+ </data>
+ <data name="ValidatorMessageSuccess" xml:space="preserve">
+ <value>Validation succeeded.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs b/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs new file mode 100644 index 00000000..0872059a --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class DataManager {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal DataManager() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.DataManager", typeof(DataManager).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Entry with given tag does not exist..
+ /// </summary>
+ internal static string ExceptionEntryNotExist {
+ get {
+ return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/DataManager.resx b/BackEnd/Timeline/Resources/Services/DataManager.resx new file mode 100644 index 00000000..688e0e96 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/DataManager.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionEntryNotExist" xml:space="preserve">
+ <value>Entry with given tag does not exist.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/Exception.Designer.cs b/BackEnd/Timeline/Resources/Services/Exception.Designer.cs new file mode 100644 index 00000000..21ca7b86 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exception.Designer.cs @@ -0,0 +1,234 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Exception {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Exception() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exception", typeof(Exception).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The password is wrong..
+ /// </summary>
+ internal static string BadPasswordException {
+ get {
+ return ResourceManager.GetString("BadPasswordException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server..
+ /// </summary>
+ internal static string HashedPasswordBadFromatException {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Not of valid base64 format. See inner exception..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotBase64 {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Decoded hashed password is of length 0..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotLength0 {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to See inner exception..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotOthers {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Salt length < 128 bits..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotSaltTooShort {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Subkey length < 128 bits..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown format marker..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotUnknownMarker {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token didn't pass verification because {0}..
+ /// </summary>
+ internal static string JwtUserTokenBadFormatException {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to id claim is not a number.
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionIdBadFormat {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to id claim does not exist.
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionIdMissing {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to other error, see inner exception for information.
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionOthers {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionOthers", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown error.
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionUnknown {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionUnknown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to version claim is not a number..
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionVersionBadFormat {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to version claim does not exist..
+ /// </summary>
+ internal static string JwtUserTokenBadFormatExceptionVersionMissing {
+ get {
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password is of bad format..
+ /// </summary>
+ internal static string PasswordBadFormatException {
+ get {
+ return ResourceManager.GetString("PasswordBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server..
+ /// </summary>
+ internal static string UserTokenBadFormatException {
+ get {
+ return ResourceManager.GetString("UserTokenBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad version..
+ /// </summary>
+ internal static string UserTokenBadVersionException {
+ get {
+ return ResourceManager.GetString("UserTokenBadVersionException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is expired because its expiration time has passed..
+ /// </summary>
+ internal static string UserTokenTimeExpireException {
+ get {
+ return ResourceManager.GetString("UserTokenTimeExpireException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/Exception.resx b/BackEnd/Timeline/Resources/Services/Exception.resx new file mode 100644 index 00000000..c31ed7c7 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exception.resx @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BadPasswordException" xml:space="preserve">
+ <value>The password is wrong.</value>
+ </data>
+ <data name="HashedPasswordBadFromatException" xml:space="preserve">
+ <value>The hashes password is of bad format. It might not be created by server.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotBase64" xml:space="preserve">
+ <value>Not of valid base64 format. See inner exception.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotLength0" xml:space="preserve">
+ <value>Decoded hashed password is of length 0.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotOthers" xml:space="preserve">
+ <value>See inner exception.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotSaltTooShort" xml:space="preserve">
+ <value>Salt length < 128 bits.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotSubkeyTooShort" xml:space="preserve">
+ <value>Subkey length < 128 bits.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotUnknownMarker" xml:space="preserve">
+ <value>Unknown format marker.</value>
+ </data>
+ <data name="JwtUserTokenBadFormatException" xml:space="preserve">
+ <value>The token didn't pass verification because {0}.</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionIdBadFormat" xml:space="preserve">
+ <value>id claim is not a number</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionIdMissing" xml:space="preserve">
+ <value>id claim does not exist</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionOthers" xml:space="preserve">
+ <value>other error, see inner exception for information</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionUnknown" xml:space="preserve">
+ <value>unknown error</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionVersionBadFormat" xml:space="preserve">
+ <value>version claim is not a number.</value>
+ </data>
+ <data name="JwtUserTokenBadFormatExceptionVersionMissing" xml:space="preserve">
+ <value>version claim does not exist.</value>
+ </data>
+ <data name="PasswordBadFormatException" xml:space="preserve">
+ <value>Password is of bad format.</value>
+ </data>
+ <data name="UserTokenBadFormatException" xml:space="preserve">
+ <value>The token is of bad format, which means it may not be created by the server.</value>
+ </data>
+ <data name="UserTokenBadVersionException" xml:space="preserve">
+ <value>The token is of bad version.</value>
+ </data>
+ <data name="UserTokenTimeExpireException" xml:space="preserve">
+ <value>The token is expired because its expiration time has passed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs b/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs new file mode 100644 index 00000000..1dbe11c9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Exceptions {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Exceptions() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exceptions", typeof(Exceptions).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A entity of type "{0}" already exists..
+ /// </summary>
+ internal static string EntityAlreadyExistError {
+ get {
+ return ResourceManager.GetString("EntityAlreadyExistError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The entity already exists..
+ /// </summary>
+ internal static string EntityAlreadyExistErrorDefault {
+ get {
+ return ResourceManager.GetString("EntityAlreadyExistErrorDefault", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The required entity of type "{0}" does not exist..
+ /// </summary>
+ internal static string EntityNotExistError {
+ get {
+ return ResourceManager.GetString("EntityNotExistError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The required entity does not exist..
+ /// </summary>
+ internal static string EntityNotExistErrorDefault {
+ get {
+ return ResourceManager.GetString("EntityNotExistErrorDefault", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image is in valid because {0}..
+ /// </summary>
+ internal static string ImageException {
+ get {
+ return ResourceManager.GetString("ImageException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image is not of required size.
+ /// </summary>
+ internal static string ImageExceptionBadSize {
+ get {
+ return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to failed to decode image, see inner exception.
+ /// </summary>
+ internal static string ImageExceptionCantDecode {
+ get {
+ return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown error.
+ /// </summary>
+ internal static string ImageExceptionUnknownError {
+ get {
+ return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image's actual mime type is not the specified one.
+ /// </summary>
+ internal static string ImageExceptionUnmatchedFormat {
+ get {
+ return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline has no data..
+ /// </summary>
+ internal static string TimelineNoDataException {
+ get {
+ return ResourceManager.GetString("TimelineNoDataException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException..
+ /// </summary>
+ internal static string TimelineNotExistException {
+ get {
+ return ResourceManager.GetString("TimelineNotExistException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}"..
+ /// </summary>
+ internal static string TimelinePostNotExistException {
+ get {
+ return ResourceManager.GetString("TimelinePostNotExistException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted..
+ /// </summary>
+ internal static string TimelinePostNotExistExceptionDeleted {
+ get {
+ return ResourceManager.GetString("TimelinePostNotExistExceptionDeleted", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Request username is "{0}". Request id is "{1}"..
+ /// </summary>
+ internal static string UserNotExistException {
+ get {
+ return ResourceManager.GetString("UserNotExistException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/Exceptions.resx b/BackEnd/Timeline/Resources/Services/Exceptions.resx new file mode 100644 index 00000000..e9595caa --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exceptions.resx @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 1.3
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">1.3</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1">this is my long string</data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ [base64 mime encoded serialized .NET Framework object]
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ [base64 mime encoded string representing a byte array form of the .NET Framework object]
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="EntityNotExistError" xml:space="preserve">
+ <value>The required entity of type "{0}" does not exist.</value>
+ </data>
+ <data name="EntityAlreadyExistErrorDefault" xml:space="preserve">
+ <value>The entity already exists.</value>
+ </data>
+ <data name="EntityAlreadyExistError" xml:space="preserve">
+ <value>A entity of type "{0}" already exists.</value>
+ </data>
+ <data name="EntityNotExistErrorDefault" xml:space="preserve">
+ <value>The required entity does not exist.</value>
+ </data>
+ <data name="TimelineNotExistException" xml:space="preserve">
+ <value>Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException.</value>
+ </data>
+ <data name="TimelinePostNotExistException" xml:space="preserve">
+ <value>Request timeline name is "{0}". Request timeline post id is "{1}".</value>
+ </data>
+ <data name="UserNotExistException" xml:space="preserve">
+ <value>Request username is "{0}". Request id is "{1}".</value>
+ </data>
+ <data name="TimelineNoDataException" xml:space="preserve">
+ <value>The timeline has no data.</value>
+ </data>
+ <data name="ImageException" xml:space="preserve">
+ <value>Image is in valid because {0}.</value>
+ </data>
+ <data name="ImageExceptionBadSize" xml:space="preserve">
+ <value>image is not of required size</value>
+ </data>
+ <data name="ImageExceptionCantDecode" xml:space="preserve">
+ <value>failed to decode image, see inner exception</value>
+ </data>
+ <data name="ImageExceptionUnknownError" xml:space="preserve">
+ <value>unknown error</value>
+ </data>
+ <data name="ImageExceptionUnmatchedFormat" xml:space="preserve">
+ <value>image's actual mime type is not the specified one</value>
+ </data>
+ <data name="TimelinePostNotExistExceptionDeleted" xml:space="preserve">
+ <value>Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs b/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs new file mode 100644 index 00000000..e16c1337 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class TimelineService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TimelineService() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The number {0} username is invalid..
+ /// </summary>
+ internal static string ExceptionChangeMemberUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown post content type "{0}" is saved in database..
+ /// </summary>
+ internal static string ExceptionDatabaseUnknownContentType {
+ get {
+ return ResourceManager.GetString("ExceptionDatabaseUnknownContentType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The owner username of personal timeline is of bad format..
+ /// </summary>
+ internal static string ExceptionFindTimelineUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The data entry of the tag of the image post does not exist..
+ /// </summary>
+ internal static string ExceptionGetDataDataEntryNotExist {
+ get {
+ return ResourceManager.GetString("ExceptionGetDataDataEntryNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Can't get data of a non-image post..
+ /// </summary>
+ internal static string ExceptionGetDataNonImagePost {
+ get {
+ return ResourceManager.GetString("ExceptionGetDataNonImagePost", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post has been deleted because content of entity is null..
+ /// </summary>
+ internal static string ExceptionPostDeleted {
+ get {
+ return ResourceManager.GetString("ExceptionPostDeleted", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline name is of bad format..
+ /// </summary>
+ internal static string ExceptionTimelineNameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionTimelineNameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline with given name already exists..
+ /// </summary>
+ internal static string ExceptionTimelineNameConflict {
+ get {
+ return ResourceManager.GetString("ExceptionTimelineNameConflict", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it..
+ /// </summary>
+ internal static string LogGetDataNoFormat {
+ get {
+ return ResourceManager.GetString("LogGetDataNoFormat", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/TimelineService.resx b/BackEnd/Timeline/Resources/Services/TimelineService.resx new file mode 100644 index 00000000..9314f51b --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/TimelineService.resx @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionChangeMemberUsernameBadFormat" xml:space="preserve">
+ <value>The number {0} username is invalid.</value>
+ </data>
+ <data name="ExceptionDatabaseUnknownContentType" xml:space="preserve">
+ <value>Unknown post content type "{0}" is saved in database.</value>
+ </data>
+ <data name="ExceptionFindTimelineUsernameBadFormat" xml:space="preserve">
+ <value>The owner username of personal timeline is of bad format.</value>
+ </data>
+ <data name="ExceptionGetDataDataEntryNotExist" xml:space="preserve">
+ <value>The data entry of the tag of the image post does not exist.</value>
+ </data>
+ <data name="ExceptionGetDataNonImagePost" xml:space="preserve">
+ <value>Can't get data of a non-image post.</value>
+ </data>
+ <data name="ExceptionTimelineNameBadFormat" xml:space="preserve">
+ <value>The timeline name is of bad format.</value>
+ </data>
+ <data name="ExceptionTimelineNameConflict" xml:space="preserve">
+ <value>The timeline with given name already exists.</value>
+ </data>
+ <data name="LogGetDataNoFormat" xml:space="preserve">
+ <value>Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it.</value>
+ </data>
+ <data name="ExceptionPostDeleted" xml:space="preserve">
+ <value>The post has been deleted because content of entity is null.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs new file mode 100644 index 00000000..c72d4215 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserAvatarService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarService() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserAvatarService", typeof(UserAvatarService).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data of avatar is null..
+ /// </summary>
+ internal static string ExceptionAvatarDataNull {
+ get {
+ return ResourceManager.GetString("ExceptionAvatarDataNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type of avatar is null or empty..
+ /// </summary>
+ internal static string ExceptionAvatarTypeNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ExceptionAvatarTypeNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not..
+ /// </summary>
+ internal static string ExceptionDatabaseCorruptedDataAndTypeNotSame {
+ get {
+ return ResourceManager.GetString("ExceptionDatabaseCorruptedDataAndTypeNotSame", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Created an entry in user_avatars..
+ /// </summary>
+ internal static string LogCreateEntity {
+ get {
+ return ResourceManager.GetString("LogCreateEntity", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Updated an entry in user_avatars..
+ /// </summary>
+ internal static string LogUpdateEntity {
+ get {
+ return ResourceManager.GetString("LogUpdateEntity", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/UserAvatarService.resx b/BackEnd/Timeline/Resources/Services/UserAvatarService.resx new file mode 100644 index 00000000..da9d7203 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserAvatarService.resx @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionAvatarDataNull" xml:space="preserve">
+ <value>Data of avatar is null.</value>
+ </data>
+ <data name="ExceptionAvatarTypeNullOrEmpty" xml:space="preserve">
+ <value>Type of avatar is null or empty.</value>
+ </data>
+ <data name="ExceptionDatabaseCorruptedDataAndTypeNotSame" xml:space="preserve">
+ <value>Database corupted! One of type and data of a avatar is null but the other is not.</value>
+ </data>
+ <data name="LogCreateEntity" xml:space="preserve">
+ <value>Created an entry in user_avatars.</value>
+ </data>
+ <data name="LogUpdateEntity" xml:space="preserve">
+ <value>Updated an entry in user_avatars.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserService.Designer.cs new file mode 100644 index 00000000..cdf7f390 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserService.Designer.cs @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserService() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserService", typeof(UserService).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to New username is of bad format..
+ /// </summary>
+ internal static string ExceptionNewUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionNewUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Nickname is of bad format, because {}..
+ /// </summary>
+ internal static string ExceptionNicknameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionNicknameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Old username is of bad format..
+ /// </summary>
+ internal static string ExceptionOldUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionOldUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password can't be empty..
+ /// </summary>
+ internal static string ExceptionPasswordEmpty {
+ get {
+ return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password can't be null..
+ /// </summary>
+ internal static string ExceptionPasswordNull {
+ get {
+ return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Username is of bad format, because {}..
+ /// </summary>
+ internal static string ExceptionUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user with given username already exists..
+ /// </summary>
+ internal static string ExceptionUsernameConflict {
+ get {
+ return ResourceManager.GetString("ExceptionUsernameConflict", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Username can't be null..
+ /// </summary>
+ internal static string ExceptionUsernameNull {
+ get {
+ return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A new user entry is added to the database..
+ /// </summary>
+ internal static string LogDatabaseCreate {
+ get {
+ return ResourceManager.GetString("LogDatabaseCreate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user entry is removed from the database..
+ /// </summary>
+ internal static string LogDatabaseRemove {
+ get {
+ return ResourceManager.GetString("LogDatabaseRemove", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user entry is updated to the database..
+ /// </summary>
+ internal static string LogDatabaseUpdate {
+ get {
+ return ResourceManager.GetString("LogDatabaseUpdate", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/UserService.resx b/BackEnd/Timeline/Resources/Services/UserService.resx new file mode 100644 index 00000000..09bd4abb --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserService.resx @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionNewUsernameBadFormat" xml:space="preserve">
+ <value>New username is of bad format.</value>
+ </data>
+ <data name="ExceptionNicknameBadFormat" xml:space="preserve">
+ <value>Nickname is of bad format, because {}.</value>
+ </data>
+ <data name="ExceptionOldUsernameBadFormat" xml:space="preserve">
+ <value>Old username is of bad format.</value>
+ </data>
+ <data name="ExceptionPasswordEmpty" xml:space="preserve">
+ <value>Password can't be empty.</value>
+ </data>
+ <data name="ExceptionPasswordNull" xml:space="preserve">
+ <value>Password can't be null.</value>
+ </data>
+ <data name="ExceptionUsernameBadFormat" xml:space="preserve">
+ <value>Username is of bad format, because {}.</value>
+ </data>
+ <data name="ExceptionUsernameConflict" xml:space="preserve">
+ <value>A user with given username already exists.</value>
+ </data>
+ <data name="ExceptionUsernameNull" xml:space="preserve">
+ <value>Username can't be null.</value>
+ </data>
+ <data name="LogDatabaseCreate" xml:space="preserve">
+ <value>A new user entry is added to the database.</value>
+ </data>
+ <data name="LogDatabaseRemove" xml:space="preserve">
+ <value>A user entry is removed from the database.</value>
+ </data>
+ <data name="LogDatabaseUpdate" xml:space="preserve">
+ <value>A user entry is updated to the database.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs new file mode 100644 index 00000000..3c3c7e41 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserTokenService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserTokenService() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserTokenService", typeof(UserTokenService).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Jwt token key is not set in database. Maybe you forget to migrate the database..
+ /// </summary>
+ internal static string JwtKeyNotExist {
+ get {
+ return ResourceManager.GetString("JwtKeyNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Resources/Services/UserTokenService.resx b/BackEnd/Timeline/Resources/Services/UserTokenService.resx new file mode 100644 index 00000000..1ce78427 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserTokenService.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="JwtKeyNotExist" xml:space="preserve">
+ <value>Jwt token key is not set in database. Maybe you forget to migrate the database.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs b/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs new file mode 100644 index 00000000..ca38a0d9 --- /dev/null +++ b/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using System.Linq;
+
+namespace Timeline.Routes
+{
+ public static class MvcOptionsExtensions
+ {
+ public static void UseApiRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute)
+ {
+ opts.Conventions.Add(new ApiRoutePrefixConvention(routeAttribute));
+ }
+
+ public static void UseApiRoutePrefix(this MvcOptions opts, string prefix)
+ {
+ opts.UseApiRoutePrefix(new RouteAttribute(prefix));
+ }
+ }
+
+ public class ApiRoutePrefixConvention : IApplicationModelConvention
+ {
+ private readonly AttributeRouteModel _routePrefix;
+
+ public ApiRoutePrefixConvention(IRouteTemplateProvider route)
+ {
+ _routePrefix = new AttributeRouteModel(route);
+ }
+
+ public void Apply(ApplicationModel application)
+ {
+ foreach (var selector in application.Controllers.Where(c => c.Filters.Any(f => f is IApiBehaviorMetadata)).SelectMany(c => c.Selectors))
+ {
+ if (selector.AttributeRouteModel != null)
+ {
+ selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel);
+ }
+ else
+ {
+ selector.AttributeRouteModel = _routePrefix;
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs b/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs new file mode 100644 index 00000000..25ec563c --- /dev/null +++ b/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Net.Mime;
+using System.Text.Json;
+using Timeline.Models.Http;
+
+namespace Timeline.Routes
+{
+ public static class UnknownEndpointMiddleware
+ {
+ public static void Attach(IApplicationBuilder app)
+ {
+ app.Use(async (context, next) =>
+ {
+ if (context.GetEndpoint() != null)
+ {
+ await next();
+ return;
+ }
+
+ if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase))
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ context.Response.ContentType = MediaTypeNames.Application.Json;
+
+ var body = JsonSerializer.SerializeToUtf8Bytes(ErrorResponse.Common.UnknownEndpoint(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+
+ context.Response.ContentLength = body.Length;
+ await context.Response.Body.WriteAsync(body);
+ await context.Response.CompleteAsync();
+ return;
+ }
+
+ await next();
+ });
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/BadPasswordException.cs b/BackEnd/Timeline/Services/BadPasswordException.cs new file mode 100644 index 00000000..f609371d --- /dev/null +++ b/BackEnd/Timeline/Services/BadPasswordException.cs @@ -0,0 +1,27 @@ +using System;
+using Timeline.Helpers;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class BadPasswordException : Exception
+ {
+ public BadPasswordException() : base(Resources.Services.Exception.BadPasswordException) { }
+ public BadPasswordException(string message, Exception inner) : base(message, inner) { }
+
+ public BadPasswordException(string badPassword)
+ : base(Log.Format(Resources.Services.Exception.BadPasswordException, ("Bad Password", badPassword)))
+ {
+ Password = badPassword;
+ }
+
+ protected BadPasswordException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ /// <summary>
+ /// The wrong password.
+ /// </summary>
+ public string? Password { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Clock.cs b/BackEnd/Timeline/Services/Clock.cs new file mode 100644 index 00000000..4395edcd --- /dev/null +++ b/BackEnd/Timeline/Services/Clock.cs @@ -0,0 +1,29 @@ +using System;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Convenient for unit test.
+ /// </summary>
+ public interface IClock
+ {
+ /// <summary>
+ /// Get current time.
+ /// </summary>
+ /// <returns>Current time.</returns>
+ DateTime GetCurrentTime();
+ }
+
+ public class Clock : IClock
+ {
+ public Clock()
+ {
+
+ }
+
+ public DateTime GetCurrentTime()
+ {
+ return DateTime.UtcNow;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs new file mode 100644 index 00000000..d447b0d5 --- /dev/null +++ b/BackEnd/Timeline/Services/DataManager.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// A data manager controlling data.
+ /// </summary>
+ /// <remarks>
+ /// Identical data will be saved as one copy and return the same tag.
+ /// Every data has a ref count. When data is retained, ref count increase.
+ /// When data is freed, ref count decease. If ref count is decreased
+ /// to 0, the data entry will be destroyed and no longer occupy space.
+ /// </remarks>
+ public interface IDataManager
+ {
+ /// <summary>
+ /// Saves the data to a new entry if it does not exist,
+ /// increases its ref count and returns a tag to the entry.
+ /// </summary>
+ /// <param name="data">The data. Can't be null.</param>
+ /// <returns>The tag of the created entry.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ public Task<string> RetainEntry(byte[] data);
+
+ /// <summary>
+ /// Decrease the the ref count of the entry.
+ /// Remove it if ref count is zero.
+ /// </summary>
+ /// <param name="tag">The tag of the entry.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
+ /// <remarks>
+ /// It's no-op if entry with tag does not exist.
+ /// </remarks>
+ public Task FreeEntry(string tag);
+
+ /// <summary>
+ /// Retrieve the entry with given tag.
+ /// </summary>
+ /// <param name="tag">The tag of the entry.</param>
+ /// <returns>The data of the entry.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tag"/> is null.</exception>
+ /// <exception cref="InvalidOperationException">Thrown when entry with given tag does not exist.</exception>
+ public Task<byte[]> GetEntry(string tag);
+ }
+
+ public class DataManager : IDataManager
+ {
+ private readonly DatabaseContext _database;
+ private readonly IETagGenerator _eTagGenerator;
+
+ public DataManager(DatabaseContext database, IETagGenerator eTagGenerator)
+ {
+ _database = database;
+ _eTagGenerator = eTagGenerator;
+ }
+
+ public async Task<string> RetainEntry(byte[] data)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var tag = await _eTagGenerator.Generate(data);
+
+ var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
+
+ if (entity == null)
+ {
+ entity = new DataEntity
+ {
+ Tag = tag,
+ Data = data,
+ Ref = 1
+ };
+ _database.Data.Add(entity);
+ }
+ else
+ {
+ entity.Ref += 1;
+ }
+ await _database.SaveChangesAsync();
+ return tag;
+ }
+
+ public async Task FreeEntry(string tag)
+ {
+ if (tag == null)
+ throw new ArgumentNullException(nameof(tag));
+
+ var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
+
+ if (entity != null)
+ {
+ if (entity.Ref == 1)
+ {
+ _database.Data.Remove(entity);
+ }
+ else
+ {
+ entity.Ref -= 1;
+ }
+ await _database.SaveChangesAsync();
+ }
+ }
+
+ public async Task<byte[]> GetEntry(string tag)
+ {
+ if (tag == null)
+ throw new ArgumentNullException(nameof(tag));
+
+ var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist);
+
+ return entity.Data;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/DatabaseBackupService.cs b/BackEnd/Timeline/Services/DatabaseBackupService.cs new file mode 100644 index 00000000..a76b2a0d --- /dev/null +++ b/BackEnd/Timeline/Services/DatabaseBackupService.cs @@ -0,0 +1,35 @@ +using System.Globalization;
+using System.IO;
+
+namespace Timeline.Services
+{
+ public interface IDatabaseBackupService
+ {
+ void BackupNow();
+ }
+
+ public class DatabaseBackupService : IDatabaseBackupService
+ {
+ private readonly IPathProvider _pathProvider;
+ private readonly IClock _clock;
+
+ public DatabaseBackupService(IPathProvider pathProvider, IClock clock)
+ {
+ _pathProvider = pathProvider;
+ _clock = clock;
+ }
+
+ public void BackupNow()
+ {
+ var databasePath = _pathProvider.GetDatabaseFilePath();
+ if (File.Exists(databasePath))
+ {
+ var backupDirPath = _pathProvider.GetDatabaseBackupDirectory();
+ Directory.CreateDirectory(backupDirPath);
+ var fileName = _clock.GetCurrentTime().ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture);
+ var path = Path.Combine(backupDirPath, fileName);
+ File.Copy(databasePath, path);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/DatabaseCorruptedException.cs b/BackEnd/Timeline/Services/DatabaseCorruptedException.cs new file mode 100644 index 00000000..9988e0ad --- /dev/null +++ b/BackEnd/Timeline/Services/DatabaseCorruptedException.cs @@ -0,0 +1,15 @@ +using System;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class DatabaseCorruptedException : Exception
+ {
+ public DatabaseCorruptedException() { }
+ public DatabaseCorruptedException(string message) : base(message) { }
+ public DatabaseCorruptedException(string message, Exception inner) : base(message, inner) { }
+ protected DatabaseCorruptedException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/BackEnd/Timeline/Services/ETagGenerator.cs b/BackEnd/Timeline/Services/ETagGenerator.cs new file mode 100644 index 00000000..4493e903 --- /dev/null +++ b/BackEnd/Timeline/Services/ETagGenerator.cs @@ -0,0 +1,45 @@ +using System;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+ public interface IETagGenerator
+ {
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
+ Task<string> Generate(byte[] source);
+ }
+
+ public sealed class ETagGenerator : IETagGenerator, IDisposable
+ {
+ private readonly SHA1 _sha1;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")]
+ public ETagGenerator()
+ {
+ _sha1 = SHA1.Create();
+ }
+
+ public Task<string> Generate(byte[] source)
+ {
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
+
+ return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source)));
+ }
+
+ private bool _disposed; // To detect redundant calls
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _sha1.Dispose();
+ _disposed = true;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/EntityNames.cs b/BackEnd/Timeline/Services/EntityNames.cs new file mode 100644 index 00000000..0ce1de3b --- /dev/null +++ b/BackEnd/Timeline/Services/EntityNames.cs @@ -0,0 +1,14 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+ public static class EntityNames
+ {
+ public const string User = "User";
+ public const string Timeline = "Timeline";
+ public const string TimelinePost = "TimelinePost";
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs b/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs new file mode 100644 index 00000000..7db2e860 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs @@ -0,0 +1,63 @@ +using System;
+using System.Globalization;
+using System.Text;
+
+namespace Timeline.Services.Exceptions
+{
+ /// <summary>
+ /// Thrown when an entity is already exists.
+ /// </summary>
+ /// <remarks>
+ /// For example, want to create a timeline but a timeline with the same name already exists.
+ /// </remarks>
+ [Serializable]
+ public class EntityAlreadyExistException : Exception
+ {
+ private readonly string? _entityName;
+
+ public EntityAlreadyExistException() : this(null, null, null, null) { }
+
+ public EntityAlreadyExistException(string? entityName) : this(entityName, null) { }
+
+ public EntityAlreadyExistException(string? entityName, Exception? inner) : this(entityName, null, null, null, inner) { }
+
+ public EntityAlreadyExistException(string? entityName, object? entity = null) : this(entityName, null, entity, null, null) { }
+ public EntityAlreadyExistException(Type? entityType, object? entity = null) : this(null, entityType, entity, null, null) { }
+ public EntityAlreadyExistException(string? entityName, Type? entityType, object? entity = null, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner)
+ {
+ _entityName = entityName;
+ EntityType = entityType;
+ Entity = entity;
+ }
+
+ private static string MakeMessage(string? entityName, Type? entityType, string? message)
+ {
+ string? name = entityName ?? (entityType?.Name);
+
+ var result = new StringBuilder();
+
+ if (name == null)
+ result.Append(Resources.Services.Exceptions.EntityAlreadyExistErrorDefault);
+ else
+ result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityAlreadyExistError, name);
+
+ if (message != null)
+ {
+ result.Append(' ');
+ result.Append(message);
+ }
+
+ return result.ToString();
+ }
+
+ protected EntityAlreadyExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? EntityName => _entityName ?? (EntityType?.Name);
+
+ public Type? EntityType { get; }
+
+ public object? Entity { get; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs b/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs new file mode 100644 index 00000000..e79496d3 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs @@ -0,0 +1,55 @@ +using System;
+using System.Globalization;
+using System.Text;
+
+namespace Timeline.Services.Exceptions
+{
+ /// <summary>
+ /// Thrown when you want to get an entity that does not exist.
+ /// </summary>
+ /// <example>
+ /// For example, you want to get a timeline with given name but it does not exist.
+ /// </example>
+ [Serializable]
+ public class EntityNotExistException : Exception
+ {
+ public EntityNotExistException() : this(null, null, null, null) { }
+ public EntityNotExistException(string? entityName) : this(entityName, null, null, null) { }
+ public EntityNotExistException(Type? entityType) : this(null, entityType, null, null) { }
+ public EntityNotExistException(string? entityName, Exception? inner) : this(entityName, null, null, inner) { }
+ public EntityNotExistException(Type? entityType, Exception? inner) : this(null, entityType, null, inner) { }
+ public EntityNotExistException(string? entityName, Type? entityType, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner)
+ {
+ EntityName = entityName;
+ EntityType = entityType;
+ }
+
+ private static string MakeMessage(string? entityName, Type? entityType, string? message)
+ {
+ string? name = entityName ?? (entityType?.Name);
+
+ var result = new StringBuilder();
+
+ if (name == null)
+ result.Append(Resources.Services.Exceptions.EntityNotExistErrorDefault);
+ else
+ result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityNotExistError, name);
+
+ if (message != null)
+ {
+ result.Append(' ');
+ result.Append(message);
+ }
+
+ return result.ToString();
+ }
+
+ protected EntityNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? EntityName { get; }
+
+ public Type? EntityType { get; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs b/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs new file mode 100644 index 00000000..be3c42a4 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs @@ -0,0 +1,13 @@ +namespace Timeline.Services.Exceptions
+{
+ public static class ExceptionMessageHelper
+ {
+ public static string AppendAdditionalMessage(this string origin, string? message)
+ {
+ if (message == null)
+ return origin;
+ else
+ return origin + " " + message;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/ImageException.cs b/BackEnd/Timeline/Services/Exceptions/ImageException.cs new file mode 100644 index 00000000..20dd48ae --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/ImageException.cs @@ -0,0 +1,57 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services.Exceptions
+{
+ [Serializable]
+ public class ImageException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not of required size.
+ /// </summary>
+ NotSquare,
+ /// <summary>
+ /// Other unknown errer.
+ /// </summary>
+ Unknown
+ }
+
+ public ImageException() : this(null) { }
+ public ImageException(string? message) : this(message, null) { }
+ public ImageException(string? message, Exception? inner) : this(ErrorReason.Unknown, null, null, null, message, inner) { }
+
+ public ImageException(ErrorReason error, byte[]? data, string? requestType = null, string? realType = null, string? message = null, Exception? inner = null) : base(MakeMessage(error).AppendAdditionalMessage(message), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; }
+
+ protected ImageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ private static string MakeMessage(ErrorReason? reason) =>
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.ImageException, reason switch
+ {
+ ErrorReason.CantDecode => Resources.Services.Exceptions.ImageExceptionCantDecode,
+ ErrorReason.UnmatchedFormat => Resources.Services.Exceptions.ImageExceptionUnmatchedFormat,
+ ErrorReason.NotSquare => Resources.Services.Exceptions.ImageExceptionBadSize,
+ _ => Resources.Services.Exceptions.ImageExceptionUnknownError
+ });
+
+ public ErrorReason Error { get; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[]? ImageData { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ public string? RequestType { get; }
+
+ // This field will be null if decoding failed.
+ public string? RealType { get; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs new file mode 100644 index 00000000..70970b24 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs @@ -0,0 +1,21 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services.Exceptions
+{
+ [Serializable]
+ public class TimelineNotExistException : EntityNotExistException
+ {
+ public TimelineNotExistException() : this(null, null) { }
+ public TimelineNotExistException(string? timelineName) : this(timelineName, null) { }
+ public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { }
+ public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null)
+ : base(EntityNames.Timeline, null, string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.TimelineNotExistException, timelineName ?? "").AppendAdditionalMessage(message), inner) { TimelineName = timelineName; }
+
+ protected TimelineNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? TimelineName { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs new file mode 100644 index 00000000..c4b6bf62 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs @@ -0,0 +1,15 @@ +using System;
+
+namespace Timeline.Services.Exceptions
+{
+ [Serializable]
+ public class TimelinePostNoDataException : Exception
+ {
+ public TimelinePostNoDataException() : this(null, null) { }
+ public TimelinePostNoDataException(string? message) : this(message, null) { }
+ public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { }
+ protected TimelinePostNoDataException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs new file mode 100644 index 00000000..f95dd410 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs @@ -0,0 +1,33 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services.Exceptions
+{
+ [Serializable]
+ public class TimelinePostNotExistException : EntityNotExistException
+ {
+ public TimelinePostNotExistException() : this(null, null, false, null, null) { }
+ [Obsolete("This has no meaning.")]
+ public TimelinePostNotExistException(string? message) : this(message, null) { }
+ [Obsolete("This has no meaning.")]
+ public TimelinePostNotExistException(string? message, Exception? inner) : this(null, null, false, message, inner) { }
+ protected TimelinePostNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public TimelinePostNotExistException(string? timelineName, long? id, bool isDelete, string? message = null, Exception? inner = null) : base(EntityNames.TimelinePost, null, MakeMessage(timelineName, id, isDelete).AppendAdditionalMessage(message), inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
+
+ private static string MakeMessage(string? timelineName, long? id, bool isDelete)
+ {
+ return string.Format(CultureInfo.InvariantCulture, isDelete ? Resources.Services.Exceptions.TimelinePostNotExistExceptionDeleted : Resources.Services.Exceptions.TimelinePostNotExistException, timelineName ?? "", id);
+ }
+
+ public string? TimelineName { get; set; }
+ public long? Id { get; set; }
+
+ /// <summary>
+ /// True if the post is deleted. False if the post does not exist at all.
+ /// </summary>
+ public bool IsDelete { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs new file mode 100644 index 00000000..7ef714df --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs @@ -0,0 +1,40 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services.Exceptions
+{
+ /// <summary>
+ /// The user requested does not exist.
+ /// </summary>
+ [Serializable]
+ public class UserNotExistException : EntityNotExistException
+ {
+ public UserNotExistException() : this(null, null, null, null) { }
+ public UserNotExistException(string? username, Exception? inner) : this(username, null, null, inner) { }
+
+ public UserNotExistException(string? username) : this(username, null, null, null) { }
+
+ public UserNotExistException(long id) : this(null, id, null, null) { }
+
+ public UserNotExistException(string? username, long? id, string? message, Exception? inner) : base(EntityNames.User, null,
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.UserNotExistException, username ?? "", id).AppendAdditionalMessage(message), inner)
+ {
+ Username = username;
+ Id = id;
+ }
+
+ protected UserNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ /// <summary>
+ /// The username of the user that does not exist.
+ /// </summary>
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// The id of the user that does not exist.
+ /// </summary>
+ public long? Id { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/ImageValidator.cs b/BackEnd/Timeline/Services/ImageValidator.cs new file mode 100644 index 00000000..59424a7c --- /dev/null +++ b/BackEnd/Timeline/Services/ImageValidator.cs @@ -0,0 +1,54 @@ +using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public interface IImageValidator
+ {
+ /// <summary>
+ /// Validate a image data.
+ /// </summary>
+ /// <param name="data">The data of the image. Can't be null.</param>
+ /// <param name="requestType">If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check.</param>
+ /// <param name="square">If true, image must be square.</param>
+ /// <returns>The format.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
+ Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false);
+ }
+
+ public class ImageValidator : IImageValidator
+ {
+ public ImageValidator()
+ {
+ }
+
+ public async Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var format = await Task.Run(() =>
+ {
+ try
+ {
+ using var image = Image.Load(data, out IImageFormat format);
+ if (requestType != null && !format.MimeTypes.Contains(requestType))
+ throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType);
+ if (square && image.Width != image.Height)
+ throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType);
+ return format;
+ }
+ catch (UnknownImageFormatException e)
+ {
+ throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e);
+ }
+ });
+ return format;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs b/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs new file mode 100644 index 00000000..c528c3e3 --- /dev/null +++ b/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs @@ -0,0 +1,48 @@ +using System;
+using System.Globalization;
+using static Timeline.Resources.Services.Exception;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class JwtUserTokenBadFormatException : UserTokenBadFormatException
+ {
+ public enum ErrorKind
+ {
+ NoIdClaim,
+ IdClaimBadFormat,
+ NoVersionClaim,
+ VersionClaimBadFormat,
+ Other
+ }
+
+ public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { }
+ public JwtUserTokenBadFormatException(string message) : base(message) { }
+ public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; }
+ protected JwtUserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public ErrorKind ErrorType { get; set; }
+
+ private static string GetErrorMessage(ErrorKind type)
+ {
+ var reason = type switch
+ {
+ ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing,
+ ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat,
+ ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing,
+ ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat,
+ ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers,
+ _ => JwtUserTokenBadFormatExceptionUnknown
+ };
+
+ return string.Format(CultureInfo.CurrentCulture,
+ Resources.Services.Exception.JwtUserTokenBadFormatException, reason);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/PasswordBadFormatException.cs b/BackEnd/Timeline/Services/PasswordBadFormatException.cs new file mode 100644 index 00000000..2029ebb4 --- /dev/null +++ b/BackEnd/Timeline/Services/PasswordBadFormatException.cs @@ -0,0 +1,27 @@ +using System;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class PasswordBadFormatException : Exception
+ {
+ public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { }
+ public PasswordBadFormatException(string message) : base(message) { }
+ public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public PasswordBadFormatException(string password, string validationMessage) : this()
+ {
+ Password = password;
+ ValidationMessage = validationMessage;
+ }
+
+ protected PasswordBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Password { get; set; } = "";
+
+ public string ValidationMessage { get; set; } = "";
+ }
+}
diff --git a/BackEnd/Timeline/Services/PasswordService.cs b/BackEnd/Timeline/Services/PasswordService.cs new file mode 100644 index 00000000..8114a520 --- /dev/null +++ b/BackEnd/Timeline/Services/PasswordService.cs @@ -0,0 +1,224 @@ +using Microsoft.AspNetCore.Cryptography.KeyDerivation;
+using System;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Hashed password is of bad format.
+ /// </summary>
+ /// <seealso cref="IPasswordService.VerifyPassword(string, string)"/>
+ [Serializable]
+ public class HashedPasswordBadFromatException : Exception
+ {
+ private static string MakeMessage(string reason)
+ {
+ return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason;
+ }
+
+ public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { }
+
+ public HashedPasswordBadFromatException(string message) : base(message) { }
+ public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { }
+
+ public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; }
+ public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; }
+ protected HashedPasswordBadFromatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? HashedPassword { get; set; }
+ }
+
+ public interface IPasswordService
+ {
+ /// <summary>
+ /// Hash a password.
+ /// </summary>
+ /// <param name="password">The password to hash.</param>
+ /// <returns>A hashed representation of the supplied <paramref name="password"/>.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="password"/> is null.</exception>
+ string HashPassword(string password);
+
+ /// <summary>
+ /// Verify whether the password fits into the hashed one.
+ ///
+ /// Usually you only need to check the returned bool value.
+ /// Catching <see cref="HashedPasswordBadFromatException"/> usually is not necessary.
+ /// Because if your program logic is right and always call <see cref="HashPassword(string)"/>
+ /// and <see cref="VerifyPassword(string, string)"/> in pair, this exception will never be thrown.
+ /// A thrown one usually means the data you saved is corupted, which is a critical problem.
+ /// </summary>
+ /// <param name="hashedPassword">The hashed password.</param>
+ /// <param name="providedPassword">The password supplied for comparison.</param>
+ /// <returns>True indicating password is right. Otherwise false.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="hashedPassword"/> or <paramref name="providedPassword"/> is null.</exception>
+ /// <exception cref="HashedPasswordBadFromatException">Thrown when the hashed password is of bad format.</exception>
+ bool VerifyPassword(string hashedPassword, string providedPassword);
+ }
+
+ /// <summary>
+ /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs
+ /// Remove V2 format and unnecessary format version check.
+ /// Remove configuration options.
+ /// Remove user related parts.
+ /// Change the exceptions.
+ /// </summary>
+ public class PasswordService : IPasswordService
+ {
+ /* =======================
+ * HASHED PASSWORD FORMATS
+ * =======================
+ *
+ * Version 3:
+ * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
+ * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
+ * (All UInt32s are stored big-endian.)
+ */
+
+ private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
+
+ public PasswordService()
+ {
+ }
+
+ // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static bool ByteArraysEqual(byte[] a, byte[] b)
+ {
+ if (a == null && b == null)
+ {
+ return true;
+ }
+ if (a == null || b == null || a.Length != b.Length)
+ {
+ return false;
+ }
+ var areSame = true;
+ for (var i = 0; i < a.Length; i++)
+ {
+ areSame &= (a[i] == b[i]);
+ }
+ return areSame;
+ }
+
+ public string HashPassword(string password)
+ {
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+ return Convert.ToBase64String(HashPasswordV3(password, _rng));
+ }
+
+ private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
+ {
+ return HashPasswordV3(password, rng,
+ prf: KeyDerivationPrf.HMACSHA256,
+ iterCount: 10000,
+ saltSize: 128 / 8,
+ numBytesRequested: 256 / 8);
+ }
+
+ private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
+ {
+ // Produce a version 3 (see comment above) text hash.
+ byte[] salt = new byte[saltSize];
+ rng.GetBytes(salt);
+ byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
+
+ var outputBytes = new byte[13 + salt.Length + subkey.Length];
+ outputBytes[0] = 0x01; // format marker
+ WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
+ WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
+ WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
+ Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
+ Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
+ return outputBytes;
+ }
+
+ public bool VerifyPassword(string hashedPassword, string providedPassword)
+ {
+ if (hashedPassword == null)
+ throw new ArgumentNullException(nameof(hashedPassword));
+ if (providedPassword == null)
+ throw new ArgumentNullException(nameof(providedPassword));
+
+ byte[] decodedHashedPassword;
+ try
+ {
+ decodedHashedPassword = Convert.FromBase64String(hashedPassword);
+ }
+ catch (FormatException e)
+ {
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e);
+ }
+
+ // read the format marker from the hashed password
+ if (decodedHashedPassword.Length == 0)
+ {
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0);
+ }
+
+ return (decodedHashedPassword[0]) switch
+ {
+ 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword),
+ _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker),
+ };
+ }
+
+ private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString)
+ {
+ try
+ {
+ // Read header information
+ KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
+ int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
+ int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
+
+ // Read the salt: must be >= 128 bits
+ if (saltLength < 128 / 8)
+ {
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort);
+ }
+ byte[] salt = new byte[saltLength];
+ Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
+
+ // Read the subkey (the rest of the payload): must be >= 128 bits
+ int subkeyLength = hashedPassword.Length - 13 - salt.Length;
+ if (subkeyLength < 128 / 8)
+ {
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort);
+ }
+ byte[] expectedSubkey = new byte[subkeyLength];
+ Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
+
+ // Hash the incoming password and verify it
+ byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
+ return ByteArraysEqual(actualSubkey, expectedSubkey);
+ }
+ catch (Exception e)
+ {
+ // This should never occur except in the case of a malformed payload, where
+ // we might go off the end of the array. Regardless, a malformed payload
+ // implies verification failed.
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e);
+ }
+ }
+
+ private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
+ {
+ return ((uint)(buffer[offset + 0]) << 24)
+ | ((uint)(buffer[offset + 1]) << 16)
+ | ((uint)(buffer[offset + 2]) << 8)
+ | ((uint)(buffer[offset + 3]));
+ }
+
+ private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
+ {
+ buffer[offset + 0] = (byte)(value >> 24);
+ buffer[offset + 1] = (byte)(value >> 16);
+ buffer[offset + 2] = (byte)(value >> 8);
+ buffer[offset + 3] = (byte)(value >> 0);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/PathProvider.cs b/BackEnd/Timeline/Services/PathProvider.cs new file mode 100644 index 00000000..1baba5c0 --- /dev/null +++ b/BackEnd/Timeline/Services/PathProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration;
+using System.IO;
+using Timeline.Configs;
+
+namespace Timeline.Services
+{
+ public interface IPathProvider
+ {
+ public string GetWorkingDirectory();
+ public string GetDatabaseFilePath();
+ public string GetDatabaseBackupDirectory();
+ }
+
+ public class PathProvider : IPathProvider
+ {
+ private readonly IConfiguration _configuration;
+
+ private readonly string _workingDirectory;
+
+
+ public PathProvider(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ _workingDirectory = configuration.GetValue<string?>(ApplicationConfiguration.WorkDirKey) ?? ApplicationConfiguration.DefaultWorkDir;
+ }
+
+ public string GetWorkingDirectory()
+ {
+ return _workingDirectory;
+ }
+
+ public string GetDatabaseFilePath()
+ {
+ return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseFileName);
+ }
+
+ public string GetDatabaseBackupDirectory()
+ {
+ return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseBackupDirectoryName);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs new file mode 100644 index 00000000..4bcae596 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -0,0 +1,1166 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SixLabors.ImageSharp;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Services.TimelineService;
+
+namespace Timeline.Services
+{
+ public static class TimelineHelper
+ {
+ public static string ExtractTimelineName(string name, out bool isPersonal)
+ {
+ if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase))
+ {
+ isPersonal = true;
+ return name.Substring(1);
+ }
+ else
+ {
+ isPersonal = false;
+ return name;
+ }
+ }
+ }
+
+ public enum TimelineUserRelationshipType
+ {
+ Own = 0b1,
+ Join = 0b10,
+ Default = Own | Join
+ }
+
+ public class TimelineUserRelationship
+ {
+ public TimelineUserRelationship(TimelineUserRelationshipType type, long userId)
+ {
+ Type = type;
+ UserId = userId;
+ }
+
+ public TimelineUserRelationshipType Type { get; set; }
+ public long UserId { get; set; }
+ }
+
+ public class PostData : ICacheableData
+ {
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; } = default!;
+#pragma warning restore CA1819 // Properties should not return arrays
+ public string Type { get; set; } = default!;
+ public string ETag { get; set; } = default!;
+ public DateTime? LastModified { get; set; } // TODO: Why nullable?
+ }
+
+ /// <summary>
+ /// This define the interface of both personal timeline and ordinary timeline.
+ /// </summary>
+ public interface ITimelineService
+ {
+ /// <summary>
+ /// Get the timeline last modified time (not include name change).
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<DateTime> GetTimelineLastModifiedTime(string timelineName);
+
+ /// <summary>
+ /// Get the timeline unique id.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<string> GetTimelineUniqueId(string timelineName);
+
+ /// <summary>
+ /// Get the timeline info.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<Models.Timeline> GetTimeline(string timelineName);
+
+ /// <summary>
+ /// Set the properties of a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="newProperties">The new properties. Null member means not to change.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="newProperties"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
+
+ /// <summary>
+ /// Get all the posts in the timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="modifiedSince">The time that posts have been modified since.</param>
+ /// <param name="includeDeleted">Whether include deleted posts.</param>
+ /// <returns>A list of all posts.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
+
+ /// <summary>
+ /// Get the etag of data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostData(string, long)"/>
+ Task<string> GetPostDataETag(string timelineName, long postId);
+
+ /// <summary>
+ /// Get the data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostDataETag(string, long)"/>
+ Task<PostData> GetPostData(string timelineName, long postId);
+
+ /// <summary>
+ /// Create a new text post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="text">The content text.</param>
+ /// <param name="time">The time of the post. If null, then current time is used.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
+
+ /// <summary>
+ /// Create a new image post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="imageData">The image data.</param>
+ /// <param name="time">The time of the post. If null, then use current time.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="imageData"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time);
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to delete post against.</param>
+ /// <param name="postId">The id of the post to delete.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
+ /// <remarks>
+ /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
+ /// </remarks>
+ Task DeletePost(string timelineName, long postId);
+
+ /// <summary>
+ /// Delete all posts of the given user. Used when delete a user.
+ /// </summary>
+ /// <param name="userId">The id of the user.</param>
+ Task DeleteAllPostsOfUser(long userId);
+
+ /// <summary>
+ /// Change member of timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="membersToAdd">A list of usernames of members to add. May be null.</param>
+ /// <param name="membersToRemove">A list of usernames of members to remove. May be null.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="ArgumentException">Thrown when names in <paramref name="membersToAdd"/> or <paramref name="membersToRemove"/> is not a valid username.</exception>
+ /// <exception cref="UserNotExistException">Thrown when one of the user to change does not exist.</exception>
+ /// <remarks>
+ /// Operating on a username that is of bad format or does not exist always throws.
+ /// Add a user that already is a member has no effects.
+ /// Remove a user that is not a member also has not effects.
+ /// Add and remove an identical user results in no effects.
+ /// More than one same usernames are regarded as one.
+ /// </remarks>
+ Task ChangeMember(string timelineName, IList<string>? membersToAdd, IList<string>? membersToRemove);
+
+ /// <summary>
+ /// Check whether a user can manage(change timeline info, member, ...) a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="userId">The id of the user to check on.</param>
+ /// <returns>True if the user can manage the timeline, otherwise false.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// This method does not check whether visitor is administrator.
+ /// Return false if user with user id does not exist.
+ /// </remarks>
+ Task<bool> HasManagePermission(string timelineName, long userId);
+
+ /// <summary>
+ /// Verify whether a visitor has the permission to read a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
+ /// <returns>True if can read, false if can't read.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// This method does not check whether visitor is administrator.
+ /// Return false if user with visitor id does not exist.
+ /// </remarks>
+ Task<bool> HasReadPermission(string timelineName, long? visitorId);
+
+ /// <summary>
+ /// Verify whether a user has the permission to modify a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <param name="modifierId">The id of the user to check on.</param>
+ /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
+ /// <returns>True if can modify, false if can't modify.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
+ /// <remarks>
+ /// Unless <paramref name="throwOnPostNotExist"/> is true, this method should return true if the post does not exist.
+ /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless <paramref name="throwOnPostNotExist"/> is true.
+ /// This method does not check whether the user is administrator.
+ /// It only checks whether he is the author of the post or the owner of the timeline.
+ /// Return false when user with modifier id does not exist.
+ /// </remarks>
+ Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
+
+ /// <summary>
+ /// Verify whether a user is member of a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="userId">The id of user to check on.</param>
+ /// <returns>True if it is a member, false if not.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// Timeline owner is also considered as a member.
+ /// Return false when user with user id does not exist.
+ /// </remarks>
+ Task<bool> IsMemberOf(string timelineName, long userId);
+
+ /// <summary>
+ /// Get all timelines including personal and ordinary timelines.
+ /// </summary>
+ /// <param name="relate">Filter timelines related (own or is a member) to specific user.</param>
+ /// <param name="visibility">Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored.</param>
+ /// <returns>The list of timelines.</returns>
+ /// <remarks>
+ /// If user with related user id does not exist, empty list will be returned.
+ /// </remarks>
+ Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
+
+ /// <summary>
+ /// Create a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="ownerId">The id of owner of the timeline.</param>
+ /// <returns>The info of the new timeline.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
+ /// <exception cref="EntityAlreadyExistException">Thrown when the timeline already exists.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
+ Task<Models.Timeline> CreateTimeline(string timelineName, long ownerId);
+
+ /// <summary>
+ /// Delete a timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to delete.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when the timeline does not exist.</exception>
+ Task DeleteTimeline(string timelineName);
+
+ /// <summary>
+ /// Change name of a timeline.
+ /// </summary>
+ /// <param name="oldTimelineName">The old timeline name.</param>
+ /// <param name="newTimelineName">The new timeline name.</param>
+ /// <returns>The new timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="oldTimelineName"/> or <paramref name="newTimelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="oldTimelineName"/> or <paramref name="newTimelineName"/> is of invalid format.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
+ /// <exception cref="EntityAlreadyExistException">Thrown when a timeline with new name already exists.</exception>
+ /// <remarks>
+ /// You can only change name of general timeline.
+ /// </remarks>
+ Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName);
+ }
+
+ public class TimelineService : ITimelineService
+ {
+ public TimelineService(ILogger<TimelineService> logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock)
+ {
+ _logger = logger;
+ _database = database;
+ _dataManager = dataManager;
+ _userService = userService;
+ _imageValidator = imageValidator;
+ _clock = clock;
+ }
+
+ private readonly ILogger<TimelineService> _logger;
+
+ private readonly DatabaseContext _database;
+
+ private readonly IDataManager _dataManager;
+
+ private readonly IUserService _userService;
+
+ private readonly IImageValidator _imageValidator;
+
+ private readonly IClock _clock;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
+
+ private void ValidateTimelineName(string name, string paramName)
+ {
+ if (!_timelineNameValidator.Validate(name, out var message))
+ {
+ throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName);
+ }
+ }
+
+ /// Remember to include Members when query.
+ private async Task<Models.Timeline> MapTimelineFromEntity(TimelineEntity entity)
+ {
+ var owner = await _userService.GetUserById(entity.OwnerId);
+
+ var members = new List<User>();
+ foreach (var memberEntity in entity.Members)
+ {
+ members.Add(await _userService.GetUserById(memberEntity.UserId));
+ }
+
+ var name = entity.Name ?? ("@" + owner.Username);
+
+ return new Models.Timeline
+ {
+ UniqueID = entity.UniqueId,
+ Name = name,
+ NameLastModified = entity.NameLastModified,
+ Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title,
+ Description = entity.Description ?? "",
+ Owner = owner,
+ Visibility = entity.Visibility,
+ Members = members,
+ CreateTime = entity.CreateTime,
+ LastModified = entity.LastModified
+ };
+ }
+
+ private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
+ {
+ User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(entity.AuthorId.Value) : null;
+
+ ITimelinePostContent? content = null;
+
+ if (entity.Content != null)
+ {
+ var type = entity.ContentType;
+
+ content = type switch
+ {
+ TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
+ TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
+ _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
+ };
+ }
+
+ return new TimelinePost(
+ id: entity.LocalId,
+ author: author,
+ content: content,
+ time: entity.Time,
+ lastUpdated: entity.LastUpdated,
+ timelineName: timelineName
+ );
+ }
+
+ private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
+ {
+ var currentTime = _clock.GetCurrentTime();
+
+ return new TimelineEntity
+ {
+ Name = name,
+ NameLastModified = currentTime,
+ OwnerId = ownerId,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = currentTime,
+ LastModified = currentTime,
+ CurrentPostLocalId = 0,
+ Members = new List<TimelineMemberEntity>()
+ };
+ }
+
+
+
+ // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created.
+ //
+ // This method will check the name format and if it is invalid, ArgumentException is thrown.
+ //
+ // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception.
+ // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown.
+ //
+ // It follows all timeline-related function common interface contracts.
+ private async Task<long> FindTimelineId(string timelineName)
+ {
+ timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+
+ if (isPersonal)
+ {
+ long userId;
+ try
+ {
+ userId = await _userService.GetUserIdByUsername(timelineName);
+ }
+ catch (ArgumentException e)
+ {
+ throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(timelineName, e);
+ }
+
+ var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity != null)
+ {
+ return timelineEntity.Id;
+ }
+ else
+ {
+ var newTimelineEntity = CreateNewTimelineEntity(null, userId);
+ _database.Timelines.Add(newTimelineEntity);
+ await _database.SaveChangesAsync();
+
+ return newTimelineEntity.Id;
+ }
+ }
+ else
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ ValidateTimelineName(timelineName, nameof(timelineName));
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity == null)
+ {
+ throw new TimelineNotExistException(timelineName);
+ }
+ else
+ {
+ return timelineEntity.Id;
+ }
+ }
+ }
+
+ public async Task<DateTime> GetTimelineLastModifiedTime(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
+
+ return timelineEntity.LastModified;
+ }
+
+ public async Task<string> GetTimelineUniqueId(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync();
+
+ return timelineEntity.UniqueId;
+ }
+
+ public async Task<Models.Timeline> GetTimeline(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync();
+
+ return await MapTimelineFromEntity(timelineEntity);
+ }
+
+ public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
+ {
+ modifiedSince = modifiedSince?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+ IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
+
+ if (!includeDeleted)
+ {
+ query = query.Where(p => p.Content != null);
+ }
+
+ if (modifiedSince.HasValue)
+ {
+ query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
+ }
+
+ query = query.OrderBy(p => p.Time);
+
+ var postEntities = await query.ToListAsync();
+
+ var posts = new List<TimelinePost>();
+ foreach (var entity in postEntities)
+ {
+ posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
+ }
+ return posts;
+ }
+
+ public async Task<string> GetPostDataETag(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ return tag;
+ }
+
+ public async Task<PostData> GetPostData(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ byte[] data;
+
+ try
+ {
+ data = await _dataManager.GetEntry(tag);
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
+ }
+
+ if (postEntity.ExtraContent == null)
+ {
+ _logger.LogWarning(LogGetDataNoFormat);
+ var format = Image.DetectFormat(data);
+ postEntity.ExtraContent = format.DefaultMimeType;
+ await _database.SaveChangesAsync();
+ }
+
+ return new PostData
+ {
+ Data = data,
+ Type = postEntity.ExtraContent,
+ ETag = tag,
+ LastModified = postEntity.LastUpdated
+ };
+ }
+
+ public async Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var timelineId = await FindTimelineId(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUserById(authorId);
+
+ var currentTime = _clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
+ {
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Text,
+ Content = text,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = finalTime,
+ LastUpdated = currentTime
+ };
+ _database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var timelineId = await FindTimelineId(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUserById(authorId);
+
+ var imageFormat = await _imageValidator.Validate(data);
+
+ var imageFormatText = imageFormat.DefaultMimeType;
+
+ var tag = await _dataManager.RetainEntry(data);
+
+ var currentTime = _clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
+ {
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Image,
+ Content = tag,
+ ExtraContent = imageFormatText,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = finalTime,
+ LastUpdated = currentTime
+ };
+ _database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new ImageTimelinePostContent(tag),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task DeletePost(string timelineName, long id)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
+
+ if (post == null)
+ throw new TimelinePostNotExistException(timelineName, id, false);
+
+ if (post.Content == null)
+ throw new TimelinePostNotExistException(timelineName, id, true);
+
+ string? dataTag = null;
+
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTag = post.Content;
+ }
+
+ post.Content = null;
+ post.LastUpdated = _clock.GetCurrentTime();
+
+ await _database.SaveChangesAsync();
+
+ if (dataTag != null)
+ {
+ await _dataManager.FreeEntry(dataTag);
+ }
+ }
+
+ public async Task DeleteAllPostsOfUser(long userId)
+ {
+ var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
+
+ var now = _clock.GetCurrentTime();
+
+ var dataTags = new List<string>();
+
+ foreach (var post in posts)
+ {
+ if (post.Content != null)
+ {
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTags.Add(post.Content);
+ }
+ post.Content = null;
+ }
+ post.LastUpdated = now;
+ }
+
+ await _database.SaveChangesAsync();
+
+ foreach (var dataTag in dataTags)
+ {
+ await _dataManager.FreeEntry(dataTag);
+ }
+ }
+
+ public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (newProperties == null)
+ throw new ArgumentNullException(nameof(newProperties));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var changed = false;
+
+ if (newProperties.Title != null)
+ {
+ changed = true;
+ timelineEntity.Title = newProperties.Title;
+ }
+
+ if (newProperties.Description != null)
+ {
+ changed = true;
+ timelineEntity.Description = newProperties.Description;
+ }
+
+ if (newProperties.Visibility.HasValue)
+ {
+ changed = true;
+ timelineEntity.Visibility = newProperties.Visibility.Value;
+ }
+
+ if (changed)
+ {
+ var currentTime = _clock.GetCurrentTime();
+ timelineEntity.LastModified = currentTime;
+ }
+
+ await _database.SaveChangesAsync();
+ }
+
+ public async Task ChangeMember(string timelineName, IList<string>? add, IList<string>? remove)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ List<string>? RemoveDuplicateAndCheckFormat(IList<string>? list, string paramName)
+ {
+ if (list != null)
+ {
+ List<string> result = new List<string>();
+ var count = list.Count;
+ for (var index = 0; index < count; index++)
+ {
+ var username = list[index];
+ if (result.Contains(username))
+ {
+ continue;
+ }
+ var (validationResult, message) = _usernameValidator.Validate(username);
+ if (!validationResult)
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName));
+ result.Add(username);
+ }
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add));
+ var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove));
+
+ // remove those both in add and remove
+ if (simplifiedAdd != null && simplifiedRemove != null)
+ {
+ var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList();
+ foreach (var u in usersToClean)
+ {
+ simplifiedAdd.Remove(u);
+ simplifiedRemove.Remove(u);
+ }
+
+ if (simplifiedAdd.Count == 0)
+ simplifiedAdd = null;
+
+ if (simplifiedRemove.Count == 0)
+ simplifiedRemove = null;
+ }
+
+ if (simplifiedAdd == null && simplifiedRemove == null)
+ return;
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
+ {
+ if (list == null)
+ return null;
+
+ List<long> result = new List<long>();
+ foreach (var username in list)
+ {
+ result.Add(await _userService.GetUserIdByUsername(username));
+ }
+ return result;
+ }
+ var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd);
+ var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove);
+
+ if (userIdsAdd != null)
+ {
+ var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList();
+ _database.TimelineMembers.AddRange(membersToAdd);
+ }
+
+ if (userIdsRemove != null)
+ {
+ var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync();
+ _database.TimelineMembers.RemoveRange(membersToRemove);
+ }
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+ timelineEntity.LastModified = _clock.GetCurrentTime();
+
+ await _database.SaveChangesAsync();
+ }
+
+ public async Task<bool> HasManagePermission(string timelineName, long userId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ return userId == timelineEntity.OwnerId;
+ }
+
+ public async Task<bool> HasReadPermission(string timelineName, long? visitorId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
+
+ if (timelineEntity.Visibility == TimelineVisibility.Public)
+ return true;
+
+ if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null)
+ return true;
+
+ if (visitorId == null)
+ {
+ return false;
+ }
+ else
+ {
+ var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync();
+ return memberEntity != null;
+ }
+ }
+
+ public async Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ {
+ if (throwOnPostNotExist)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+ else
+ return true;
+ }
+
+ if (postEntity.Content == null && throwOnPostNotExist)
+ {
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+ }
+
+ return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
+ }
+
+ public async Task<bool> IsMemberOf(string timelineName, long userId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await FindTimelineId(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ if (userId == timelineEntity.OwnerId)
+ return true;
+
+ return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
+ }
+
+ public async Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
+ {
+ List<TimelineEntity> entities;
+
+ IQueryable<TimelineEntity> ApplyTimelineVisibilityFilter(IQueryable<TimelineEntity> query)
+ {
+ if (visibility != null && visibility.Count != 0)
+ {
+ return query.Where(t => visibility.Contains(t.Visibility));
+ }
+ return query;
+ }
+
+ bool allVisibilities = visibility == null || visibility.Count == 0;
+
+ if (relate == null)
+ {
+ entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync();
+ }
+ else
+ {
+ entities = new List<TimelineEntity>();
+
+ if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
+ {
+ entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
+ }
+
+ if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
+ {
+ entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync());
+ }
+ }
+
+ var result = new List<Models.Timeline>();
+
+ foreach (var entity in entities)
+ {
+ result.Add(await MapTimelineFromEntity(entity));
+ }
+
+ return result;
+ }
+
+ public async Task<Models.Timeline> CreateTimeline(string name, long owner)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ ValidateTimelineName(name, nameof(name));
+
+ var user = await _userService.GetUserById(owner);
+
+ var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
+
+ if (conflict)
+ throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
+
+ var newEntity = CreateNewTimelineEntity(name, user.Id!.Value);
+
+ _database.Timelines.Add(newEntity);
+ await _database.SaveChangesAsync();
+
+ return await MapTimelineFromEntity(newEntity);
+ }
+
+ public async Task DeleteTimeline(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ ValidateTimelineName(name, nameof(name));
+
+ var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new TimelineNotExistException(name);
+
+ _database.Timelines.Remove(entity);
+ await _database.SaveChangesAsync();
+ }
+
+ public async Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName)
+ {
+ if (oldTimelineName == null)
+ throw new ArgumentNullException(nameof(oldTimelineName));
+ if (newTimelineName == null)
+ throw new ArgumentNullException(nameof(newTimelineName));
+
+ ValidateTimelineName(oldTimelineName, nameof(oldTimelineName));
+ ValidateTimelineName(newTimelineName, nameof(newTimelineName));
+
+ var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new TimelineNotExistException(oldTimelineName);
+
+ if (oldTimelineName == newTimelineName)
+ return await MapTimelineFromEntity(entity);
+
+ var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName);
+
+ if (conflict)
+ throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
+
+ var now = _clock.GetCurrentTime();
+
+ entity.Name = newTimelineName;
+ entity.NameLastModified = now;
+ entity.LastModified = now;
+
+ await _database.SaveChangesAsync();
+
+ return await MapTimelineFromEntity(entity);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs new file mode 100644 index 00000000..b41c45fd --- /dev/null +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -0,0 +1,265 @@ +using Microsoft.AspNetCore.Hosting;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public class Avatar
+ {
+ public string Type { get; set; } = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")]
+ public byte[] Data { get; set; } = default!;
+ }
+
+ public class AvatarInfo
+ {
+ public Avatar Avatar { get; set; } = default!;
+ public DateTime LastModified { get; set; }
+
+ public CacheableData ToCacheableData()
+ {
+ return new CacheableData(Avatar.Type, Avatar.Data, LastModified);
+ }
+ }
+
+ /// <summary>
+ /// Provider for default user avatar.
+ /// </summary>
+ /// <remarks>
+ /// Mainly for unit tests.
+ /// </remarks>
+ public interface IDefaultUserAvatarProvider
+ {
+ /// <summary>
+ /// Get the etag of default avatar.
+ /// </summary>
+ /// <returns></returns>
+ Task<string> GetDefaultAvatarETag();
+
+ /// <summary>
+ /// Get the default avatar.
+ /// </summary>
+ Task<AvatarInfo> GetDefaultAvatar();
+ }
+
+ public interface IUserAvatarService
+ {
+ /// <summary>
+ /// Get the etag of a user's avatar. Warning: This method does not check the user existence.
+ /// </summary>
+ /// <param name="id">The id of the user to get avatar etag of.</param>
+ /// <returns>The etag.</returns>
+ Task<string> GetAvatarETag(long id);
+
+ /// <summary>
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence.
+ /// </summary>
+ /// <param name="id">The id of the user to get avatar of.</param>
+ /// <returns>The avatar info.</returns>
+ Task<AvatarInfo> GetAvatar(long id);
+
+ /// <summary>
+ /// Set avatar for a user. Warning: This method does not check the user existence.
+ /// </summary>
+ /// <param name="id">The id of the user to set avatar for.</param>
+ /// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
+ /// <returns>The etag of the avatar.</returns>
+ /// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
+ /// <exception cref="ImageException">Thrown if avatar is of bad format.</exception>
+ Task<string> SetAvatar(long id, Avatar? avatar);
+ }
+
+ // TODO! : Make this configurable.
+ public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
+ {
+ private readonly IETagGenerator _eTagGenerator;
+
+ private readonly string _avatarPath;
+
+ private byte[] _cacheData = default!;
+ private DateTime _cacheLastModified;
+ private string _cacheETag = default!;
+
+ public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
+ {
+ _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
+ _eTagGenerator = eTagGenerator;
+ }
+
+ private async Task CheckAndInit()
+ {
+ var path = _avatarPath;
+ if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified)
+ {
+ _cacheData = await File.ReadAllBytesAsync(path);
+ _cacheLastModified = File.GetLastWriteTime(path);
+ _cacheETag = await _eTagGenerator.Generate(_cacheData);
+ }
+ }
+
+ public async Task<string> GetDefaultAvatarETag()
+ {
+ await CheckAndInit();
+ return _cacheETag;
+ }
+
+ public async Task<AvatarInfo> GetDefaultAvatar()
+ {
+ await CheckAndInit();
+ return new AvatarInfo
+ {
+ Avatar = new Avatar
+ {
+ Type = "image/png",
+ Data = _cacheData
+ },
+ LastModified = _cacheLastModified
+ };
+ }
+ }
+
+ public class UserAvatarService : IUserAvatarService
+ {
+
+ private readonly ILogger<UserAvatarService> _logger;
+
+ private readonly DatabaseContext _database;
+
+ private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
+
+ private readonly IImageValidator _imageValidator;
+
+ private readonly IDataManager _dataManager;
+
+ private readonly IClock _clock;
+
+ public UserAvatarService(
+ ILogger<UserAvatarService> logger,
+ DatabaseContext database,
+ IDefaultUserAvatarProvider defaultUserAvatarProvider,
+ IImageValidator imageValidator,
+ IDataManager dataManager,
+ IClock clock)
+ {
+ _logger = logger;
+ _database = database;
+ _defaultUserAvatarProvider = defaultUserAvatarProvider;
+ _imageValidator = imageValidator;
+ _dataManager = dataManager;
+ _clock = clock;
+ }
+
+ public async Task<string> GetAvatarETag(long id)
+ {
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag;
+ if (eTag == null)
+ return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
+ else
+ return eTag;
+ }
+
+ public async Task<AvatarInfo> GetAvatar(long id)
+ {
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync();
+
+ if (avatarEntity != null)
+ {
+ if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null))
+ {
+ var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame;
+ _logger.LogCritical(message);
+ throw new DatabaseCorruptedException(message);
+ }
+
+
+ if (avatarEntity.DataTag != null)
+ {
+ var data = await _dataManager.GetEntry(avatarEntity.DataTag);
+ return new AvatarInfo
+ {
+ Avatar = new Avatar
+ {
+ Type = avatarEntity.Type!,
+ Data = data
+ },
+ LastModified = avatarEntity.LastModified
+ };
+ }
+ }
+ var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
+ if (avatarEntity != null)
+ defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified;
+ return defaultAvatar;
+ }
+
+ public async Task<string> SetAvatar(long id, Avatar? avatar)
+ {
+ if (avatar != null)
+ {
+ if (avatar.Data == null)
+ throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar));
+ if (string.IsNullOrEmpty(avatar.Type))
+ throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar));
+ }
+
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync();
+
+ if (avatar == null)
+ {
+ if (avatarEntity != null && avatarEntity.DataTag != null)
+ {
+ await _dataManager.FreeEntry(avatarEntity.DataTag);
+ avatarEntity.DataTag = null;
+ avatarEntity.Type = null;
+ avatarEntity.LastModified = _clock.GetCurrentTime();
+ await _database.SaveChangesAsync();
+ _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity);
+ }
+ return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
+ }
+ else
+ {
+ await _imageValidator.Validate(avatar.Data, avatar.Type, true);
+ var tag = await _dataManager.RetainEntry(avatar.Data);
+ var oldTag = avatarEntity?.DataTag;
+ var create = avatarEntity == null;
+ if (avatarEntity == null)
+ {
+ avatarEntity = new UserAvatarEntity();
+ _database.UserAvatars.Add(avatarEntity);
+ }
+ avatarEntity.DataTag = tag;
+ avatarEntity.Type = avatar.Type;
+ avatarEntity.LastModified = _clock.GetCurrentTime();
+ avatarEntity.UserId = id;
+ await _database.SaveChangesAsync();
+ _logger.LogInformation(create ?
+ Resources.Services.UserAvatarService.LogCreateEntity
+ : Resources.Services.UserAvatarService.LogUpdateEntity);
+ if (oldTag != null)
+ {
+ await _dataManager.FreeEntry(oldTag);
+ }
+
+ return avatarEntity.DataTag;
+ }
+ }
+ }
+
+ public static class UserAvatarServiceCollectionExtensions
+ {
+ public static void AddUserAvatarService(this IServiceCollection services)
+ {
+ services.AddScoped<IUserAvatarService, UserAvatarService>();
+ services.AddScoped<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs new file mode 100644 index 00000000..845de573 --- /dev/null +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
+using static Timeline.Resources.Services.UserService;
+
+namespace Timeline.Services
+{
+ public interface IUserDeleteService
+ {
+ /// <summary>
+ /// Delete a user of given username.
+ /// </summary>
+ /// <param name="username">Username of the user to delete. Can't be null.</param>
+ /// <returns>True if user is deleted, false if user not exist.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ Task<bool> DeleteUser(string username);
+ }
+
+ public class UserDeleteService : IUserDeleteService
+ {
+ private readonly ILogger<UserDeleteService> _logger;
+
+ private readonly DatabaseContext _databaseContext;
+
+ private readonly ITimelineService _timelineService;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelineService timelineService)
+ {
+ _logger = logger;
+ _databaseContext = databaseContext;
+ _timelineService = timelineService;
+ }
+
+ public async Task<bool> DeleteUser(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ if (!_usernameValidator.Validate(username, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), nameof(username));
+ }
+
+ var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
+ if (user == null)
+ return false;
+
+ await _timelineService.DeleteAllPostsOfUser(user.Id);
+
+ _databaseContext.Users.Remove(user);
+
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", user.Username)));
+
+ return true;
+ }
+
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserRoleConvert.cs b/BackEnd/Timeline/Services/UserRoleConvert.cs new file mode 100644 index 00000000..f27ee1bb --- /dev/null +++ b/BackEnd/Timeline/Services/UserRoleConvert.cs @@ -0,0 +1,43 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ public static class UserRoleConvert
+ {
+ public const string UserRole = UserRoles.User;
+ public const string AdminRole = UserRoles.Admin;
+
+ public static string[] ToArray(bool administrator)
+ {
+ return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole };
+ }
+
+ public static string[] ToArray(string s)
+ {
+ return s.Split(',').ToArray();
+ }
+
+ public static bool ToBool(IReadOnlyCollection<string> roles)
+ {
+ return roles.Contains(AdminRole);
+ }
+
+ public static string ToString(IReadOnlyCollection<string> roles)
+ {
+ return string.Join(',', roles);
+ }
+
+ public static string ToString(bool administrator)
+ {
+ return administrator ? UserRole + "," + AdminRole : UserRole;
+ }
+
+ public static bool ToBool(string s)
+ {
+ return s.Contains("admin", StringComparison.InvariantCulture);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs new file mode 100644 index 00000000..821bc33d --- /dev/null +++ b/BackEnd/Timeline/Services/UserService.cs @@ -0,0 +1,437 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Services.UserService;
+
+namespace Timeline.Services
+{
+ public interface IUserService
+ {
+ /// <summary>
+ /// Try to verify the given username and password.
+ /// </summary>
+ /// <param name="username">The username of the user to verify.</param>
+ /// <param name="password">The password of the user to verify.</param>
+ /// <returns>The user info and auth info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or <paramref name="password"/> is empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
+ Task<User> VerifyCredential(string username, string password);
+
+ /// <summary>
+ /// Try to get a user by id.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <returns>The user info.</returns>
+ /// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
+ Task<User> GetUserById(long id);
+
+ /// <summary>
+ /// Get the user info of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The info of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<User> GetUserByUsername(string username);
+
+ /// <summary>
+ /// Get the user id of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The id of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsername(string username);
+
+ /// <summary>
+ /// List all users.
+ /// </summary>
+ /// <returns>The user info of users.</returns>
+ Task<User[]> GetUsers();
+
+ /// <summary>
+ /// Create a user with given info.
+ /// </summary>
+ /// <param name="info">The info of new user.</param>
+ /// <returns>The the new user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="info"/>is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="EntityAlreadyExistException">Thrown when a user with given username already exists.</exception>
+ /// <remarks>
+ /// <see cref="User.Username"/> must not be null and must be a valid username.
+ /// <see cref="User.Password"/> must not be null or empty.
+ /// <see cref="User.Administrator"/> is false by default (null).
+ /// <see cref="User.Nickname"/> must be a valid nickname if set. It is empty by default.
+ /// Other fields are ignored.
+ /// </remarks>
+ Task<User> CreateUser(User info);
+
+ /// <summary>
+ /// Modify a user's info.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <param name="info">The new info. May be null.</param>
+ /// <returns>The new user info.</returns>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <remarks>
+ /// Only <see cref="User.Username"/>, <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
+ /// If null, then not change.
+ /// Other fields are ignored.
+ /// Version will increase if password is changed.
+ ///
+ /// <see cref="User.Username"/> must be a valid username if set.
+ /// <see cref="User.Password"/> can't be empty if set.
+ /// <see cref="User.Nickname"/> must be a valid nickname if set.
+ ///
+ /// </remarks>
+ /// <seealso cref="ModifyUser(string, User)"/>
+ Task<User> ModifyUser(long id, User? info);
+
+ /// <summary>
+ /// Modify a user's info.
+ /// </summary>
+ /// <param name="username">The username of the user.</param>
+ /// <param name="info">The new info. May be null.</param>
+ /// <returns>The new user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <exception cref="EntityAlreadyExistException">Thrown when user with the newusername already exist.</exception>
+ /// <remarks>
+ /// Only <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
+ /// If null, then not change.
+ /// Other fields are ignored.
+ /// After modified, even if nothing is changed, version will increase.
+ ///
+ /// <see cref="User.Username"/> must be a valid username if set.
+ /// <see cref="User.Password"/> can't be empty if set.
+ /// <see cref="User.Nickname"/> must be a valid nickname if set.
+ ///
+ /// Note: Whether <see cref="User.Version"/> is set or not, version will increase and not set to the specified value if there is one.
+ /// </remarks>
+ /// <seealso cref="ModifyUser(long, User)"/>
+ Task<User> ModifyUser(string username, User? info);
+
+ /// <summary>
+ /// Try to change a user's password with old password.
+ /// </summary>
+ /// <param name="id">The id of user to change password of.</param>
+ /// <param name="oldPassword">Old password.</param>
+ /// <param name="newPassword">New password.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
+ Task ChangePassword(long id, string oldPassword, string newPassword);
+ }
+
+ public class UserService : IUserService
+ {
+ private readonly ILogger<UserService> _logger;
+ private readonly IClock _clock;
+
+ private readonly DatabaseContext _databaseContext;
+
+ private readonly IPasswordService _passwordService;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+ private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock)
+ {
+ _logger = logger;
+ _clock = clock;
+ _databaseContext = databaseContext;
+ _passwordService = passwordService;
+ }
+
+ private void CheckUsernameFormat(string username, string? paramName)
+ {
+ if (!_usernameValidator.Validate(username, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName);
+ }
+ }
+
+ private static void CheckPasswordFormat(string password, string? paramName)
+ {
+ if (password.Length == 0)
+ {
+ throw new ArgumentException(ExceptionPasswordEmpty, paramName);
+ }
+ }
+
+ private void CheckNicknameFormat(string nickname, string? paramName)
+ {
+ if (!_nicknameValidator.Validate(nickname, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName);
+ }
+ }
+
+ private static void ThrowUsernameConflict()
+ {
+ throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict);
+ }
+
+ private static User CreateUserFromEntity(UserEntity entity)
+ {
+ return new User
+ {
+ UniqueId = entity.UniqueId,
+ Username = entity.Username,
+ Administrator = UserRoleConvert.ToBool(entity.Roles),
+ Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
+ Id = entity.Id,
+ Version = entity.Version,
+ CreateTime = entity.CreateTime,
+ UsernameChangeTime = entity.UsernameChangeTime,
+ LastModified = entity.LastModified
+ };
+ }
+
+ public async Task<User> VerifyCredential(string username, string password)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ CheckUsernameFormat(username, nameof(username));
+ CheckPasswordFormat(password, nameof(password));
+
+ var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ if (!_passwordService.VerifyPassword(entity.Password, password))
+ throw new BadPasswordException(password);
+
+ return CreateUserFromEntity(entity);
+ }
+
+ public async Task<User> GetUserById(long id)
+ {
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+
+ if (user == null)
+ throw new UserNotExistException(id);
+
+ return CreateUserFromEntity(user);
+ }
+
+ public async Task<User> GetUserByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ CheckUsernameFormat(username, nameof(username));
+
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return CreateUserFromEntity(entity);
+ }
+
+ public async Task<long> GetUserIdByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ CheckUsernameFormat(username, nameof(username));
+
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
+ }
+
+ public async Task<User[]> GetUsers()
+ {
+ var entities = await _databaseContext.Users.ToArrayAsync();
+ return entities.Select(user => CreateUserFromEntity(user)).ToArray();
+ }
+
+ public async Task<User> CreateUser(User info)
+ {
+ if (info == null)
+ throw new ArgumentNullException(nameof(info));
+
+ if (info.Username == null)
+ throw new ArgumentException(ExceptionUsernameNull, nameof(info));
+ CheckUsernameFormat(info.Username, nameof(info));
+
+ if (info.Password == null)
+ throw new ArgumentException(ExceptionPasswordNull, nameof(info));
+ CheckPasswordFormat(info.Password, nameof(info));
+
+ if (info.Nickname != null)
+ CheckNicknameFormat(info.Nickname, nameof(info));
+
+ var username = info.Username;
+
+ var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+ if (conflict)
+ ThrowUsernameConflict();
+
+ var administrator = info.Administrator ?? false;
+ var password = info.Password;
+ var nickname = info.Nickname;
+
+ var newEntity = new UserEntity
+ {
+ Username = username,
+ Password = _passwordService.HashPassword(password),
+ Roles = UserRoleConvert.ToString(administrator),
+ Nickname = nickname,
+ Version = 1
+ };
+ _databaseContext.Users.Add(newEntity);
+ await _databaseContext.SaveChangesAsync();
+
+ _logger.LogInformation(Log.Format(LogDatabaseCreate,
+ ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator)));
+
+ return CreateUserFromEntity(newEntity);
+ }
+
+ private void ValidateModifyUserInfo(User? info)
+ {
+ if (info != null)
+ {
+ if (info.Username != null)
+ CheckUsernameFormat(info.Username, nameof(info));
+
+ if (info.Password != null)
+ CheckPasswordFormat(info.Password, nameof(info));
+
+ if (info.Nickname != null)
+ CheckNicknameFormat(info.Nickname, nameof(info));
+ }
+ }
+
+ private async Task UpdateUserEntity(UserEntity entity, User? info)
+ {
+ if (info != null)
+ {
+ var now = _clock.GetCurrentTime();
+ bool updateLastModified = false;
+
+ var username = info.Username;
+ if (username != null && username != entity.Username)
+ {
+ var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+ if (conflict)
+ ThrowUsernameConflict();
+
+ entity.Username = username;
+ entity.UsernameChangeTime = now;
+ updateLastModified = true;
+ }
+
+ var password = info.Password;
+ if (password != null)
+ {
+ entity.Password = _passwordService.HashPassword(password);
+ entity.Version += 1;
+ }
+
+ var administrator = info.Administrator;
+ if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator)
+ {
+ entity.Roles = UserRoleConvert.ToString(administrator.Value);
+ updateLastModified = true;
+ }
+
+ var nickname = info.Nickname;
+ if (nickname != null && nickname != entity.Nickname)
+ {
+ entity.Nickname = nickname;
+ updateLastModified = true;
+ }
+
+ if (updateLastModified)
+ {
+ entity.LastModified = now;
+ }
+ }
+ }
+
+
+ public async Task<User> ModifyUser(long id, User? info)
+ {
+ ValidateModifyUserInfo(info);
+
+ var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ if (entity == null)
+ throw new UserNotExistException(id);
+
+ await UpdateUserEntity(entity, info);
+
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(LogDatabaseUpdate, ("Id", id));
+
+ return CreateUserFromEntity(entity);
+ }
+
+ public async Task<User> ModifyUser(string username, User? info)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username, nameof(username));
+
+ ValidateModifyUserInfo(info);
+
+ var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ await UpdateUserEntity(entity, info);
+
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(LogDatabaseUpdate, ("Username", username));
+
+ return CreateUserFromEntity(entity);
+ }
+
+ public async Task ChangePassword(long id, string oldPassword, string newPassword)
+ {
+ if (oldPassword == null)
+ throw new ArgumentNullException(nameof(oldPassword));
+ if (newPassword == null)
+ throw new ArgumentNullException(nameof(newPassword));
+ CheckPasswordFormat(oldPassword, nameof(oldPassword));
+ CheckPasswordFormat(newPassword, nameof(newPassword));
+
+ var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(id);
+
+ if (!_passwordService.VerifyPassword(entity.Password, oldPassword))
+ throw new BadPasswordException(oldPassword);
+
+ entity.Password = _passwordService.HashPassword(newPassword);
+ entity.Version += 1;
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserTokenException.cs b/BackEnd/Timeline/Services/UserTokenException.cs new file mode 100644 index 00000000..d25fabb3 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenException.cs @@ -0,0 +1,68 @@ +using System;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class UserTokenException : Exception
+ {
+ public UserTokenException() { }
+ public UserTokenException(string message) : base(message) { }
+ public UserTokenException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenException(string token, string message) : base(message) { Token = token; }
+ public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; }
+ protected UserTokenException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Token { get; private set; } = "";
+ }
+
+
+ [Serializable]
+ public class UserTokenTimeExpireException : UserTokenException
+ {
+ public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { }
+ public UserTokenTimeExpireException(string message) : base(message) { }
+ public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ protected UserTokenTimeExpireException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public DateTime ExpireTime { get; private set; }
+
+ public DateTime VerifyTime { get; private set; }
+ }
+
+ [Serializable]
+ public class UserTokenBadVersionException : UserTokenException
+ {
+ public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { }
+ public UserTokenBadVersionException(string message) : base(message) { }
+ public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ protected UserTokenBadVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public long TokenVersion { get; set; }
+
+ public long RequiredVersion { get; set; }
+ }
+
+ [Serializable]
+ public class UserTokenBadFormatException : UserTokenException
+ {
+ public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token, string message) : base(token, message) { }
+ public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { }
+ public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { }
+ protected UserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs new file mode 100644 index 00000000..813dae67 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -0,0 +1,97 @@ +using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public class UserTokenCreateResult
+ {
+ public string Token { get; set; } = default!;
+ public User User { get; set; } = default!;
+ }
+
+ public interface IUserTokenManager
+ {
+ /// <summary>
+ /// Try to create a token for given username and password.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <param name="password">The password.</param>
+ /// <param name="expireAt">The expire time of the token.</param>
+ /// <returns>The created token and the user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with <paramref name="username"/> does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when <paramref name="password"/> is wrong.</exception>
+ public Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null);
+
+ /// <summary>
+ /// Verify a token and get the saved user info. This also check the database for existence of the user.
+ /// </summary>
+ /// <param name="token">The token.</param>
+ /// <returns>The user stored in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="UserTokenTimeExpireException">Thrown when the token is expired.</exception>
+ /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
+ public Task<User> VerifyToken(string token);
+ }
+
+ public class UserTokenManager : IUserTokenManager
+ {
+ private readonly ILogger<UserTokenManager> _logger;
+ private readonly IUserService _userService;
+ private readonly IUserTokenService _userTokenService;
+ private readonly IClock _clock;
+
+ public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
+ {
+ _logger = logger;
+ _userService = userService;
+ _userTokenService = userTokenService;
+ _clock = clock;
+ }
+
+ public async Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null)
+ {
+ expireAt = expireAt?.MyToUtc();
+
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ var user = await _userService.VerifyCredential(username, password);
+ var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt });
+
+ return new UserTokenCreateResult { Token = token, User = user };
+ }
+
+
+ public async Task<User> VerifyToken(string token)
+ {
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+
+ var tokenInfo = _userTokenService.VerifyToken(token);
+
+ if (tokenInfo.ExpireAt.HasValue)
+ {
+ var currentTime = _clock.GetCurrentTime();
+ if (tokenInfo.ExpireAt < currentTime)
+ throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime);
+ }
+
+ var user = await _userService.GetUserById(tokenInfo.Id);
+
+ if (tokenInfo.Version < user.Version)
+ throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value);
+
+ return user;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserTokenService.cs b/BackEnd/Timeline/Services/UserTokenService.cs new file mode 100644 index 00000000..86f3a0f7 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenService.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using Timeline.Configs;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ public class UserTokenInfo
+ {
+ public long Id { get; set; }
+ public long Version { get; set; }
+ public DateTime? ExpireAt { get; set; }
+ }
+
+ public interface IUserTokenService
+ {
+ /// <summary>
+ /// Create a token for a given token info.
+ /// </summary>
+ /// <param name="tokenInfo">The info to generate token.</param>
+ /// <returns>Return the generated token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
+ string GenerateToken(UserTokenInfo tokenInfo);
+
+ /// <summary>
+ /// Verify a token and get the saved info.
+ /// </summary>
+ /// <param name="token">The token to verify.</param>
+ /// <returns>The saved info in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <remarks>
+ /// If this method throw <see cref="UserTokenBadFormatException"/>, it usually means the token is not created by this service.
+ /// </remarks>
+ UserTokenInfo VerifyToken(string token);
+ }
+
+ public class JwtUserTokenService : IUserTokenService
+ {
+ private const string VersionClaimType = "timeline_version";
+
+ private readonly IOptionsMonitor<JwtConfiguration> _jwtConfig;
+ private readonly IClock _clock;
+
+ private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private SymmetricSecurityKey _tokenSecurityKey;
+
+ public JwtUserTokenService(IOptionsMonitor<JwtConfiguration> jwtConfig, IClock clock, DatabaseContext database)
+ {
+ _jwtConfig = jwtConfig;
+ _clock = clock;
+
+ var key = database.JwtToken.Select(t => t.Key).SingleOrDefault();
+
+ if (key == null)
+ {
+ throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist);
+ }
+
+ _tokenSecurityKey = new SymmetricSecurityKey(key);
+ }
+
+ public string GenerateToken(UserTokenInfo tokenInfo)
+ {
+ if (tokenInfo == null)
+ throw new ArgumentNullException(nameof(tokenInfo));
+
+ var config = _jwtConfig.CurrentValue;
+
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
+ identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
+
+ var tokenDescriptor = new SecurityTokenDescriptor()
+ {
+ Subject = identity,
+ Issuer = config.Issuer,
+ Audience = config.Audience,
+ SigningCredentials = new SigningCredentials(_tokenSecurityKey, SecurityAlgorithms.HmacSha384),
+ IssuedAt = _clock.GetCurrentTime(),
+ Expires = tokenInfo.ExpireAt.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
+ NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass.
+ };
+
+ var token = _tokenHandler.CreateToken(tokenDescriptor);
+ var tokenString = _tokenHandler.WriteToken(token);
+
+ return tokenString;
+ }
+
+
+ public UserTokenInfo VerifyToken(string token)
+ {
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+
+ var config = _jwtConfig.CurrentValue;
+ try
+ {
+ var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateIssuerSigningKey = true,
+ ValidateLifetime = false,
+ ValidIssuer = config.Issuer,
+ ValidAudience = config.Audience,
+ IssuerSigningKey = _tokenSecurityKey
+ }, out var t);
+
+ var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
+ if (idClaim == null)
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim);
+ if (!long.TryParse(idClaim, out var id))
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat);
+
+ var versionClaim = principal.FindFirstValue(VersionClaimType);
+ if (versionClaim == null)
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim);
+ if (!long.TryParse(versionClaim, out var version))
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat);
+
+ var decodedToken = (JwtSecurityToken)t;
+ var exp = decodedToken.Payload.Exp;
+ DateTime? expireAt = null;
+ if (exp.HasValue)
+ {
+ expireAt = EpochTime.DateTime(exp.Value);
+ }
+
+ return new UserTokenInfo
+ {
+ Id = id,
+ Version = version,
+ ExpireAt = expireAt
+ };
+ }
+ catch (Exception e) when (e is SecurityTokenException || e is ArgumentException)
+ {
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs new file mode 100644 index 00000000..576836eb --- /dev/null +++ b/BackEnd/Timeline/Startup.cs @@ -0,0 +1,185 @@ +using AutoMapper;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using NSwag;
+using NSwag.Generation.Processors.Security;
+using System;
+using System.ComponentModel;
+using System.Net.Mime;
+using System.Text.Json.Serialization;
+using Timeline.Auth;
+using Timeline.Configs;
+using Timeline.Entities;
+using Timeline.Formatters;
+using Timeline.Helpers;
+using Timeline.Models.Converters;
+using Timeline.Routes;
+using Timeline.Services;
+using Timeline.Swagger;
+
+namespace Timeline
+{
+ public class Startup
+ {
+ private readonly bool disableFrontEnd;
+ private readonly bool useMockFrontEnd;
+
+ public Startup(IConfiguration configuration, IWebHostEnvironment environment)
+ {
+ Environment = environment;
+ Configuration = configuration;
+
+ disableFrontEnd = Configuration.GetValue<bool?>(ApplicationConfiguration.DisableFrontEndKey) ?? false;
+ useMockFrontEnd = Configuration.GetValue<bool?>(ApplicationConfiguration.UseMockFrontEndKey) ?? false;
+ }
+
+ public IWebHostEnvironment Environment { get; }
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(MyDateTimeConverter)));
+
+ services.AddControllers(setup =>
+ {
+ setup.InputFormatters.Add(new StringInputFormatter());
+ setup.InputFormatters.Add(new BytesInputFormatter());
+ setup.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json, "text/json"));
+ setup.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json, "text/json"));
+ setup.UseApiRoutePrefix("api");
+ })
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.IgnoreNullValues = true;
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
+ })
+ .ConfigureApiBehaviorOptions(options =>
+ {
+ options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory;
+ });
+
+ services.Configure<JwtConfiguration>(Configuration.GetSection("Jwt"));
+ services.AddAuthentication(AuthenticationConstants.Scheme)
+ .AddScheme<MyAuthenticationOptions, MyAuthenticationHandler>(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { });
+ services.AddAuthorization();
+
+ services.AddSingleton<IPathProvider, PathProvider>();
+
+ services.AddSingleton<IDatabaseBackupService, DatabaseBackupService>();
+
+ services.AddAutoMapper(GetType().Assembly);
+
+ services.AddTransient<IClock, Clock>();
+
+ services.AddTransient<IPasswordService, PasswordService>();
+ services.AddScoped<IUserService, UserService>();
+ services.AddScoped<IUserDeleteService, UserDeleteService>();
+ services.AddScoped<IUserTokenService, JwtUserTokenService>();
+ services.AddScoped<IUserTokenManager, UserTokenManager>();
+
+ services.AddScoped<IETagGenerator, ETagGenerator>();
+ services.AddScoped<IDataManager, DataManager>();
+
+ services.AddScoped<IImageValidator, ImageValidator>();
+
+ services.AddUserAvatarService();
+
+ services.AddScoped<ITimelineService, TimelineService>();
+
+ services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
+
+ services.AddDbContext<DatabaseContext>((services, options) =>
+ {
+ var pathProvider = services.GetRequiredService<IPathProvider>();
+ options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}");
+ });
+
+ services.AddSwaggerDocument(document =>
+ {
+ document.DocumentName = "Timeline";
+ document.Title = "Timeline REST API Reference";
+ document.Version = typeof(Startup).Assembly.GetName().Version?.ToString() ?? "unknown version";
+ document.DocumentProcessors.Add(new DocumentDescriptionDocumentProcessor());
+ document.DocumentProcessors.Add(
+ new SecurityDefinitionAppender("JWT",
+ new OpenApiSecurityScheme
+ {
+ Type = OpenApiSecuritySchemeType.ApiKey,
+ Name = "Authorization",
+ In = OpenApiSecurityApiKeyLocation.Header,
+ Description = "Create token via `/api/token/create` ."
+ }));
+ document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
+ document.OperationProcessors.Add(new DefaultDescriptionOperationProcessor());
+ document.OperationProcessors.Add(new ByteDataRequestOperationProcessor());
+ });
+
+ if (!disableFrontEnd)
+ {
+ if (useMockFrontEnd)
+ {
+ services.AddSpaStaticFiles(config =>
+ {
+ config.RootPath = "MockClientApp";
+ });
+
+ }
+ else if (!Environment.IsDevelopment()) // In development, we don't want to serve dist. Or it will take precedence than front end dev server.
+ {
+ services.AddSpaStaticFiles(config =>
+ {
+ config.RootPath = "ClientApp";
+ });
+ }
+ }
+ }
+
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseRouting();
+
+ if (!disableFrontEnd && (useMockFrontEnd || !Environment.IsDevelopment()))
+ {
+ app.UseSpaStaticFiles(new StaticFileOptions
+ {
+ ServeUnknownFileTypes = true
+ });
+ }
+
+ app.UseOpenApi();
+ app.UseReDoc();
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+
+ UnknownEndpointMiddleware.Attach(app);
+
+ if (!disableFrontEnd)
+ {
+ app.UseSpa(spa =>
+ {
+ if (!useMockFrontEnd && (Configuration.GetValue<bool?>(ApplicationConfiguration.UseProxyFrontEndKey) ?? false))
+ {
+ spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri);
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Swagger/ApiConvention.cs b/BackEnd/Timeline/Swagger/ApiConvention.cs new file mode 100644 index 00000000..dbf0b2fe --- /dev/null +++ b/BackEnd/Timeline/Swagger/ApiConvention.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc;
+
+[assembly: ApiConventionType(typeof(Timeline.Controllers.ApiConvention))]
+
+namespace Timeline.Controllers
+{
+ // There is some bug if nullable is enable. So disable it.
+#nullable disable
+ /// <summary>
+ /// My api convention.
+ /// </summary>
+ public static class ApiConvention
+ {
+ }
+}
diff --git a/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs b/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs new file mode 100644 index 00000000..887831ac --- /dev/null +++ b/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs @@ -0,0 +1,27 @@ +using NJsonSchema;
+using NSwag;
+using NSwag.Generation.Processors;
+using NSwag.Generation.Processors.Contexts;
+using System.Linq;
+using Timeline.Models;
+
+namespace Timeline.Swagger
+{
+ /// <summary>
+ /// Coerce ByteData body type into the right one.
+ /// </summary>
+ public class ByteDataRequestOperationProcessor : IOperationProcessor
+ {
+ /// <inheritdoc/>
+ public bool Process(OperationProcessorContext context)
+ {
+ var hasByteDataBody = context.MethodInfo.GetParameters().Where(p => p.ParameterType == typeof(ByteData)).Any();
+ if (hasByteDataBody)
+ {
+ var bodyParameter = context.OperationDescription.Operation.Parameters.Where(p => p.Kind == OpenApiParameterKind.Body).Single();
+ bodyParameter.Schema = JsonSchema.FromType<byte[]>();
+ }
+ return true;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs b/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs new file mode 100644 index 00000000..4967cc6a --- /dev/null +++ b/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs @@ -0,0 +1,39 @@ +using NSwag.Generation.Processors;
+using NSwag.Generation.Processors.Contexts;
+using System.Collections.Generic;
+
+namespace Timeline.Swagger
+{
+ /// <summary>
+ /// Swagger operation processor that adds default description to response.
+ /// </summary>
+ public class DefaultDescriptionOperationProcessor : IOperationProcessor
+ {
+ private readonly Dictionary<string, string> defaultDescriptionMap = new Dictionary<string, string>
+ {
+ ["200"] = "Succeeded to perform the operation.",
+ ["304"] = "Item does not change.",
+ ["400"] = "See code and message for error info.",
+ ["401"] = "You need to log in to perform this operation.",
+ ["403"] = "You have no permission to perform the operation.",
+ ["404"] = "Item does not exist. See code and message for error info."
+ };
+
+ /// <inheritdoc/>
+ public bool Process(OperationProcessorContext context)
+ {
+ var responses = context.OperationDescription.Operation.Responses;
+
+ foreach (var (httpStatusCode, res) in responses)
+ {
+ if (!string.IsNullOrEmpty(res.Description)) continue;
+ if (defaultDescriptionMap.ContainsKey(httpStatusCode))
+ {
+ res.Description = defaultDescriptionMap[httpStatusCode];
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs b/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs new file mode 100644 index 00000000..dc5ddd96 --- /dev/null +++ b/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs @@ -0,0 +1,55 @@ +using NSwag.Generation.Processors;
+using NSwag.Generation.Processors.Contexts;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+
+namespace Timeline.Swagger
+{
+ public class DocumentDescriptionDocumentProcessor : IDocumentProcessor
+ {
+ private static Dictionary<string, int> GetAllErrorCodes()
+ {
+ var errorCodes = new Dictionary<string, int>();
+
+ void RecursiveCheckErrorCode(Type type)
+ {
+ foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
+ .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int)))
+ {
+ var name = (type.FullName + "." + field.Name).Remove(0, typeof(ErrorCodes).FullName!.Length + 1).Replace("+", ".", StringComparison.OrdinalIgnoreCase);
+ int value = (int)field.GetRawConstantValue()!;
+ errorCodes.Add(name, value);
+ }
+
+ foreach (var nestedType in type.GetNestedTypes())
+ {
+ RecursiveCheckErrorCode(nestedType);
+ }
+ }
+
+ RecursiveCheckErrorCode(typeof(ErrorCodes));
+
+ return errorCodes;
+ }
+
+ public void Process(DocumentProcessorContext context)
+ {
+ StringBuilder description = new StringBuilder();
+ description.AppendLine("# Error Codes");
+ description.AppendLine("name | value");
+ description.AppendLine("---- | -----");
+ foreach (var (name, value) in GetAllErrorCodes())
+ {
+ description.AppendLine($"`{name}` | `{value}`");
+ }
+
+ context.Document.Info.Description = description.ToString();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj new file mode 100644 index 00000000..5131cdb0 --- /dev/null +++ b/BackEnd/Timeline/Timeline.csproj @@ -0,0 +1,263 @@ +<Project Sdk="Microsoft.NET.Sdk.Web">
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId>
+ <Authors>crupest</Authors>
+
+ <IsPackable>false</IsPackable>
+
+ <LangVersion>8.0</LangVersion>
+ <Nullable>enable</Nullable>
+
+ <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
+ <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
+ <SpaRoot>ClientApp\</SpaRoot>
+
+ <Version>0.3.0</Version>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
+
+ <NoWarn>1701;1702;1591</NoWarn>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Content Include="default-avatar.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="MockClientApp\index.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="$(SpaRoot)**" >
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoMapper" Version="10.1.1" />
+ <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
+ <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="3.1.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="NSwag.AspNetCore" Version="13.8.2" />
+ <PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
+ <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="Resources\Authentication\AuthHandler.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>AuthHandler.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\ControllerAuthExtensions.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>ControllerAuthExtensions.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\TimelineController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TimelineController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\TokenController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TokenController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\UserAvatarController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\UserController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Entities.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Entities.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Filters.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Filters.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Helper\DataCacheHelper.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>DataCacheHelper.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Messages.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Messages.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Http\Common.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Common.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Http\Exception.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Exception.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\NicknameValidator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>NicknameValidator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\NameValidator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>NameValidator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\Validator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Validator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\DataManager.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>DataManager.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\Exception.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Exception.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\Exceptions.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Exceptions.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\TimelineService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TimelineService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserTokenService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserTokenService.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Update="Resources\Authentication\AuthHandler.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>AuthHandler.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\ControllerAuthExtensions.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>ControllerAuthExtensions.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TimelineController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TimelineController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TokenController.resx">
+ <SubType>Designer</SubType>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TokenController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserAvatarController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Entities.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Entities.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Filters.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Filters.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Helper\DataCacheHelper.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>DataCacheHelper.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Messages.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Messages.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Http\Common.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Common.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Http\Exception.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Exception.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\NicknameValidator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>NicknameValidator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\NameValidator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>NameValidator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\Validator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Validator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\DataManager.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>DataManager.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\Exception.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Exception.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\Exceptions.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Exceptions.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\TimelineService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TimelineService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserTokenService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserTokenService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+</Project>
\ No newline at end of file diff --git a/BackEnd/Timeline/appsettings.Development.json b/BackEnd/Timeline/appsettings.Development.json new file mode 100644 index 00000000..a2880cbf --- /dev/null +++ b/BackEnd/Timeline/appsettings.Development.json @@ -0,0 +1,9 @@ +{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/BackEnd/Timeline/appsettings.json b/BackEnd/Timeline/appsettings.json new file mode 100644 index 00000000..804ca43a --- /dev/null +++ b/BackEnd/Timeline/appsettings.json @@ -0,0 +1,11 @@ +{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "Jwt": {
+ "Issuer": "api.crupest.xyz",
+ "Audience": "api.crupest.xyz"
+ }
+}
diff --git a/BackEnd/Timeline/default-avatar.png b/BackEnd/Timeline/default-avatar.png Binary files differnew file mode 100644 index 00000000..4086e1d2 --- /dev/null +++ b/BackEnd/Timeline/default-avatar.png diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json new file mode 100644 index 00000000..ed92c672 --- /dev/null +++ b/BackEnd/Timeline/packages.lock.json @@ -0,0 +1,1563 @@ +{
+ "version": 1,
+ "dependencies": {
+ ".NETCoreApp,Version=v3.1": {
+ "AutoMapper": {
+ "type": "Direct",
+ "requested": "[10.1.1, )",
+ "resolved": "10.1.1",
+ "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.7.0",
+ "System.Reflection.Emit": "4.7.0"
+ }
+ },
+ "AutoMapper.Extensions.Microsoft.DependencyInjection": {
+ "type": "Direct",
+ "requested": "[8.1.0, )",
+ "resolved": "8.1.0",
+ "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==",
+ "dependencies": {
+ "AutoMapper": "[10.1.0, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
+ "Microsoft.Extensions.Options": "3.0.0"
+ }
+ },
+ "Microsoft.AspNetCore.SpaServices.Extensions": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.SpaServices": "3.1.9",
+ "Microsoft.Extensions.FileProviders.Physical": "3.1.9"
+ }
+ },
+ "Microsoft.CodeAnalysis.FxCopAnalyzers": {
+ "type": "Direct",
+ "requested": "[3.3.0, )",
+ "resolved": "3.3.0",
+ "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]",
+ "Microsoft.CodeQuality.Analyzers": "[3.3.0]",
+ "Microsoft.NetCore.Analyzers": "[3.3.0]",
+ "Microsoft.NetFramework.Analyzers": "[3.3.0]"
+ }
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "1.1.1",
+ "Microsoft.Bcl.HashCode": "1.1.0",
+ "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9",
+ "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9",
+ "Microsoft.Extensions.Caching.Memory": "3.1.9",
+ "Microsoft.Extensions.DependencyInjection": "3.1.9",
+ "Microsoft.Extensions.Logging": "3.1.9",
+ "System.Collections.Immutable": "1.7.1",
+ "System.ComponentModel.Annotations": "4.7.0",
+ "System.Diagnostics.DiagnosticSource": "4.7.1"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ=="
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9",
+ "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Tools": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "mSgwjp0h5iqW5V49SVijR5O+kNpI1nitcbN12n9FYx/Ga6oCEFwXR/llBDesD6ASHw3Mx16vodJYJ7CEBx5rig==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Design": "3.1.9"
+ }
+ },
+ "NSwag.AspNetCore": {
+ "type": "Direct",
+ "requested": "[13.8.2, )",
+ "resolved": "13.8.2",
+ "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Mvc.Core": "1.0.4",
+ "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
+ "Microsoft.AspNetCore.StaticFiles": "1.0.4",
+ "Microsoft.Extensions.ApiDescription.Server": "3.0.0",
+ "Microsoft.Extensions.FileProviders.Embedded": "1.0.1",
+ "NSwag.Annotations": "13.8.2",
+ "NSwag.Core": "13.8.2",
+ "NSwag.Generation": "13.8.2",
+ "NSwag.Generation.AspNetCore": "13.8.2",
+ "System.IO.FileSystem": "4.3.0",
+ "System.Xml.XPath.XDocument": "4.0.1"
+ }
+ },
+ "SixLabors.ImageSharp": {
+ "type": "Direct",
+ "requested": "[1.0.1, )",
+ "resolved": "1.0.1",
+ "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ=="
+ },
+ "System.IdentityModel.Tokens.Jwt": {
+ "type": "Direct",
+ "requested": "[6.8.0, )",
+ "resolved": "6.8.0",
+ "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==",
+ "dependencies": {
+ "Microsoft.IdentityModel.JsonWebTokens": "6.8.0",
+ "Microsoft.IdentityModel.Tokens": "6.8.0"
+ }
+ },
+ "Microsoft.AspNetCore.Authorization": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "1.0.2",
+ "Microsoft.Extensions.Options": "1.0.2",
+ "System.Security.Claims": "4.0.1"
+ }
+ },
+ "Microsoft.AspNetCore.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4",
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.3",
+ "Microsoft.Extensions.Configuration.Abstractions": "1.0.2",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2",
+ "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "1.0.2"
+ }
+ },
+ "Microsoft.AspNetCore.Hosting.Server.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Features": "1.0.3",
+ "Microsoft.Extensions.Configuration.Abstractions": "1.0.2"
+ }
+ },
+ "Microsoft.AspNetCore.Http": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.3",
+ "Microsoft.AspNetCore.WebUtilities": "1.0.3",
+ "Microsoft.Extensions.ObjectPool": "1.0.1",
+ "Microsoft.Extensions.Options": "1.0.2",
+ "Microsoft.Net.Http.Headers": "1.0.3",
+ "System.Buffers": "4.0.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Features": "1.0.3",
+ "System.Globalization.Extensions": "4.0.1",
+ "System.Linq.Expressions": "4.1.1",
+ "System.Reflection.TypeExtensions": "4.1.0",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Text.Encodings.Web": "4.0.1"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Extensions": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.3",
+ "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1",
+ "Microsoft.Net.Http.Headers": "1.0.3",
+ "System.Buffers": "4.0.0",
+ "System.IO.FileSystem": "4.0.1"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Features": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "1.0.1",
+ "System.Collections": "4.0.11",
+ "System.ComponentModel": "4.0.1",
+ "System.Linq": "4.1.0",
+ "System.Net.Primitives": "4.0.11",
+ "System.Net.WebSockets": "4.0.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Security.Claims": "4.0.1",
+ "System.Security.Cryptography.X509Certificates": "4.1.0",
+ "System.Security.Principal": "4.0.1"
+ }
+ },
+ "Microsoft.AspNetCore.JsonPatch": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.0.1",
+ "Newtonsoft.Json": "9.0.1",
+ "System.Collections.Concurrent": "4.0.12",
+ "System.ComponentModel.TypeConverter": "4.1.0",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.Linq": "4.1.0",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Serialization.Primitives": "4.1.1",
+ "System.Text.Encoding.Extensions": "4.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.Mvc.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4",
+ "Microsoft.CSharp": "4.0.1",
+ "Microsoft.Net.Http.Headers": "1.0.3",
+ "System.ComponentModel.TypeConverter": "4.1.0",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Text.Encoding.Extensions": "4.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.Mvc.ApiExplorer": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Mvc.Core": "1.0.4"
+ }
+ },
+ "Microsoft.AspNetCore.Mvc.Core": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Authorization": "1.0.3",
+ "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3",
+ "Microsoft.AspNetCore.Http": "1.0.3",
+ "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4",
+ "Microsoft.AspNetCore.Routing": "1.0.4",
+ "Microsoft.Extensions.DependencyModel": "1.0.0",
+ "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "1.0.2",
+ "Microsoft.Extensions.PlatformAbstractions": "1.0.0",
+ "System.Buffers": "4.0.0",
+ "System.Diagnostics.DiagnosticSource": "4.0.0",
+ "System.Text.Encoding": "4.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.Mvc.Formatters.Json": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.JsonPatch": "1.0.0",
+ "Microsoft.AspNetCore.Mvc.Core": "1.0.4"
+ }
+ },
+ "Microsoft.AspNetCore.NodeServices": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Console": "3.1.9",
+ "Newtonsoft.Json": "12.0.2"
+ }
+ },
+ "Microsoft.AspNetCore.Routing": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Extensions": "1.0.3",
+ "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "1.0.2",
+ "Microsoft.Extensions.ObjectPool": "1.0.1",
+ "Microsoft.Extensions.Options": "1.0.2",
+ "System.Collections": "4.0.11",
+ "System.Text.RegularExpressions": "4.1.0"
+ }
+ },
+ "Microsoft.AspNetCore.Routing.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.3",
+ "System.Collections.Concurrent": "4.0.12",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "Microsoft.AspNetCore.SpaServices": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.NodeServices": "3.1.9"
+ }
+ },
+ "Microsoft.AspNetCore.StaticFiles": {
+ "type": "Transitive",
+ "resolved": "1.0.4",
+ "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4",
+ "Microsoft.AspNetCore.Http.Extensions": "1.0.3",
+ "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "1.0.2",
+ "Microsoft.Extensions.WebEncoders": "1.0.3"
+ }
+ },
+ "Microsoft.AspNetCore.WebUtilities": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "1.0.1",
+ "System.Buffers": "4.0.0",
+ "System.Collections": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.IO.FileSystem": "4.0.1",
+ "System.Text.Encodings.Web": "4.0.1"
+ }
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "1.1.1",
+ "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w=="
+ },
+ "Microsoft.Bcl.HashCode": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg=="
+ },
+ "Microsoft.CodeAnalysis.VersionCheckAnalyzer": {
+ "type": "Transitive",
+ "resolved": "3.3.0",
+ "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg=="
+ },
+ "Microsoft.CodeQuality.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.3.0",
+ "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw=="
+ },
+ "Microsoft.CSharp": {
+ "type": "Transitive",
+ "resolved": "4.7.0",
+ "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
+ },
+ "Microsoft.Data.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.0.2"
+ }
+ },
+ "Microsoft.DotNet.PlatformAbstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.6",
+ "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw=="
+ },
+ "Microsoft.EntityFrameworkCore.Design": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "2zgP7BWcw5nqGQiT4bEtiI6ras+4pvKg5D+tA3AYvjEifzzaWvmRTb3B9nRHpIYJAhPtmWNBVnVXLbu3fS1OYA==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.7.0",
+ "Microsoft.EntityFrameworkCore.Relational": "3.1.9"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "3.1.9"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==",
+ "dependencies": {
+ "Microsoft.Data.Sqlite.Core": "3.1.9",
+ "Microsoft.DotNet.PlatformAbstractions": "3.1.6",
+ "Microsoft.EntityFrameworkCore.Relational": "3.1.9",
+ "Microsoft.Extensions.DependencyModel": "3.1.6"
+ }
+ },
+ "Microsoft.Extensions.ApiDescription.Server": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w=="
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "3.1.9",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Logging.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Options": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "3.1.6",
+ "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==",
+ "dependencies": {
+ "System.Text.Json": "4.7.2"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Embedded": {
+ "type": "Transitive",
+ "resolved": "1.0.1",
+ "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1",
+ "System.Runtime.Extensions": "4.1.0"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Physical": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9",
+ "Microsoft.Extensions.FileSystemGlobbing": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.FileSystemGlobbing": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "3.1.9",
+ "Microsoft.Extensions.DependencyInjection": "3.1.9",
+ "Microsoft.Extensions.Logging.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Options": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg=="
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging": "3.1.9",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Logging.Console": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Logging": "3.1.9",
+ "Microsoft.Extensions.Logging.Configuration": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "1.0.1",
+ "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Primitives": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Configuration.Binder": "3.1.9",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9",
+ "Microsoft.Extensions.Options": "3.1.9"
+ }
+ },
+ "Microsoft.Extensions.PlatformAbstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==",
+ "dependencies": {
+ "System.AppContext": "4.1.0",
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Reflection.TypeExtensions": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime.Extensions": "4.1.0"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA=="
+ },
+ "Microsoft.Extensions.WebEncoders": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2",
+ "Microsoft.Extensions.Options": "1.0.2",
+ "System.Text.Encodings.Web": "4.0.1"
+ }
+ },
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Transitive",
+ "resolved": "6.8.0",
+ "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Tokens": "6.8.0"
+ }
+ },
+ "Microsoft.IdentityModel.Logging": {
+ "type": "Transitive",
+ "resolved": "6.8.0",
+ "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg=="
+ },
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Transitive",
+ "resolved": "6.8.0",
+ "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.5.0",
+ "Microsoft.IdentityModel.Logging": "6.8.0",
+ "System.Security.Cryptography.Cng": "4.5.0"
+ }
+ },
+ "Microsoft.Net.Http.Headers": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==",
+ "dependencies": {
+ "System.Buffers": "4.0.0",
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Contracts": "4.0.1",
+ "System.Globalization": "4.0.11",
+ "System.Linq": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Text.Encoding": "4.0.11"
+ }
+ },
+ "Microsoft.NetCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.3.0",
+ "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA=="
+ },
+ "Microsoft.NETCore.Platforms": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
+ },
+ "Microsoft.NETCore.Targets": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
+ },
+ "Microsoft.NetFramework.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.3.0",
+ "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg=="
+ },
+ "Microsoft.Win32.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "fQnBHO9DgcmkC9dYSJoBqo6sH1VJwJprUHh8F3hbcRlxiQiBUuTntdk8tUwV490OqC2kQUrinGwZyQHTieuXRA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "Namotion.Reflection": {
+ "type": "Transitive",
+ "resolved": "1.0.14",
+ "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.3.0"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "12.0.2",
+ "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA=="
+ },
+ "NJsonSchema": {
+ "type": "Transitive",
+ "resolved": "10.2.1",
+ "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==",
+ "dependencies": {
+ "Namotion.Reflection": "1.0.14",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "NSwag.Annotations": {
+ "type": "Transitive",
+ "resolved": "13.8.2",
+ "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q=="
+ },
+ "NSwag.Core": {
+ "type": "Transitive",
+ "resolved": "13.8.2",
+ "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==",
+ "dependencies": {
+ "NJsonSchema": "10.2.1",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "NSwag.Generation": {
+ "type": "Transitive",
+ "resolved": "13.8.2",
+ "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==",
+ "dependencies": {
+ "NJsonSchema": "10.2.1",
+ "NSwag.Core": "13.8.2",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "NSwag.Generation.AspNetCore": {
+ "type": "Transitive",
+ "resolved": "13.8.2",
+ "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4",
+ "Microsoft.AspNetCore.Mvc.Core": "1.0.4",
+ "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
+ "NJsonSchema": "10.2.1",
+ "NSwag.Generation": "13.8.2"
+ }
+ },
+ "runtime.native.System": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1"
+ }
+ },
+ "runtime.native.System.Net.Http": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1"
+ }
+ },
+ "runtime.native.System.Security.Cryptography": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1"
+ }
+ },
+ "SQLitePCLRaw.bundle_e_sqlite3": {
+ "type": "Transitive",
+ "resolved": "2.0.2",
+ "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.0.2",
+ "SQLitePCLRaw.lib.e_sqlite3": "2.0.2",
+ "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2"
+ }
+ },
+ "SQLitePCLRaw.core": {
+ "type": "Transitive",
+ "resolved": "2.0.2",
+ "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==",
+ "dependencies": {
+ "System.Memory": "4.5.3"
+ }
+ },
+ "SQLitePCLRaw.lib.e_sqlite3": {
+ "type": "Transitive",
+ "resolved": "2.0.2",
+ "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw=="
+ },
+ "SQLitePCLRaw.provider.dynamic_cdecl": {
+ "type": "Transitive",
+ "resolved": "2.0.2",
+ "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==",
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.0.2"
+ }
+ },
+ "System.AppContext": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "3QjO4jNV7PdKkmQAVp9atA+usVnKRwI3Kx1nMwJ93T0LcQfx7pKAYk0nKz5wn1oP5iqlhZuy6RXOFdhr7rDwow==",
+ "dependencies": {
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Buffers": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "msXumHfjjURSkvxUjYuq4N2ghHoRi2VpXcKMA7gK6ujQfU3vGpl+B6ld0ATRg+FZFpRyA6PgEPA+VlIkTeNf2w==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Diagnostics.Tracing": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Collections": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Collections.Concurrent": {
+ "type": "Transitive",
+ "resolved": "4.0.12",
+ "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Diagnostics.Tracing": "4.1.0",
+ "System.Globalization": "4.0.11",
+ "System.Reflection": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "System.Collections.Immutable": {
+ "type": "Transitive",
+ "resolved": "1.7.1",
+ "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q=="
+ },
+ "System.Collections.NonGeneric": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "hMxFT2RhhlffyCdKLDXjx8WEC5JfCvNozAZxCablAuFRH74SCV4AgzE8yJCh/73bFnEoZgJ9MJmkjQ0dJmnKqA==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Collections.Specialized": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "/HKQyVP0yH1I0YtK7KJL/28snxHNH/bi+0lgk/+MbURF6ULhAE31MDI+NZDerNWu264YbxklXCCygISgm+HMug==",
+ "dependencies": {
+ "System.Collections.NonGeneric": "4.0.1",
+ "System.Globalization": "4.0.11",
+ "System.Globalization.Extensions": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.ComponentModel": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "oBZFnm7seFiVfugsIyOvQCWobNZs7FzqDV/B7tx20Ep/l3UUFCPDkdTnCNaJZTU27zjeODmy2C/cP60u3D4c9w==",
+ "dependencies": {
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.ComponentModel.Annotations": {
+ "type": "Transitive",
+ "resolved": "4.7.0",
+ "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ=="
+ },
+ "System.ComponentModel.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "sc/7eVCdxPrp3ljpgTKVaQGUXiW05phNWvtv/m2kocXqrUQvTVWKou1Edas2aDjTThLPZOxPYIGNb/HN0QjURg==",
+ "dependencies": {
+ "System.ComponentModel": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.ComponentModel.TypeConverter": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "MnDAlaeJZy9pdB5ZdOlwdxfpI+LJQ6e0hmH7d2+y2LkiD8DRJynyDYl4Xxf3fWFm7SbEwBZh4elcfzONQLOoQw==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Collections.NonGeneric": "4.0.1",
+ "System.Collections.Specialized": "4.0.1",
+ "System.ComponentModel": "4.0.1",
+ "System.ComponentModel.Primitives": "4.1.0",
+ "System.Globalization": "4.0.11",
+ "System.Linq": "4.1.0",
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Reflection.TypeExtensions": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Diagnostics.Contracts": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==",
+ "dependencies": {
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Diagnostics.Debug": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "4.7.1",
+ "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
+ },
+ "System.Diagnostics.Tools": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "xBfJ8pnd4C17dWaC9FM6aShzbJcRNMChUMD42I6772KGGrqaFdumwhn9OdM68erj1ueNo3xdQ1EwiFjK5k8p0g==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Diagnostics.Tracing": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Globalization": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Globalization.Calendars": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Globalization": "4.0.11",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Globalization.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "KKo23iKeOaIg61SSXwjANN7QYDr/3op3OWGGzDzz7mypx0Za0fZSeG0l6cco8Ntp8YMYkIQcAqlk8yhm5/Uhcg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.Globalization": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.InteropServices": "4.1.0"
+ }
+ },
+ "System.IO": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "System.IO.FileSystem": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.IO": "4.3.0",
+ "System.IO.FileSystem.Primitives": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "System.IO.FileSystem.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==",
+ "dependencies": {
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Linq": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0"
+ }
+ },
+ "System.Linq.Expressions": {
+ "type": "Transitive",
+ "resolved": "4.1.1",
+ "contentHash": "bXwi8FrK/XIGPvtk1ZnawffhqLPyacj7dZnbFaV52YGaQigNqGEzNAByAIvL9FlEe3TCzoInorHF91IK//Q3Xg==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Linq": "4.1.0",
+ "System.ObjectModel": "4.0.12",
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Emit": "4.0.1",
+ "System.Reflection.Emit.ILGeneration": "4.0.1",
+ "System.Reflection.Emit.Lightweight": "4.0.1",
+ "System.Reflection.Extensions": "4.0.1",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Reflection.TypeExtensions": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Memory": {
+ "type": "Transitive",
+ "resolved": "4.5.3",
+ "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
+ },
+ "System.Net.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "hVvfl4405DRjA2408luZekbPhplJK03j2Y2lSfMlny7GHXlkByw1iLnc9mgKW0GdQn73vvMcWrWewAhylXA4Nw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Handles": "4.0.1"
+ }
+ },
+ "System.Net.WebSockets": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==",
+ "dependencies": {
+ "Microsoft.Win32.Primitives": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "System.ObjectModel": {
+ "type": "Transitive",
+ "resolved": "4.0.12",
+ "contentHash": "tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Reflection": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.IO": "4.1.0",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Reflection.Emit": {
+ "type": "Transitive",
+ "resolved": "4.7.0",
+ "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ=="
+ },
+ "System.Reflection.Emit.ILGeneration": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==",
+ "dependencies": {
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Reflection.Emit.Lightweight": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==",
+ "dependencies": {
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Emit.ILGeneration": "4.0.1",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Reflection.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Reflection": "4.1.0",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Reflection.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Reflection.TypeExtensions": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==",
+ "dependencies": {
+ "System.Reflection": "4.1.0",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Resources.ResourceManager": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Globalization": "4.0.11",
+ "System.Reflection": "4.1.0",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Runtime": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0"
+ }
+ },
+ "System.Runtime.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Runtime.Handles": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Runtime.InteropServices": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Reflection": "4.1.0",
+ "System.Reflection.Primitives": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Handles": "4.0.1"
+ }
+ },
+ "System.Runtime.Numerics": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==",
+ "dependencies": {
+ "System.Globalization": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0"
+ }
+ },
+ "System.Runtime.Serialization.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.1.1",
+ "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==",
+ "dependencies": {
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Security.Claims": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Security.Principal": "4.0.1"
+ }
+ },
+ "System.Security.Cryptography.Algorithms": {
+ "type": "Transitive",
+ "resolved": "4.2.0",
+ "contentHash": "8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.Collections": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Handles": "4.0.1",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Runtime.Numerics": "4.0.1",
+ "System.Security.Cryptography.Encoding": "4.0.0",
+ "System.Security.Cryptography.Primitives": "4.0.0",
+ "System.Text.Encoding": "4.0.11",
+ "runtime.native.System.Security.Cryptography": "4.0.0"
+ }
+ },
+ "System.Security.Cryptography.Cng": {
+ "type": "Transitive",
+ "resolved": "4.5.0",
+ "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A=="
+ },
+ "System.Security.Cryptography.Csp": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.IO": "4.1.0",
+ "System.Reflection": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Handles": "4.0.1",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Security.Cryptography.Algorithms": "4.2.0",
+ "System.Security.Cryptography.Encoding": "4.0.0",
+ "System.Security.Cryptography.Primitives": "4.0.0",
+ "System.Text.Encoding": "4.0.11",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Security.Cryptography.Encoding": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.Collections": "4.0.11",
+ "System.Collections.Concurrent": "4.0.12",
+ "System.Linq": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Handles": "4.0.1",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Security.Cryptography.Primitives": "4.0.0",
+ "System.Text.Encoding": "4.0.11",
+ "runtime.native.System.Security.Cryptography": "4.0.0"
+ }
+ },
+ "System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Handles": "4.0.1",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Runtime.Numerics": "4.0.1",
+ "System.Security.Cryptography.Algorithms": "4.2.0",
+ "System.Security.Cryptography.Encoding": "4.0.0",
+ "System.Security.Cryptography.Primitives": "4.0.0",
+ "System.Text.Encoding": "4.0.11",
+ "runtime.native.System.Security.Cryptography": "4.0.0"
+ }
+ },
+ "System.Security.Cryptography.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Threading": "4.0.11",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "System.Security.Cryptography.X509Certificates": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.Globalization.Calendars": "4.0.1",
+ "System.IO": "4.1.0",
+ "System.IO.FileSystem": "4.0.1",
+ "System.IO.FileSystem.Primitives": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.Handles": "4.0.1",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Runtime.Numerics": "4.0.1",
+ "System.Security.Cryptography.Algorithms": "4.2.0",
+ "System.Security.Cryptography.Cng": "4.2.0",
+ "System.Security.Cryptography.Csp": "4.0.0",
+ "System.Security.Cryptography.Encoding": "4.0.0",
+ "System.Security.Cryptography.OpenSsl": "4.0.0",
+ "System.Security.Cryptography.Primitives": "4.0.0",
+ "System.Text.Encoding": "4.0.11",
+ "System.Threading": "4.0.11",
+ "runtime.native.System": "4.0.0",
+ "runtime.native.System.Net.Http": "4.0.1",
+ "runtime.native.System.Security.Cryptography": "4.0.0"
+ }
+ },
+ "System.Security.Principal": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==",
+ "dependencies": {
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Text.Encoding": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Text.Encoding.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "Microsoft.NETCore.Targets": "1.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Text.Encoding": "4.0.11"
+ }
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Reflection": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "4.7.2",
+ "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg=="
+ },
+ "System.Text.RegularExpressions": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11"
+ }
+ },
+ "System.Threading": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==",
+ "dependencies": {
+ "System.Runtime": "4.1.0",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "System.Threading.Tasks": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Threading.Tasks.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Runtime": "4.1.0",
+ "System.Threading.Tasks": "4.0.11"
+ }
+ },
+ "System.Xml.ReaderWriter": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.IO.FileSystem": "4.0.1",
+ "System.IO.FileSystem.Primitives": "4.0.1",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Runtime.InteropServices": "4.1.0",
+ "System.Text.Encoding": "4.0.11",
+ "System.Text.Encoding.Extensions": "4.0.11",
+ "System.Text.RegularExpressions": "4.1.0",
+ "System.Threading.Tasks": "4.0.11",
+ "System.Threading.Tasks.Extensions": "4.0.0"
+ }
+ },
+ "System.Xml.XDocument": {
+ "type": "Transitive",
+ "resolved": "4.0.11",
+ "contentHash": "Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Diagnostics.Tools": "4.0.1",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Reflection": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Text.Encoding": "4.0.11",
+ "System.Threading": "4.0.11",
+ "System.Xml.ReaderWriter": "4.0.11"
+ }
+ },
+ "System.Xml.XPath": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==",
+ "dependencies": {
+ "System.Collections": "4.0.11",
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Globalization": "4.0.11",
+ "System.IO": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11",
+ "System.Xml.ReaderWriter": "4.0.11"
+ }
+ },
+ "System.Xml.XPath.XDocument": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.11",
+ "System.Linq": "4.1.0",
+ "System.Resources.ResourceManager": "4.0.1",
+ "System.Runtime": "4.1.0",
+ "System.Runtime.Extensions": "4.1.0",
+ "System.Threading": "4.0.11",
+ "System.Xml.ReaderWriter": "4.0.11",
+ "System.Xml.XDocument": "4.0.11",
+ "System.Xml.XPath": "4.0.1"
+ }
+ },
+ "timeline.errorcodes": {
+ "type": "Project"
+ }
+ }
+ }
+}
\ No newline at end of file |