diff options
author | 杨宇千 <crupest@outlook.com> | 2020-02-03 18:38:14 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-03 18:38:14 +0800 |
commit | 97010e98a11ad6fd6c075cee414c30174001fe5d (patch) | |
tree | 28ef20f9dfe742008bb934f09b99d1d4719cabaa /Timeline | |
parent | 1c880744293c9f47ff1241109b72c035680251df (diff) | |
parent | be8cbe2c4ddf2076cc02bcb2feb1a70d30a4bda0 (diff) | |
download | timeline-97010e98a11ad6fd6c075cee414c30174001fe5d.tar.gz timeline-97010e98a11ad6fd6c075cee414c30174001fe5d.tar.bz2 timeline-97010e98a11ad6fd6c075cee414c30174001fe5d.zip |
Merge pull request #57 from crupest/dev
Add normal timeline feature.
Diffstat (limited to 'Timeline')
19 files changed, 517 insertions, 166 deletions
diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 11353bb5..b6c213d9 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -26,9 +26,9 @@ namespace Timeline.Controllers }
[HttpGet("users/{username}/timeline")]
- public async Task<ActionResult<BaseTimelineInfo>> TimelineGet([FromRoute][Username] string username)
+ public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][Username] string username)
{
- return await _service.GetTimeline(username);
+ return (await _service.GetTimeline(username)).FillLinksForPersonalTimeline(Url);
}
[HttpGet("users/{username}/timeline/posts")]
@@ -77,14 +77,14 @@ namespace Timeline.Controllers [HttpPatch("users/{username}/timeline")]
[Authorize]
- public async Task<ActionResult<BaseTimelineInfo>> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body)
+ public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body)
{
- if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
await _service.ChangeProperty(username, body);
- var timeline = await _service.GetTimeline(username);
+ var timeline = (await _service.GetTimeline(username)).FillLinksForPersonalTimeline(Url);
return Ok(timeline);
}
@@ -92,7 +92,7 @@ namespace Timeline.Controllers [Authorize]
public async Task<ActionResult> TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member)
{
- if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
@@ -104,7 +104,7 @@ namespace Timeline.Controllers }
catch (UserNotExistException)
{
- return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist());
+ return BadRequest(ErrorResponse.TimelineCommon.MemberPut_NotExist());
}
}
@@ -112,7 +112,7 @@ namespace Timeline.Controllers [Authorize]
public async Task<ActionResult> TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member)
{
- if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs new file mode 100644 index 00000000..a514ccd9 --- /dev/null +++ b/Timeline/Controllers/TimelineController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Timeline.Filters;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+
+namespace Timeline.Controllers
+{
+ [ApiController]
+ [CatchTimelineNotExistException]
+ public class TimelineController : Controller
+ {
+ private readonly ILogger<TimelineController> _logger;
+
+ private readonly ITimelineService _service;
+
+ public TimelineController(ILogger<TimelineController> logger, ITimelineService service)
+ {
+ _logger = logger;
+ _service = service;
+ }
+
+ [HttpGet("timelines/{name}")]
+ public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][TimelineName] string name)
+ {
+ var result = (await _service.GetTimeline(name)).FillLinksForNormalTimeline(Url);
+ return Ok(result);
+ }
+
+ [HttpGet("timelines/{name}/posts")]
+ public async Task<ActionResult<IList<TimelinePostInfo>>> PostListGet([FromRoute][TimelineName] string name)
+ {
+ if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ return await _service.GetPosts(name);
+ }
+
+ [HttpPost("timelines/{name}/posts")]
+ [Authorize]
+ public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][TimelineName] 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 res = await _service.CreatePost(name, id, body.Content, body.Time);
+ return res;
+ }
+
+ [HttpDelete("timelines/{name}/posts/{id}")]
+ [Authorize]
+ public async Task<ActionResult> PostDelete([FromRoute][TimelineName] string name, [FromRoute] long id)
+ {
+ try
+ {
+ if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+ await _service.DeletePost(name, id);
+ return Ok(CommonDeleteResponse.Delete());
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return Ok(CommonDeleteResponse.NotExist());
+ }
+ }
+
+ [HttpPatch("timelines/{name}")]
+ [Authorize]
+ public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][TimelineName] 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, body);
+ var timeline = (await _service.GetTimeline(name)).FillLinksForNormalTimeline(Url);
+ return Ok(timeline);
+ }
+
+ [HttpPut("timelines/{name}/members/{member}")]
+ [Authorize]
+ public async Task<ActionResult> TimelineMemberPut([FromRoute][TimelineName] 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.TimelineCommon.MemberPut_NotExist());
+ }
+ }
+
+ [HttpDelete("timelines/{name}/members/{member}")]
+ [Authorize]
+ public async Task<ActionResult> TimelineMemberDelete([FromRoute][TimelineName] 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());
+ }
+ }
+
+ [HttpPost("timelines")]
+ [Authorize]
+ public async Task<ActionResult<TimelineInfo>> TimelineCreate([FromBody] TimelineCreateRequest body)
+ {
+ var userId = this.GetUserId();
+
+ try
+ {
+ var timelineInfo = (await _service.CreateTimeline(body.Name, userId)).FillLinksForNormalTimeline(Url);
+ return Ok(timelineInfo);
+ }
+ catch (ConflictException)
+ {
+ return BadRequest(ErrorResponse.TimelineCommon.NameConflict());
+ }
+ }
+ }
+}
diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index c50fe6dd..0b252bab 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -6,7 +6,8 @@ using Timeline.Models.Http; namespace Timeline.Entities
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is entity object.")]
+#pragma warning disable CA2227 // Collection properties should be read only
+ // TODO: Create index for this table.
[Table("timelines")]
public class TimelineEntity
{
@@ -38,4 +39,5 @@ namespace Timeline.Entities public List<TimelinePostEntity> Posts { get; set; } = default!;
}
+#pragma warning restore CA2227 // Collection properties should be read only
}
diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index ed78e645..e133c9d6 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -17,7 +17,7 @@ namespace Timeline.Filters }
else
{
- throw new System.NotImplementedException();
+ context.Result = new NotFoundObjectResult(ErrorResponse.TimelineCommon.NotExist());
}
}
}
diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index 87516638..0d23fe59 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -1,4 +1,5 @@ -using static Timeline.Resources.Messages;
+
+using static Timeline.Resources.Messages;
namespace Timeline.Models.Http
{
@@ -241,21 +242,41 @@ namespace Timeline.Models.Http }
- public static class TimelineController
+ public static class TimelineCommon
{
+ public static CommonResponse NameConflict(params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(TimelineCommon_NameConflict, formatArgs));
+ }
+
+ public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(message, formatArgs));
+ }
+
+ public static CommonResponse NotExist(params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineCommon.NotExist, string.Format(TimelineCommon_NotExist, formatArgs));
+ }
+
+ public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineCommon.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));
+ return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(TimelineCommon_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));
+ return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(message, formatArgs));
}
}
}
-}
\ No newline at end of file +}
diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs index febb8186..1cb47dac 100644 --- a/Timeline/Models/Http/TimelineCommon.cs +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -1,5 +1,7 @@ -using System;
+using Microsoft.AspNetCore.Mvc;
+using System;
using System.Collections.Generic;
+using Timeline.Controllers;
namespace Timeline.Models.Http
{
@@ -28,17 +30,56 @@ namespace Timeline.Models.Http public DateTime LastUpdated { get; set; } = default!;
}
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")]
- public class BaseTimelineInfo
+ public class TimelineInfo
{
+ public string? Name { get; set; }
public string Description { get; set; } = default!;
public UserInfo Owner { get; set; } = default!;
public TimelineVisibility Visibility { get; set; }
+#pragma warning disable CA2227 // Collection properties should be read only
public List<UserInfo> Members { get; set; } = default!;
+#pragma warning restore CA2227 // Collection properties should be read only
+
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+ public TimelineInfoLinks? _links { get; set; }
+#pragma warning restore CA1707 // Identifiers should not contain underscores
}
- public class TimelineInfo : BaseTimelineInfo
+ public class TimelineInfoLinks
{
- public string Name { get; set; } = default!;
+ public string Posts { get; set; } = default!;
+ }
+
+ public static class TimelineInfoExtensions
+ {
+ public static TimelineInfo FillLinksForPersonalTimeline(this TimelineInfo info, IUrlHelper urlHelper)
+ {
+ if (info == null)
+ throw new ArgumentNullException(nameof(info));
+ if (urlHelper == null)
+ throw new ArgumentNullException(nameof(urlHelper));
+
+ info._links = new TimelineInfoLinks
+ {
+ Posts = urlHelper.ActionLink(nameof(PersonalTimelineController.PostListGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { info.Owner.Username })
+ };
+
+ return info;
+ }
+
+ public static TimelineInfo FillLinksForNormalTimeline(this TimelineInfo info, IUrlHelper urlHelper)
+ {
+ if (info == null)
+ throw new ArgumentNullException(nameof(info));
+ if (urlHelper == null)
+ throw new ArgumentNullException(nameof(urlHelper));
+
+ info._links = new TimelineInfoLinks
+ {
+ Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { info.Name })
+ };
+
+ return info;
+ }
}
}
diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs index f9a4d3e5..6d461bb9 100644 --- a/Timeline/Models/Http/TimelineController.cs +++ b/Timeline/Models/Http/TimelineController.cs @@ -1,5 +1,6 @@ using System;
using System.ComponentModel.DataAnnotations;
+using Timeline.Models.Validation;
namespace Timeline.Models.Http
{
@@ -11,6 +12,13 @@ namespace Timeline.Models.Http public DateTime? Time { get; set; }
}
+ public class TimelineCreateRequest
+ {
+ [Required]
+ [TimelineName]
+ public string Name { get; set; } = default!;
+ }
+
public class TimelinePatchRequest
{
public string? Description { get; set; }
diff --git a/Timeline/Models/Validation/NameValidator.cs b/Timeline/Models/Validation/NameValidator.cs new file mode 100644 index 00000000..8db10ebd --- /dev/null +++ b/Timeline/Models/Validation/NameValidator.cs @@ -0,0 +1,33 @@ +using System.Linq;
+using static Timeline.Resources.Models.Validation.NameValidator;
+
+namespace Timeline.Models.Validation
+{
+ public class NameValidator : Validator<string>
+ {
+ public const int MaxLength = 26;
+
+ protected override (bool, string) DoValidate(string value)
+ {
+ if (value.Length == 0)
+ {
+ return (false, MessageEmptyString);
+ }
+
+ if (value.Length > 26)
+ {
+ return (false, MessageTooLong);
+ }
+
+ foreach ((char c, int i) in value.Select((c, i) => (c, i)))
+ {
+ if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_'))
+ {
+ return (false, MessageInvalidChar);
+ }
+ }
+
+ return (true, GetSuccessMessage());
+ }
+ }
+}
diff --git a/Timeline/Models/Validation/TimelineNameValidator.cs b/Timeline/Models/Validation/TimelineNameValidator.cs new file mode 100644 index 00000000..f1ab54e8 --- /dev/null +++ b/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/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index d8f3bdc0..87bbf85f 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -1,35 +1,9 @@ using System;
-using System.Linq;
-using static Timeline.Resources.Models.Validation.UsernameValidator;
namespace Timeline.Models.Validation
{
- public class UsernameValidator : Validator<string>
+ public class UsernameValidator : NameValidator
{
- public const int MaxLength = 26;
-
- protected override (bool, string) DoValidate(string value)
- {
- if (value.Length == 0)
- {
- return (false, MessageEmptyString);
- }
-
- if (value.Length > 26)
- {
- return (false, MessageTooLong);
- }
-
- foreach ((char c, int i) in value.Select((c, i) => (c, i)))
- {
- if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_'))
- {
- return (false, MessageInvalidChar);
- }
- }
-
- return (true, GetSuccessMessage());
- }
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 332c8817..eeb44f10 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -151,38 +151,29 @@ namespace Timeline.Resources { }
/// <summary>
- /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format..
- /// </summary>
- internal static string TimelineController_ChangeMember_UsernameBadFormat {
- get {
- return ResourceManager.GetString("TimelineController_ChangeMember_UsernameBadFormat", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist..
+ /// Looks up a localized string similar to The user to set as member does not exist..
/// </summary>
- internal static string TimelineController_ChangeMember_UserNotExist {
+ internal static string TimelineCommon_MemberPut_NotExist {
get {
- return ResourceManager.GetString("TimelineController_ChangeMember_UserNotExist", resourceCulture);
+ return ResourceManager.GetString("TimelineCommon_MemberPut_NotExist", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to The user to set as member does not exist..
+ /// Looks up a localized string similar to A timeline with given name already exists..
/// </summary>
- internal static string TimelineController_MemberPut_NotExist {
+ internal static string TimelineCommon_NameConflict {
get {
- return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture);
+ return ResourceManager.GetString("TimelineCommon_NameConflict", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to The post to delete does not exist..
+ /// Looks up a localized string similar to The timeline with given name does not exist..
/// </summary>
- internal static string TimelineController_PostOperationDelete_NotExist {
+ internal static string TimelineCommon_NotExist {
get {
- return ResourceManager.GetString("TimelineController_PostOperationDelete_NotExist", resourceCulture);
+ return ResourceManager.GetString("TimelineCommon_NotExist", resourceCulture);
}
}
diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index cb6c3891..66a84d5f 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -147,17 +147,14 @@ <data name="Common_InvalidModel" xml:space="preserve">
<value>Model is of bad format.</value>
</data>
- <data name="TimelineController_ChangeMember_UsernameBadFormat" xml:space="preserve">
- <value>The {0}-st username to do operation {1} on is of bad format.</value>
- </data>
- <data name="TimelineController_ChangeMember_UserNotExist" xml:space="preserve">
- <value>The {0}-st user to do operation {1} on does not exist.</value>
- </data>
- <data name="TimelineController_MemberPut_NotExist" xml:space="preserve">
+ <data name="TimelineCommon_MemberPut_NotExist" xml:space="preserve">
<value>The user to set as member does not exist.</value>
</data>
- <data name="TimelineController_PostOperationDelete_NotExist" xml:space="preserve">
- <value>The post to delete does not exist.</value>
+ <data name="TimelineCommon_NameConflict" xml:space="preserve">
+ <value>A timeline with given name already exists.</value>
+ </data>
+ <data name="TimelineCommon_NotExist" xml:space="preserve">
+ <value>The timeline with given name does not exist.</value>
</data>
<data name="TokenController_Create_BadCredential" xml:space="preserve">
<value>Username or password is invalid.</value>
diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.Designer.cs b/Timeline/Resources/Models/Validation/NameValidator.Designer.cs index ac925504..5b869226 100644 --- a/Timeline/Resources/Models/Validation/UsernameValidator.Designer.cs +++ b/Timeline/Resources/Models/Validation/NameValidator.Designer.cs @@ -22,14 +22,14 @@ namespace Timeline.Resources.Models.Validation { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class UsernameValidator {
+ 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 UsernameValidator() {
+ internal NameValidator() {
}
/// <summary>
@@ -39,7 +39,7 @@ namespace Timeline.Resources.Models.Validation { 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.UsernameValidator", typeof(UsernameValidator).Assembly);
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NameValidator", typeof(NameValidator).Assembly);
resourceMan = temp;
}
return resourceMan;
diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.resx b/Timeline/Resources/Models/Validation/NameValidator.resx index 08a814d0..08a814d0 100644 --- a/Timeline/Resources/Models/Validation/UsernameValidator.resx +++ b/Timeline/Resources/Models/Validation/NameValidator.resx diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs index 8212c252..3ee5959f 100644 --- a/Timeline/Resources/Services/TimelineService.Designer.cs +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -77,5 +77,23 @@ namespace Timeline.Resources.Services { return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline name is of bad format because {0}..
+ /// </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);
+ }
+ }
}
}
diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx index 0429a2f8..e0d76c9a 100644 --- a/Timeline/Resources/Services/TimelineService.resx +++ b/Timeline/Resources/Services/TimelineService.resx @@ -123,4 +123,10 @@ <data name="ExceptionFindTimelineUsernameBadFormat" xml:space="preserve">
<value>The owner username of personal timeline is of bad format.</value>
</data>
+ <data name="ExceptionTimelineNameBadFormat" xml:space="preserve">
+ <value>The timeline name is of bad format because {0}.</value>
+ </data>
+ <data name="ExceptionTimelineNameConflict" xml:space="preserve">
+ <value>The timeline with given name already exists.</value>
+ </data>
</root>
\ No newline at end of file diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 0ea68265..6f9d437e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -25,6 +25,37 @@ namespace Timeline.Services public interface IBaseTimelineService
{
/// <summary>
+ /// Get the timeline info.
+ /// </summary>
+ /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<TimelineInfo> GetTimeline(string name);
+
+ /// <summary>
+ /// Set the properties of a timeline.
+ /// </summary>
+ /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="newProperties">The new properties. Null member means not to change.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task ChangeProperty(string name, TimelinePatchRequest newProperties);
+
+ /// <summary>
/// Get all the posts in the timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
@@ -108,6 +139,26 @@ namespace Timeline.Services Task ChangeMember(string name, IList<string>? add, IList<string>? remove);
/// <summary>
+ /// Check whether a user can manage(change timeline info, member, ...) a timeline.
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="id"></param>
+ /// <returns>True if the user can manage the timeline, otherwise false.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <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 name, long userId);
+
+ /// <summary>
/// Verify whether a visitor has the permission to read a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
@@ -178,64 +229,20 @@ namespace Timeline.Services public interface ITimelineService : IBaseTimelineService
{
/// <summary>
- /// Get the timeline info.
- /// </summary>
- /// <param name="name">The name of the timeline.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">
- /// Thrown when timeline name is invalid. Currently it means it is an empty string.
- /// </exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with the name does not exist.
- /// </exception>
- Task<TimelineInfo> GetTimeline(string name);
-
- /// <summary>
/// Create a timeline.
/// </summary>
/// <param name="name">The name of the timeline.</param>
/// <param name="owner">The id of owner of the timeline.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="owner"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when timeline name is invalid. Currently it means it is an empty string.</exception>
+ /// <returns>The info of the new timeline.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
/// <exception cref="ConflictException">Thrown when the timeline already exists.</exception>
/// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
- Task CreateTimeline(string name, long owner);
+ Task<TimelineInfo> CreateTimeline(string name, long owner);
}
public interface IPersonalTimelineService : IBaseTimelineService
{
- /// <summary>
- /// Get the timeline info.
- /// </summary>
- /// <param name="username">The username of the owner of the personal timeline.</param>
- /// <returns>The timeline 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.
- /// </exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
- /// </exception>
- Task<BaseTimelineInfo> GetTimeline(string username);
-
- /// <summary>
- /// Set the properties of a timeline.
- /// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="newProperties">The new properties. Null member means not to change.</param>
- /// <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="TimelineNotExistException">
- /// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
- /// </exception>
- Task ChangeProperty(string name, TimelinePatchRequest newProperties);
}
@@ -259,6 +266,17 @@ namespace Timeline.Services protected IMapper Mapper { get; }
+ protected TimelineEntity CreateNewEntity(string? name, long owner)
+ {
+ return new TimelineEntity
+ {
+ Name = name,
+ OwnerId = owner,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = Clock.GetCurrentTime()
+ };
+ }
+
/// <summary>
/// Find the timeline id by the name.
/// For details, see remarks.
@@ -283,6 +301,35 @@ namespace Timeline.Services /// </remarks>
protected abstract Task<long> FindTimelineId(string name);
+ public async Task<TimelineInfo> GetTimeline(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
+
+ var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
+
+ var members = new List<UserInfo>();
+ foreach (var memberEntity in timelineMemberEntities)
+ {
+ members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
+ }
+
+ return new TimelineInfo
+ {
+ Name = timelineEntity.Name,
+ Description = timelineEntity.Description ?? "",
+ Owner = owner,
+ Visibility = timelineEntity.Visibility,
+ Members = members
+ };
+ }
+
public async Task<List<TimelinePostInfo>> GetPosts(string name)
{
if (name == null)
@@ -464,6 +511,17 @@ namespace Timeline.Services await Database.SaveChangesAsync();
}
+ public async Task<bool> HasManagePermission(string name, long userId)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+ 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 name, long? visitorId)
{
if (name == null)
@@ -523,6 +581,72 @@ namespace Timeline.Services }
}
+ public class TimelineService : BaseTimelineService, ITimelineService
+ {
+ private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
+
+ private void ValidateTimelineName(string name, string paramName)
+ {
+ if (!_timelineNameValidator.Validate(name, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName);
+ }
+ }
+
+ public TimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
+ : base(loggerFactory, database, userService, mapper, clock)
+ {
+
+ }
+
+ protected override async Task<long> FindTimelineId(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ ValidateTimelineName(name, nameof(name));
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Name == name).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity == null)
+ {
+ throw new TimelineNotExistException(name);
+ }
+ else
+ {
+ return timelineEntity.Id;
+ }
+ }
+
+ public async Task<TimelineInfo> 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 ConflictException(ExceptionTimelineNameConflict);
+
+ var newEntity = CreateNewEntity(name, owner);
+ Database.Timelines.Add(newEntity);
+ await Database.SaveChangesAsync();
+
+ return new TimelineInfo
+ {
+ Name = name,
+ Description = "",
+ Owner = Mapper.Map<UserInfo>(user),
+ Visibility = newEntity.Visibility,
+ Members = new List<UserInfo>()
+ };
+ }
+ }
+
public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
{
public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
@@ -533,6 +657,9 @@ namespace Timeline.Services protected override async Task<long> FindTimelineId(string name)
{
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
long userId;
try
{
@@ -555,48 +682,12 @@ namespace Timeline.Services }
else
{
- var newTimelineEntity = new TimelineEntity
- {
- Name = null,
- Description = null,
- OwnerId = userId,
- Visibility = TimelineVisibility.Register,
- CreateTime = Clock.GetCurrentTime(),
- };
+ var newTimelineEntity = CreateNewEntity(null, userId);
Database.Timelines.Add(newTimelineEntity);
await Database.SaveChangesAsync();
return newTimelineEntity.Id;
}
}
-
- public async Task<BaseTimelineInfo> GetTimeline(string username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- var timelineId = await FindTimelineId(username);
-
- var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
-
- var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
-
- var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
-
- var members = new List<UserInfo>();
- foreach (var memberEntity in timelineMemberEntities)
- {
- members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
- }
-
- return new BaseTimelineInfo
- {
- Description = timelineEntity.Description ?? "",
- Owner = owner,
- Visibility = timelineEntity.Visibility,
- Members = members
- };
- }
-
}
}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 86349a27..2640a061 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -89,6 +89,7 @@ namespace Timeline services.AddScoped<IUserTokenManager, UserTokenManager>();
services.AddUserAvatarService();
+ services.AddScoped<ITimelineService, TimelineService>();
services.AddScoped<IPersonalTimelineService, PersonalTimelineService>();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 1a3a07cd..08999f82 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -89,10 +89,10 @@ <AutoGen>True</AutoGen>
<DependentUpon>NicknameValidator.resx</DependentUpon>
</Compile>
- <Compile Update="Resources\Models\Validation\UsernameValidator.Designer.cs">
+ <Compile Update="Resources\Models\Validation\NameValidator.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
- <DependentUpon>UsernameValidator.resx</DependentUpon>
+ <DependentUpon>NameValidator.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Models\Validation\Validator.Designer.cs">
<DesignTime>True</DesignTime>
@@ -163,9 +163,9 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>NicknameValidator.Designer.cs</LastGenOutput>
</EmbeddedResource>
- <EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
+ <EmbeddedResource Update="Resources\Models\Validation\NameValidator.resx">
<Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UsernameValidator.Designer.cs</LastGenOutput>
+ <LastGenOutput>NameValidator.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Models\Validation\Validator.resx">
<Generator>ResXFileCodeGenerator</Generator>
|