diff options
Diffstat (limited to 'BackEnd')
203 files changed, 26480 insertions, 0 deletions
diff --git a/BackEnd/Directory.Build.props b/BackEnd/Directory.Build.props new file mode 100644 index 00000000..5030ba17 --- /dev/null +++ b/BackEnd/Directory.Build.props @@ -0,0 +1,6 @@ +<Project>
+ <PropertyGroup>
+ <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+ <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
+ </PropertyGroup>
+</Project>
diff --git a/BackEnd/Nuget.Config b/BackEnd/Nuget.Config new file mode 100644 index 00000000..e219de2b --- /dev/null +++ b/BackEnd/Nuget.Config @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <packageSources>
+ <clear />
+ <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
+ </packageSources>
+</configuration>
diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs new file mode 100644 index 00000000..84ab5908 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs @@ -0,0 +1,77 @@ +using System;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace Timeline.ErrorCodes.CodeGenerator
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ string Indent(int n)
+ {
+ const string indent = " ";
+ return string.Concat(Enumerable.Repeat(indent, n));
+ }
+
+ StringBuilder code = new StringBuilder();
+
+ code.AppendLine("using static Timeline.Resources.Messages;");
+ code.AppendLine();
+ code.AppendLine("namespace Timeline.Models.Http");
+ code.AppendLine("{");
+
+ int depth = 1;
+
+ void RecursiveAddErrorCode(Type type, bool root)
+ {
+ code.AppendLine($"{Indent(depth)}public static class {(root ? "ErrorResponse" : type.Name)}");
+ code.AppendLine($"{Indent(depth)}{{");
+
+ foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
+ .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int)))
+ {
+ var path = type.FullName.Replace("+", ".").Replace("Timeline.Models.Http.ErrorCodes.", "") + "." + field.Name;
+
+ code.AppendLine($"{Indent(depth + 1)}public static CommonResponse {field.Name}(params object?[] formatArgs)");
+ code.AppendLine($"{Indent(depth + 1)}{{");
+ code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format({path.Replace(".", "_")}, formatArgs));");
+ code.AppendLine($"{Indent(depth + 1)}}}");
+ code.AppendLine();
+ code.AppendLine($"{Indent(depth + 1)}public static CommonResponse CustomMessage_{field.Name}(string message, params object?[] formatArgs)");
+ code.AppendLine($"{Indent(depth + 1)}{{");
+ code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format(message, formatArgs));");
+ code.AppendLine($"{Indent(depth + 1)}}}");
+ code.AppendLine();
+ }
+
+ depth += 1;
+
+ foreach (var nestedType in type.GetNestedTypes())
+ {
+ RecursiveAddErrorCode(nestedType, false);
+ }
+
+ depth -= 1;
+
+ code.AppendLine($"{Indent(depth)}}}");
+ code.AppendLine();
+ }
+
+ RecursiveAddErrorCode(typeof(Timeline.Models.Http.ErrorCodes), true);
+
+ code.AppendLine("}");
+
+ var generatedCode = code.ToString();
+
+ Console.WriteLine(generatedCode);
+
+ TextCopy.ClipboardService.SetText(generatedCode);
+ var oldColor = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("Code has copied to clipboard!");
+ Console.ForegroundColor = oldColor;
+ }
+ }
+}
diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj new file mode 100644 index 00000000..c8eb97f3 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="TextCopy" Version="4.2.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json b/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json new file mode 100644 index 00000000..69cfee1e --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json @@ -0,0 +1,24 @@ +{
+ "version": 1,
+ "dependencies": {
+ ".NETCoreApp,Version=v3.1": {
+ "TextCopy": {
+ "type": "Direct",
+ "requested": "[4.2.0, )",
+ "resolved": "4.2.0",
+ "contentHash": "NY2UAFIjBJj+3aABP5tyO6ooEdkJxIGtwRNqvMQKLmyIeZiyGvM4XYbkKNntyQlhyFhhfBww05C3D/0DdimfaQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.4"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.4",
+ "contentHash": "AceHamXNKDMDwIoZqEoApLp8s3935wSC3VXrPaRWa0wWOaEcYdDlo1nWQ1zLiezoDmpJzV7FqDm53E0Ty/hEMg=="
+ },
+ "timeline.errorcodes": {
+ "type": "Project"
+ }
+ }
+ }
+}
\ No newline at end of file diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs new file mode 100644 index 00000000..91e0c1fd --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -0,0 +1,66 @@ +namespace Timeline.Models.Http
+{
+ /// <summary>
+ /// All error code constants.
+ /// </summary>
+ /// <remarks>
+ /// Format: 1bbbccdd
+ /// </remarks>
+ public static class ErrorCodes
+ {
+ public static class Common
+ {
+ public const int InvalidModel = 1_000_0001;
+ public const int Forbid = 1_000_0002;
+ public const int UnknownEndpoint = 1_000_0003;
+
+ public static class Header
+ {
+ public const int IfNonMatch_BadFormat = 1_000_01_01;
+ }
+
+ public static class Content
+ {
+ public const int TooBig = 1_000_11_01;
+ }
+ }
+
+ public static class UserCommon
+ {
+ public const int NotExist = 1_001_0001;
+ }
+
+ public static class TokenController
+ {
+ public const int Create_BadCredential = 1_101_01_01;
+ public const int Verify_BadFormat = 1_101_02_01;
+ public const int Verify_UserNotExist = 1_101_02_02;
+ public const int Verify_OldVersion = 1_101_02_03;
+ public const int Verify_TimeExpired = 1_101_02_04;
+ }
+
+ public static class UserController
+ {
+ public const int UsernameConflict = 1_102_01_01;
+ public const int ChangePassword_BadOldPassword = 1_102_02_01;
+ }
+
+ public static class UserAvatar
+ {
+ public const int BadFormat_CantDecode = 1_103_00_01;
+ public const int BadFormat_UnmatchedFormat = 1_103_00_02;
+ public const int BadFormat_BadSize = 1_103_00_03;
+ }
+
+ public static class TimelineController
+ {
+ public const int NameConflict = 1_104_01_01;
+ public const int NotExist = 1_104_02_01;
+ public const int MemberPut_NotExist = 1_104_03_01;
+ public const int QueryRelateNotExist = 1_104_04_01;
+ public const int PostNotExist = 1_104_05_01;
+ public const int PostNoData = 1_104_05_02;
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj b/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj new file mode 100644 index 00000000..01ca2568 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj @@ -0,0 +1,7 @@ +<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/BackEnd/Timeline.ErrorCodes/packages.lock.json b/BackEnd/Timeline.ErrorCodes/packages.lock.json new file mode 100644 index 00000000..dabf86bc --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/packages.lock.json @@ -0,0 +1,6 @@ +{
+ "version": 1,
+ "dependencies": {
+ ".NETCoreApp,Version=v3.1": {}
+ }
+}
\ No newline at end of file diff --git a/BackEnd/Timeline.Tests/ErrorCodeTest.cs b/BackEnd/Timeline.Tests/ErrorCodeTest.cs new file mode 100644 index 00000000..258ebf4e --- /dev/null +++ b/BackEnd/Timeline.Tests/ErrorCodeTest.cs @@ -0,0 +1,53 @@ +using FluentAssertions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Timeline.Models.Http;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests
+{
+ public class ErrorCodeTest
+ {
+ private readonly ITestOutputHelper _output;
+
+ public ErrorCodeTest(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void ShouldWork()
+ {
+ var errorCodes = new Dictionary<int, string>();
+
+ 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;
+ var value = (int)field.GetRawConstantValue();
+ _output.WriteLine($"Find error code {name} , value is {value}.");
+
+ value.Should().BeInRange(1000_0000, 9999_9999, "Error code should have exactly 8 digits.");
+
+ errorCodes.Should().NotContainKey(value,
+ "identical error codes are found and conflict paths are {0} and {1}",
+ name, errorCodes.GetValueOrDefault(value));
+
+ errorCodes.Add(value, name);
+ }
+
+ foreach (var nestedType in type.GetNestedTypes())
+ {
+ RecursiveCheckErrorCode(nestedType);
+ }
+ }
+
+ RecursiveCheckErrorCode(typeof(ErrorCodes));
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..0f873033 --- /dev/null +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -0,0 +1,16 @@ +// 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("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
diff --git a/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs new file mode 100644 index 00000000..b78309c0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions;
+using FluentAssertions.Primitives;
+using FluentAssertions.Specialized;
+using System;
+using System.Threading.Tasks;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class AsyncFunctionAssertionsExtensions
+ {
+ public static async Task<AndConstraint<ObjectAssertions>> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs)
+ {
+ return (await assertions.ThrowAsync<Exception>(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs new file mode 100644 index 00000000..b3709a28 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs @@ -0,0 +1,64 @@ +using FluentAssertions;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class CacheTestHelper
+ {
+ public static async Task TestCache(HttpClient client, string getUrl)
+ {
+ EntityTagHeaderValue eTag;
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200);
+ var cacheControlHeader = res.Headers.CacheControl;
+ cacheControlHeader.NoCache.Should().BeTrue();
+ cacheControlHeader.NoStore.Should().BeFalse();
+ cacheControlHeader.Private.Should().BeTrue();
+ cacheControlHeader.Public.Should().BeFalse();
+ cacheControlHeader.MustRevalidate.Should().BeTrue();
+ cacheControlHeader.MaxAge.Should().NotBeNull().And.Be(TimeSpan.FromDays(14));
+ eTag = res.Headers.ETag;
+ }
+
+ {
+ using var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress, getUrl),
+ Method = HttpMethod.Get,
+ };
+ request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd");
+ var res = await client.SendAsync(request);
+ res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.HaveCommonBody(ErrorCodes.Common.Header.IfNonMatch_BadFormat);
+ }
+
+ {
+ using var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress, getUrl),
+ Method = HttpMethod.Get,
+ };
+ request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\"");
+ var res = await client.SendAsync(request);
+ res.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ {
+ using var request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress, getUrl),
+ Method = HttpMethod.Get,
+ };
+ request.Headers.Add("If-None-Match", eTag.ToString());
+ var res = await client.SendAsync(request);
+ res.Should().HaveStatusCode(HttpStatusCode.NotModified);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs new file mode 100644 index 00000000..6513bbe7 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json;
+using System;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class HttpClientExtensions
+ {
+ public static Task<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient client, string url, T body)
+ {
+ return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ public static Task<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient client, Uri url, T body)
+ {
+ return client.PatchAsync(url, new StringContent(
+ JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json));
+ }
+
+ public static Task<HttpResponseMessage> PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType)
+ {
+ return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ public static Task<HttpResponseMessage> PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType)
+ {
+ var content = new ByteArrayContent(body);
+ content.Headers.ContentLength = body.Length;
+ content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
+ return client.PutAsync(url, content);
+ }
+
+ public static Task<HttpResponseMessage> PutStringAsync(this HttpClient client, string url, string body, string mimeType = null)
+ {
+ return client.PutStringAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ public static Task<HttpResponseMessage> PutStringAsync(this HttpClient client, Uri url, string body, string mimeType = null)
+ {
+ var content = new StringContent(body, Encoding.UTF8, mimeType ?? MediaTypeNames.Text.Plain);
+ return client.PutAsync(url, content);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs new file mode 100644 index 00000000..2bd497f1 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs @@ -0,0 +1,35 @@ +using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Timeline.Models.Converters;
+using Timeline.Models.Http;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class HttpResponseExtensions
+ {
+ public static JsonSerializerOptions JsonSerializerOptions { get; }
+
+ static HttpResponseExtensions()
+ {
+ JsonSerializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
+ }
+
+ public static async Task<T> ReadBodyAsJsonAsync<T>(this HttpResponseMessage response)
+ {
+ var stream = await response.Content.ReadAsStreamAsync();
+ return await JsonSerializer.DeserializeAsync<T>(stream, JsonSerializerOptions);
+ }
+
+ public static Task<CommonResponse> ReadBodyAsCommonResponseAsync(this HttpResponseMessage response)
+ {
+ return response.ReadBodyAsJsonAsync<CommonResponse>();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs new file mode 100644 index 00000000..9bed0917 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs @@ -0,0 +1,26 @@ +using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.PixelFormats;
+using System.IO;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class ImageHelper
+ {
+ public static byte[] CreatePngWithSize(int width, int height)
+ {
+ using var image = new Image<Rgba32>(width, height);
+ using var stream = new MemoryStream();
+ image.SaveAsPng(stream);
+ return stream.ToArray();
+ }
+
+ public static byte[] CreateImageWithSize(int width, int height, IImageFormat format)
+ {
+ using var image = new Image<Rgba32>(width, height);
+ using var stream = new MemoryStream();
+ image.Save(stream, format);
+ return stream.ToArray();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs new file mode 100644 index 00000000..d3e5a41e --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs @@ -0,0 +1,60 @@ +using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Formatting;
+using FluentAssertions.Primitives;
+using System;
+using System.Reflection;
+
+namespace Timeline.Tests.Helpers
+{
+ public class ParameterInfoValueFormatter : IValueFormatter
+ {
+ public bool CanHandle(object value)
+ {
+ return value is ParameterInfo;
+ }
+
+ public string Format(object value, FormattingContext context, FormatChild formatChild)
+ {
+ var param = (ParameterInfo)value;
+ return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}";
+ }
+ }
+
+ public class ParameterInfoAssertions : ReferenceTypeAssertions<ParameterInfo, ParameterInfoAssertions>
+ {
+ static ParameterInfoAssertions()
+ {
+ Formatter.AddFormatter(new ParameterInfoValueFormatter());
+ }
+
+ public ParameterInfoAssertions(ParameterInfo parameterInfo)
+ {
+ Subject = parameterInfo;
+ }
+
+ protected override string Identifier => "parameter";
+
+ public AndWhichConstraint<ParameterInfoAssertions, TAttribute> BeDecoratedWith<TAttribute>(string because = "", params object[] becauseArgs)
+ where TAttribute : Attribute
+ {
+ var attribute = Subject.GetCustomAttribute<TAttribute>(false);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(attribute != null)
+ .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.",
+ Identifier, Subject, typeof(TAttribute).FullName);
+
+ return new AndWhichConstraint<ParameterInfoAssertions, TAttribute>(this, attribute);
+ }
+ }
+
+ public static class ParameterInfoAssertionExtensions
+ {
+ public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo)
+ {
+ return new ParameterInfoAssertions(parameterInfo);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs new file mode 100644 index 00000000..3f6036e3 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs @@ -0,0 +1,13 @@ +using System.Linq;
+using System.Reflection;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class ReflectionHelper
+ {
+ public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name)
+ {
+ return methodInfo.GetParameters().Where(p => p.Name == name).Single();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs new file mode 100644 index 00000000..024732f5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -0,0 +1,172 @@ +using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Formatting;
+using FluentAssertions.Primitives;
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Timeline.Models.Converters;
+using Timeline.Models.Http;
+
+namespace Timeline.Tests.Helpers
+{
+ public class HttpResponseMessageValueFormatter : IValueFormatter
+ {
+ public bool CanHandle(object value)
+ {
+ return value is HttpResponseMessage;
+ }
+
+ public string Format(object value, FormattingContext context, FormatChild formatChild)
+ {
+ string newline = context.UseLineBreaks ? Environment.NewLine : "";
+ string padding = new string('\t', context.Depth);
+
+ var res = (HttpResponseMessage)value;
+
+ var builder = new StringBuilder();
+ builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: ");
+
+ try
+ {
+ var task = res.Content.ReadAsStringAsync();
+ task.Wait();
+ var body = task.Result;
+ if (body.Length > 40)
+ {
+ body = body[0..40] + " ...";
+ }
+ builder.Append(body);
+ }
+ catch (AggregateException)
+ {
+ builder.Append("NOT A STRING.");
+ }
+
+ return builder.ToString();
+ }
+ }
+
+ public class HttpResponseMessageAssertions
+ : ReferenceTypeAssertions<HttpResponseMessage, HttpResponseMessageAssertions>
+ {
+ static HttpResponseMessageAssertions()
+ {
+ Formatter.AddFormatter(new HttpResponseMessageValueFormatter());
+ }
+
+ public HttpResponseMessageAssertions(HttpResponseMessage instance)
+ {
+ Subject = instance;
+ }
+
+ protected override string Identifier => "HttpResponseMessage";
+
+ public AndConstraint<HttpResponseMessageAssertions> HaveStatusCode(int expected, string because = "", params object[] becauseArgs)
+ {
+ return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs);
+ }
+
+ public AndConstraint<HttpResponseMessageAssertions> HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs)
+ {
+ Execute.Assertion.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.StatusCode == expected)
+ .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode);
+ return new AndConstraint<HttpResponseMessageAssertions>(this);
+ }
+
+ public AndWhichConstraint<HttpResponseMessageAssertions, T> HaveJsonBody<T>(string because = "", params object[] becauseArgs)
+ {
+ var a = Execute.Assertion.BecauseOf(because, becauseArgs);
+ string body;
+ try
+ {
+ var task = Subject.Content.ReadAsStringAsync();
+ task.Wait();
+ body = task.Result;
+ }
+ catch (AggregateException e)
+ {
+ a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions);
+ return new AndWhichConstraint<HttpResponseMessageAssertions, T>(this, null);
+ }
+
+
+ try
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ options.Converters.Add(new JsonStringEnumConverter());
+ options.Converters.Add(new JsonDateTimeConverter());
+
+ var result = JsonSerializer.Deserialize<T>(body, options);
+
+ return new AndWhichConstraint<HttpResponseMessageAssertions, T>(this, result);
+ }
+ catch (JsonException e)
+ {
+ a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to deserialize it. Exception is {0}.", e);
+ return new AndWhichConstraint<HttpResponseMessageAssertions, T>(this, null);
+ }
+ }
+ }
+
+ public static class AssertionResponseExtensions
+ {
+ public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance)
+ {
+ return new HttpResponseMessageAssertions(instance);
+ }
+
+ public static AndWhichConstraint<HttpResponseMessageAssertions, CommonResponse> HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ {
+ return assertions.HaveJsonBody<CommonResponse>(because, becauseArgs);
+ }
+
+ public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs)
+ {
+ message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs);
+ var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which;
+ body.Code.Should().Be(code, "Response body code is not the specified one{0}", message);
+ }
+
+ public static AndWhichConstraint<HttpResponseMessageAssertions, CommonDataResponse<TData>> HaveCommonDataBody<TData>(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ {
+ return assertions.HaveJsonBody<CommonDataResponse<TData>>(because, becauseArgs);
+ }
+
+ public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs)
+ {
+ var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs)
+ .And.HaveJsonBody<CommonPutResponse>(because, becauseArgs)
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Create.Should().Be(create);
+ }
+
+ public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs)
+ {
+ var body = assertions.HaveStatusCode(200, because, becauseArgs)
+ .And.HaveJsonBody<CommonDeleteResponse>(because, becauseArgs)
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Delete.Should().Be(delete);
+ }
+
+ public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null)
+ {
+ message = string.IsNullOrEmpty(message) ? "" : ", " + message;
+ assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message)
+ .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message)
+ .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel,
+ "Invalid Model Error must have code {0} in body{1}",
+ ErrorCodes.Common.InvalidModel, message);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/TestApplication.cs b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs new file mode 100644 index 00000000..684ffe2c --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Timeline.Configs;
+using Timeline.Entities;
+using Xunit;
+
+namespace Timeline.Tests.Helpers
+{
+ public class TestApplication : IAsyncLifetime
+ {
+ public TestDatabase Database { get; }
+
+ public IHost Host { get; private set; }
+
+ public string WorkDir { get; private set; }
+
+ public TestApplication()
+ {
+ Database = new TestDatabase(false);
+ }
+
+ public async Task InitializeAsync()
+ {
+ await Database.InitializeAsync();
+
+ WorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(WorkDir);
+
+ Host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
+ .ConfigureAppConfiguration((context, config) =>
+ {
+ config.AddInMemoryCollection(new Dictionary<string, string>
+ {
+ [ApplicationConfiguration.UseMockFrontEndKey] = "true",
+ ["WorkDir"] = WorkDir
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDbContext<DatabaseContext>(options =>
+ {
+ options.UseSqlite(Database.Connection);
+ });
+ })
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder
+ .UseTestServer()
+ .UseStartup<Startup>();
+ })
+ .StartAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Host.StopAsync();
+ Host.Dispose();
+
+ Directory.Delete(WorkDir, true);
+
+ await Database.DisposeAsync();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/TestClock.cs b/BackEnd/Timeline.Tests/Helpers/TestClock.cs new file mode 100644 index 00000000..34adb245 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestClock.cs @@ -0,0 +1,43 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Services;
+
+namespace Timeline.Tests.Helpers
+{
+ public class TestClock : IClock
+ {
+ private DateTime? _currentTime;
+
+ public DateTime GetCurrentTime()
+ {
+ return _currentTime ?? DateTime.UtcNow;
+ }
+
+ public void SetCurrentTime(DateTime? mockTime)
+ {
+ _currentTime = mockTime;
+ }
+
+ public DateTime SetMockCurrentTime()
+ {
+ var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc);
+ _currentTime = time;
+ return time;
+ }
+
+ public DateTime ForwardCurrentTime()
+ {
+ return ForwardCurrentTime(TimeSpan.FromDays(1));
+ }
+
+ public DateTime ForwardCurrentTime(TimeSpan timeSpan)
+ {
+ if (_currentTime == null)
+ return SetMockCurrentTime();
+ _currentTime += timeSpan;
+ return _currentTime.Value;
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs new file mode 100644 index 00000000..f0c26180 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -0,0 +1,76 @@ +using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Migrations;
+using Timeline.Models;
+using Timeline.Services;
+using Xunit;
+
+namespace Timeline.Tests.Helpers
+{
+ public class TestDatabase : IAsyncLifetime
+ {
+ private readonly bool _createUser;
+
+ public TestDatabase(bool createUser = true)
+ {
+ _createUser = createUser;
+ Connection = new SqliteConnection("Data Source=:memory:;");
+ }
+
+ public async Task InitializeAsync()
+ {
+ await Connection.OpenAsync();
+
+ using (var context = CreateContext())
+ {
+ await context.Database.EnsureCreatedAsync();
+ context.JwtToken.Add(new JwtTokenEntity
+ {
+ Key = JwtTokenGenerateHelper.GenerateKey()
+ });
+ await context.SaveChangesAsync();
+
+ if (_createUser)
+ {
+ var passwordService = new PasswordService();
+ var userService = new UserService(NullLogger<UserService>.Instance, context, passwordService, new Clock());
+
+ await userService.CreateUser(new User
+ {
+ Username = "admin",
+ Password = "adminpw",
+ Administrator = true,
+ Nickname = "administrator"
+ });
+
+ await userService.CreateUser(new User
+ {
+ Username = "user",
+ Password = "userpw",
+ Administrator = false,
+ Nickname = "imuser"
+ });
+ }
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Connection.CloseAsync();
+ await Connection.DisposeAsync();
+ }
+
+ public SqliteConnection Connection { get; }
+
+ public DatabaseContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder<DatabaseContext>()
+ .UseSqlite(Connection).Options;
+
+ return new DatabaseContext(options);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..38071394 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions;
+using System.Net;
+using System.Threading.Tasks;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class AuthorizationTest : IntegratedTestBase
+ {
+ private const string BaseUrl = "testing/auth/";
+ private const string AuthorizeUrl = BaseUrl + "Authorize";
+ private const string UserUrl = BaseUrl + "User";
+ private const string AdminUrl = BaseUrl + "Admin";
+
+ [Fact]
+ public async Task UnauthenticationTest()
+ {
+ using var client = await CreateDefaultClient();
+ var response = await client.GetAsync(AuthorizeUrl);
+ response.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task AuthenticationTest()
+ {
+ using var client = await CreateClientAsUser();
+ var response = await client.GetAsync(AuthorizeUrl);
+ response.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task UserAuthorizationTest()
+ {
+ using var client = await CreateClientAsUser();
+ var response1 = await client.GetAsync(UserUrl);
+ response1.Should().HaveStatusCode(HttpStatusCode.OK);
+ var response2 = await client.GetAsync(AdminUrl);
+ response2.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task AdminAuthorizationTest()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var response1 = await client.GetAsync(UserUrl);
+ response1.Should().HaveStatusCode(HttpStatusCode.OK);
+ var response2 = await client.GetAsync(AdminUrl);
+ response2.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs new file mode 100644 index 00000000..39a6e545 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs @@ -0,0 +1,29 @@ +using FluentAssertions;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class FrontEndTest : IntegratedTestBase
+ {
+ [Fact]
+ public async Task Index()
+ {
+ using var client = await CreateDefaultClient(false);
+ var res = await client.GetAsync("index.html");
+ res.Should().HaveStatusCode(200);
+ res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html);
+ }
+
+ [Fact]
+ public async Task Fallback()
+ {
+ using var client = await CreateDefaultClient(false);
+ var res = await client.GetAsync("aaaaaaaaaaaaaaa");
+ res.Should().HaveStatusCode(200);
+ res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs new file mode 100644 index 00000000..7cf27297 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -0,0 +1,164 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Models.Converters;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public abstract class IntegratedTestBase : IAsyncLifetime
+ {
+ protected TestApplication TestApp { get; }
+
+ public IReadOnlyList<UserInfo> UserInfos { get; private set; }
+
+ private readonly int _userCount;
+
+ public IntegratedTestBase() : this(1)
+ {
+
+ }
+
+ public IntegratedTestBase(int userCount)
+ {
+ if (userCount < 0)
+ throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative.");
+
+ _userCount = userCount;
+
+ TestApp = new TestApplication();
+ }
+
+ protected virtual Task OnInitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual Task OnDisposeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual void OnDispose()
+ {
+
+ }
+
+ public async Task InitializeAsync()
+ {
+ await TestApp.InitializeAsync();
+
+ using (var scope = TestApp.Host.Services.CreateScope())
+ {
+ var users = new List<User>()
+ {
+ new User
+ {
+ Username = "admin",
+ Password = "adminpw",
+ Administrator = true,
+ Nickname = "administrator"
+ }
+ };
+
+ for (int i = 1; i <= _userCount; i++)
+ {
+ users.Add(new User
+ {
+ Username = $"user{i}",
+ Password = $"user{i}pw",
+ Administrator = false,
+ Nickname = $"imuser{i}"
+ });
+ }
+
+ var userInfoList = new List<UserInfo>();
+
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ foreach (var user in users)
+ {
+ await userService.CreateUser(user);
+ }
+
+ using var client = await CreateDefaultClient();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ options.Converters.Add(new JsonStringEnumConverter());
+ options.Converters.Add(new JsonDateTimeConverter());
+ foreach (var user in users)
+ {
+ var s = await client.GetStringAsync($"users/{user.Username}");
+ userInfoList.Add(JsonSerializer.Deserialize<UserInfo>(s, options));
+ }
+
+ UserInfos = userInfoList;
+ }
+
+ await OnInitializeAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await OnDisposeAsync();
+ OnDispose();
+ await TestApp.DisposeAsync();
+ }
+
+ public Task<HttpClient> CreateDefaultClient(bool setApiBase = true)
+ {
+ var client = TestApp.Host.GetTestServer().CreateClient();
+ if (setApiBase)
+ {
+ client.BaseAddress = new Uri(client.BaseAddress, "api/");
+ }
+ return Task.FromResult(client);
+ }
+
+ public async Task<HttpClient> CreateClientWithCredential(string username, string password, bool setApiBase = true)
+ {
+ var client = TestApp.Host.GetTestServer().CreateClient();
+ if (setApiBase)
+ {
+ client.BaseAddress = new Uri(client.BaseAddress, "api/");
+ }
+ var response = await client.PostAsJsonAsync("token/create",
+ new CreateTokenRequest { Username = username, Password = password });
+ var token = response.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<CreateTokenResponse>().Which.Token;
+ client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
+ return client;
+ }
+
+ public Task<HttpClient> CreateClientAs(int userNumber, bool setApiBase = true)
+ {
+ if (userNumber < 0)
+ return CreateDefaultClient(setApiBase);
+ if (userNumber == 0)
+ return CreateClientWithCredential("admin", "adminpw", setApiBase);
+ else
+ return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase);
+ }
+
+ public Task<HttpClient> CreateClientAsAdministrator(bool setApiBase = true)
+ {
+ return CreateClientAs(0, setApiBase);
+ }
+
+ public Task<HttpClient> CreateClientAsUser(bool setApiBase = true)
+ {
+ return CreateClientAs(1, setApiBase);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs new file mode 100644 index 00000000..ec46b96a --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -0,0 +1,1523 @@ +using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public static class TimelineHelper
+ {
+ public static TimelinePostContentInfo TextPostContent(string text)
+ {
+ return new TimelinePostContentInfo
+ {
+ Type = "text",
+ Text = text
+ };
+ }
+
+ public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null)
+ {
+ return new TimelinePostCreateRequest
+ {
+ Content = new TimelinePostCreateRequestContent
+ {
+ Type = "text",
+ Text = text
+ },
+ Time = time
+ };
+ }
+ }
+
+ public class TimelineTest : IntegratedTestBase
+ {
+ public TimelineTest() : base(3)
+ {
+ }
+
+ protected override async Task OnInitializeAsync()
+ {
+ await CreateTestTimelines();
+ }
+
+ private List<TimelineInfo> _testTimelines;
+
+ private async Task CreateTestTimelines()
+ {
+ _testTimelines = new List<TimelineInfo>();
+ for (int i = 0; i <= 3; i++)
+ {
+ var client = await CreateClientAs(i);
+ var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = $"t{i}" });
+ var timelineInfo = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ _testTimelines.Add(timelineInfo);
+ }
+ }
+
+ private static string CalculateUrlTail(string subpath, ICollection<KeyValuePair<string, string>> query)
+ {
+ StringBuilder result = new StringBuilder();
+ if (subpath != null)
+ {
+ if (!subpath.StartsWith("/", StringComparison.OrdinalIgnoreCase))
+ result.Append('/');
+ result.Append(subpath);
+ }
+
+ if (query != null && query.Count != 0)
+ {
+ result.Append('?');
+ foreach (var (key, value, index) in query.Select((pair, index) => (pair.Key, pair.Value, index)))
+ {
+ result.Append(WebUtility.UrlEncode(key));
+ result.Append('=');
+ result.Append(WebUtility.UrlEncode(value));
+ if (index != query.Count - 1)
+ result.Append('&');
+ }
+ }
+
+ return result.ToString();
+ }
+
+ private static string GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/t{id}{CalculateUrlTail(subpath, query)}";
+ }
+
+ public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection<KeyValuePair<string, string>> query = null);
+
+ public static IEnumerable<object[]> TimelineUrlGeneratorData()
+ {
+ yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) };
+ yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) };
+ }
+
+ private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null)
+ {
+ return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null)
+ {
+ return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}";
+ }
+
+ public static IEnumerable<object[]> TimelineUrlByNameGeneratorData()
+ {
+ yield return new[] { new Func<string, string, string>(GeneratePersonalTimelineUrlByName) };
+ yield return new[] { new Func<string, string, string>(GenerateOrdinaryTimelineUrlByName) };
+ }
+
+ [Fact]
+ public async Task TimelineGet_Should_Work()
+ {
+ using var client = await CreateDefaultClient();
+ {
+ var res = await client.GetAsync("timelines/@user1");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ body.Owner.Should().BeEquivalentTo(UserInfos[1]);
+ body.Visibility.Should().Be(TimelineVisibility.Register);
+ body.Description.Should().Be("");
+ body.Members.Should().NotBeNull().And.BeEmpty();
+ var links = body._links;
+ links.Should().NotBeNull();
+ links.Self.Should().EndWith("timelines/@user1");
+ links.Posts.Should().EndWith("timelines/@user1/posts");
+ }
+
+ {
+ var res = await client.GetAsync("timelines/t1");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ body.Owner.Should().BeEquivalentTo(UserInfos[1]);
+ body.Visibility.Should().Be(TimelineVisibility.Register);
+ body.Description.Should().Be("");
+ body.Members.Should().NotBeNull().And.BeEmpty();
+ var links = body._links;
+ links.Should().NotBeNull();
+ links.Self.Should().EndWith("timelines/t1");
+ links.Posts.Should().EndWith("timelines/t1/posts");
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList()
+ {
+ TimelineInfo user1Timeline;
+
+ var client = await CreateDefaultClient();
+
+ {
+ var res = await client.GetAsync("timelines/@user1");
+ user1Timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ }
+
+ {
+ var testResult = new List<TimelineInfo>();
+ testResult.Add(user1Timeline);
+ testResult.AddRange(_testTimelines);
+
+ var res = await client.GetAsync("timelines");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which.Should().BeEquivalentTo(testResult);
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList_WithQuery()
+ {
+ var testResultRelate = new List<TimelineInfo>();
+ var testResultOwn = new List<TimelineInfo>();
+ var testResultJoin = new List<TimelineInfo>();
+ var testResultOwnPrivate = new List<TimelineInfo>();
+ var testResultRelatePublic = new List<TimelineInfo>();
+ var testResultRelateRegister = new List<TimelineInfo>();
+ var testResultJoinPrivate = new List<TimelineInfo>();
+ var testResultPublic = new List<TimelineInfo>();
+
+ {
+ var client = await CreateClientAsUser();
+
+ {
+ var res = await client.PutAsync("timelines/@user1/members/user3", null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PutAsync("timelines/t1/members/user3", null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/@user1");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultJoin.Add(timeline);
+ testResultRelatePublic.Add(timeline);
+ testResultPublic.Add(timeline);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/t1");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultJoin.Add(timeline);
+ testResultRelateRegister.Add(timeline);
+ }
+ }
+
+ {
+ var client = await CreateClientAs(2);
+
+ {
+ var res = await client.PutAsync("timelines/@user2/members/user3", null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PutAsync("timelines/t2/members/user3", null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/t2", new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/@user2");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultJoin.Add(timeline);
+ testResultRelateRegister.Add(timeline);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/t2");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultJoin.Add(timeline);
+ testResultJoinPrivate.Add(timeline);
+ }
+ }
+
+ {
+ var client = await CreateClientAs(3);
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync("timelines/t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/@user3");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultOwn.Add(timeline);
+ testResultOwnPrivate.Add(timeline);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/t3");
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ testResultRelate.Add(timeline);
+ testResultOwn.Add(timeline);
+ testResultRelateRegister.Add(timeline);
+ }
+ }
+
+ {
+ var client = await CreateClientAs(3);
+ {
+ var res = await client.GetAsync("timelines?relate=user3");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelate);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&relateType=own");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultOwn);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=public");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelatePublic);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=register");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelateRegister);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&relateType=join&visibility=private");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultJoinPrivate);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&relateType=own&visibility=private");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultOwnPrivate);
+ }
+ }
+
+ {
+ var client = await CreateDefaultClient();
+ {
+ var res = await client.GetAsync("timelines?visibility=public");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultPublic);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList_InvalidModel()
+ {
+ var client = await CreateClientAsUser();
+
+ {
+ var res = await client.GetAsync("timelines?relate=us!!");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relateType=aaa");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.GetAsync("timelines?visibility=aaa");
+ res.Should().BeInvalidModel();
+ }
+ }
+
+ [Fact]
+ public async Task TimelineCreate_Should_Work()
+ {
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ using (var client = await CreateClientAsUser())
+ {
+ {
+ var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "!!!" });
+ res.Should().BeInvalidModel();
+ }
+
+ TimelineInfo timelineInfo;
+ {
+ var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" });
+ timelineInfo = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ }
+
+ {
+ var res = await client.GetAsync("timelines/aaa");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timelineInfo);
+ }
+
+ {
+ var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task TimelineDelete_Should_Work()
+ {
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.DeleteAsync("timelines/t1");
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ {
+ using var client = await CreateClientAs(2);
+ var res = await client.DeleteAsync("timelines/t1");
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ {
+ using var client = await CreateClientAsAdministrator();
+
+ {
+ var res = await client.DeleteAsync("timelines/!!!");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.DeleteAsync("timelines/t2");
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.DeleteAsync("timelines/t2");
+ res.Should().BeDelete(false);
+ }
+ }
+
+ {
+ using var client = await CreateClientAs(1);
+
+ {
+ var res = await client.DeleteAsync("timelines/!!!");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.DeleteAsync("timelines/t1");
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.DeleteAsync("timelines/t1");
+ res.Should().HaveStatusCode(400);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlByNameGeneratorData))]
+ public async Task InvalidModel_BadName(Func<string, string, string> generator)
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.GetAsync(generator("aaa!!!", null));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { });
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null);
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.DeleteAsync(generator("aaa!!!", "members/user1"));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.GetAsync(generator("aaa!!!", "posts"));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.DeleteAsync(generator("aaa!!!", "posts/123"));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.GetAsync(generator("aaa!!!", "posts/123/data"));
+ res.Should().BeInvalidModel();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlByNameGeneratorData))]
+ public async Task Ordinary_NotFound(Func<string, string, string> generator)
+ {
+ var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist;
+
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.GetAsync(generator("notexist", null));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { });
+ res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.PutAsync(generator("notexist", "members/user1"), null);
+ res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.DeleteAsync(generator("notexist", "members/user1"));
+ res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.GetAsync(generator("notexist", "posts"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.DeleteAsync(generator("notexist", "posts/123"));
+ res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.GetAsync(generator("notexist", "posts/123/data"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Description_Should_Work(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ async Task AssertDescription(string description)
+ {
+ var res = await client.GetAsync(generator(1, null));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Description.Should().Be(description);
+ }
+
+ const string mockDescription = "haha";
+
+ await AssertDescription("");
+ {
+ var res = await client.PatchAsJsonAsync(generator(1, null),
+ new TimelinePatchRequest { Description = mockDescription });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
+ await AssertDescription(mockDescription);
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator(1, null),
+ new TimelinePatchRequest { Description = null });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
+ await AssertDescription(mockDescription);
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator(1, null),
+ new TimelinePatchRequest { Description = "" });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be("");
+ await AssertDescription("");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Member_Should_Work(TimelineUrlGenerator generator)
+ {
+ var getUrl = generator(1, null);
+ using var client = await CreateClientAsUser();
+
+ async Task AssertMembers(IList<UserInfo> members)
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members);
+ }
+
+ async Task AssertEmptyMembers()
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Members.Should().NotBeNull().And.BeEmpty();
+ }
+
+ await AssertEmptyMembers();
+ {
+ var res = await client.PutAsync(generator(1, "members/usernotexist"), null);
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist);
+ }
+ await AssertEmptyMembers();
+ {
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
+ res.Should().HaveStatusCode(200);
+ }
+ await AssertMembers(new List<UserInfo> { UserInfos[2] });
+ {
+ var res = await client.DeleteAsync(generator(1, "members/user2"));
+ res.Should().BeDelete(true);
+ }
+ await AssertEmptyMembers();
+ {
+ var res = await client.DeleteAsync(generator(1, "members/aaa"));
+ res.Should().BeDelete(false);
+ }
+ await AssertEmptyMembers();
+ }
+
+ public static IEnumerable<object[]> Permission_Timeline_Data()
+ {
+ yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401 };
+ yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403 };
+ yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200 };
+ yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401 };
+ yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403 };
+ yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200 };
+ }
+
+ [Theory]
+ [MemberData(nameof(Permission_Timeline_Data))]
+ public async Task Permission_Timeline(TimelineUrlGenerator generator, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin)
+ {
+ using var client = await CreateClientAs(userNumber);
+ {
+ var res = await client.GetAsync("timelines/t1");
+ res.Should().HaveStatusCode(get);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" });
+ res.Should().HaveStatusCode(opPatchUser);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" });
+ res.Should().HaveStatusCode(opPatchAdmin);
+ }
+
+ {
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
+ res.Should().HaveStatusCode(opMemberUser);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, "members/user2"));
+ res.Should().HaveStatusCode(opMemberUser);
+ }
+
+ {
+ var res = await client.PutAsync(generator(0, "members/user2"), null);
+ res.Should().HaveStatusCode(opMemberAdmin);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(0, "members/user2"));
+ res.Should().HaveStatusCode(opMemberAdmin);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Visibility_Test(TimelineUrlGenerator generator)
+ {
+ var userUrl = generator(1, "posts");
+ var adminUrl = generator(0, "posts");
+ {
+
+ using var client = await CreateClientAsUser();
+ using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json);
+ var res = await client.PatchAsync(generator(1, null), content);
+ res.Should().BeInvalidModel();
+ }
+ { // default visibility is registered
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync(userUrl);
+ res.Should().HaveStatusCode(403);
+ }
+
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync(adminUrl);
+ res.Should().HaveStatusCode(200);
+ }
+ }
+
+ { // change visibility to public
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync(generator(1, null),
+ new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync(userUrl);
+ res.Should().HaveStatusCode(200);
+ }
+ }
+
+ { // change visibility to private
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PatchAsJsonAsync(generator(1, null),
+ new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator(0, null),
+ new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ res.Should().HaveStatusCode(200);
+ }
+ }
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync(userUrl);
+ res.Should().HaveStatusCode(403);
+ }
+ { // user can't read admin's
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync(adminUrl);
+ res.Should().HaveStatusCode(403);
+ }
+ { // admin can read user's
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.GetAsync(userUrl);
+ res.Should().HaveStatusCode(200);
+ }
+ { // add member
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PutAsync(generator(0, "members/user1"), null);
+ res.Should().HaveStatusCode(200);
+ }
+ { // now user can read admin's
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync(adminUrl);
+ res.Should().HaveStatusCode(200);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Permission_Post_Create(TimelineUrlGenerator generator)
+ {
+ using (var client = await CreateClientAsUser())
+ {
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ using (var client = await CreateDefaultClient())
+ {
+ { // no auth should get 401
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(401);
+ }
+ }
+
+ using (var client = await CreateClientAsUser())
+ {
+ { // post self's
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(200);
+ }
+ { // post other not as a member should get 403
+ var res = await client.PostAsJsonAsync(generator(0, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(403);
+ }
+ }
+
+ using (var client = await CreateClientAsAdministrator())
+ {
+ { // post as admin
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(200);
+ }
+ }
+
+ using (var client = await CreateClientAs(2))
+ {
+ { // post as member
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(200);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Permission_Post_Delete(TimelineUrlGenerator generator)
+ {
+ async Task<long> CreatePost(int userNumber)
+ {
+ using var client = await CreateClientAs(userNumber);
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ return res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which.Id;
+ }
+
+ using (var client = await CreateClientAsUser())
+ {
+ {
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ var res = await client.PutAsync(generator(1, "members/user3"), null);
+ res.Should().HaveStatusCode(200);
+ }
+ }
+
+ { // no auth should get 401
+ using var client = await CreateDefaultClient();
+ var res = await client.DeleteAsync(generator(1, "posts/12"));
+ res.Should().HaveStatusCode(401);
+ }
+
+ { // self can delete self
+ var postId = await CreatePost(1);
+ using var client = await CreateClientAsUser();
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().HaveStatusCode(200);
+ }
+
+ { // admin can delete any
+ var postId = await CreatePost(1);
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().HaveStatusCode(200);
+ }
+
+ { // owner can delete other
+ var postId = await CreatePost(2);
+ using var client = await CreateClientAsUser();
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().HaveStatusCode(200);
+ }
+
+ { // author can delete self
+ var postId = await CreatePost(2);
+ using var client = await CreateClientAs(2);
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().HaveStatusCode(200);
+ }
+
+ { // otherwise is forbidden
+ var postId = await CreatePost(2);
+ using var client = await CreateClientAs(3);
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().HaveStatusCode(403);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task TextPost_ShouldWork(TimelineUrlGenerator generator)
+ {
+ {
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().NotBeNull().And.BeEmpty();
+ }
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(null));
+ res.Should().BeInvalidModel();
+ }
+ const string mockContent = "aaa";
+ TimelinePostInfo createRes;
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(mockContent));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which;
+ body.Should().NotBeNull();
+ body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent));
+ body.Author.Should().BeEquivalentTo(UserInfos[1]);
+ body.Deleted.Should().BeFalse();
+ createRes = body;
+ }
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().NotBeNull().And.BeEquivalentTo(createRes);
+ }
+ const string mockContent2 = "bbb";
+ var mockTime2 = DateTime.UtcNow.AddDays(-1);
+ TimelinePostInfo createRes2;
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which;
+ body.Should().NotBeNull();
+ body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2));
+ body.Author.Should().BeEquivalentTo(UserInfos[1]);
+ body.Time.Should().BeCloseTo(mockTime2, 1000);
+ body.Deleted.Should().BeFalse();
+ createRes2 = body;
+ }
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2);
+ }
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}"));
+ res.Should().BeDelete(true);
+ }
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}"));
+ res.Should().BeDelete(false);
+ }
+ {
+ var res = await client.DeleteAsync(generator(1, "posts/30000"));
+ res.Should().BeDelete(false);
+ }
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ async Task<long> CreatePost(DateTime time)
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa", time));
+ return res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which.Id;
+ }
+
+ var now = DateTime.UtcNow;
+ var id0 = await CreatePost(now.AddDays(1));
+ var id1 = await CreatePost(now.AddDays(-1));
+ var id2 = await CreatePost(now);
+
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Select(p => p.Id).Should().Equal(id1, id2, id0);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ // image not base64
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ // image base64 not image
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } });
+ res.Should().BeInvalidModel();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator)
+ {
+ var imageData = ImageHelper.CreatePngWithSize(100, 200);
+
+ long postId;
+ string postImageUrl;
+
+ void AssertPostContent(TimelinePostContentInfo content)
+ {
+ content.Type.Should().Be(TimelinePostContentTypes.Image);
+ content.Url.Should().EndWith(generator(1, $"posts/{postId}/data"));
+ content.Text.Should().Be(null);
+ }
+
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ new TimelinePostCreateRequest
+ {
+ Content = new TimelinePostCreateRequestContent
+ {
+ Type = TimelinePostContentTypes.Image,
+ Data = Convert.ToBase64String(imageData)
+ }
+ });
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ postId = body.Id;
+ postImageUrl = body.Content.Url;
+ AssertPostContent(body.Content);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>().Which;
+ body.Should().HaveCount(1);
+ var post = body[0];
+ post.Id.Should().Be(postId);
+ AssertPostContent(post.Content);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, $"posts/{postId}/data"));
+ res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
+ var data = await res.Content.ReadAsByteArrayAsync();
+ var image = Image.Load(data, out var format);
+ image.Width.Should().Be(100);
+ image.Height.Should().Be(200);
+ format.Name.Should().Be(PngFormat.Instance.Name);
+ }
+
+ {
+ await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data"));
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().BeDelete(false);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().BeEmpty();
+ }
+
+ {
+ using var scope = TestApp.Host.Services.CreateScope();
+ var database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
+ var count = await database.Data.CountAsync();
+ count.Should().Be(0);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task ImagePost_400(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.GetAsync(generator(1, "posts/11234/data"));
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist);
+ }
+
+ long postId;
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which;
+ postId = body.Id;
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, $"posts/{postId}/data"));
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Timeline_LastModified(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ DateTime lastModified;
+
+ {
+ var res = await client.GetAsync(generator(1));
+ lastModified = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified;
+ }
+
+ await Task.Delay(1000);
+
+ {
+ var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" });
+ lastModified = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value;
+ }
+
+ {
+ var res = await client.GetAsync(generator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified.Should().Be(lastModified);
+ }
+
+ await Task.Delay(1000);
+
+ {
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified.Should().BeAfter(lastModified);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Post_ModifiedSince(TimelineUrlGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var content in postContentList)
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ posts.Add(post);
+ await Task.Delay(1000);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts",
+ new Dictionary<string, string> { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which.Should().HaveCount(2)
+ .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var content in postContentList)
+ {
+ var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ posts.Add(res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which);
+ }
+
+ foreach (var id in new long[] { posts[0].Id, posts[2].Id })
+ {
+ var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary<string, string> { ["includeDeleted"] = "true" }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which;
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(true, false, true, false);
+ posts.Select(p => p.Content == null).Should().Equal(true, false, true, false);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ posts.Add(post);
+ await Task.Delay(1000);
+ }
+
+ {
+ var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+
+ var res = await client.GetAsync(urlGenerator(1, "posts",
+ new Dictionary<string, string> {
+ { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) },
+ { "includeDeleted", "true" }
+ }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which;
+ posts.Should().HaveCount(3);
+ posts.Select(p => p.Deleted).Should().Equal(false, true, false);
+ posts.Select(p => p.Content == null).Should().Equal(false, true, false);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ DateTime lastModifiedTime;
+ TimelineInfo timeline;
+ string uniqueId;
+
+ {
+ var res = await client.GetAsync(urlGenerator(1));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ timeline = body;
+ lastModifiedTime = body.LastModified;
+ uniqueId = body.UniqueId;
+ }
+
+ {
+ using var req = new HttpRequestMessage
+ {
+ RequestUri = new Uri(client.BaseAddress, urlGenerator(1)),
+ Method = HttpMethod.Get,
+ };
+ req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(1);
+ var res = await client.SendAsync(req);
+ res.Should().HaveStatusCode(304);
+ }
+
+ {
+ using var req = new HttpRequestMessage
+ {
+ RequestUri = new Uri(client.BaseAddress, urlGenerator(1)),
+ Method = HttpMethod.Get,
+ };
+ req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1);
+ var res = await client.SendAsync(req);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(304);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) },
+ {"checkUniqueId", uniqueId } }));
+ res.Should().HaveStatusCode(304);
+ }
+
+ {
+ var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..];
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) },
+ {"checkUniqueId", testUniqueId } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Title(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.GetAsync(urlGenerator(1));
+ var timeline = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which;
+ timeline.Title.Should().Be(timeline.Name);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Title.Should().Be("atitle");
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Title.Should().Be("atitle");
+ }
+ }
+
+ [Fact]
+ public async Task ChangeName()
+ {
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ res.Should().HaveStatusCode(401);
+ }
+
+ {
+ using var client = await CreateClientAs(2);
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ res.Should().HaveStatusCode(403);
+ }
+
+ using (var client = await CreateClientAsUser())
+ {
+ {
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" });
+ res.Should().HaveStatusCode(400).And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.TimelineController.NotExist);
+ }
+
+ {
+ var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" });
+ res.Should().HaveStatusCode(200).And.HaveJsonBody<TimelineInfo>().Which.Name.Should().Be("newt");
+ }
+
+ {
+ var res = await client.GetAsync("timelines/t1");
+ res.Should().HaveStatusCode(404);
+ }
+
+ {
+ var res = await client.GetAsync("timelines/newt");
+ res.Should().HaveStatusCode(200).And.HaveJsonBody<TimelineInfo>().Which.Name.Should().Be("newt");
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task PostDataETag(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ long id;
+ string etag;
+
+ {
+ var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), new TimelinePostCreateRequest
+ {
+ Content = new TimelinePostCreateRequestContent
+ {
+ Type = TimelinePostContentTypes.Image,
+ Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50))
+ }
+ });
+ res.Should().HaveStatusCode(200);
+ var body = await res.ReadBodyAsJsonAsync<TimelinePostInfo>();
+ body.Content.ETag.Should().NotBeNullOrEmpty();
+
+ id = body.Id;
+ etag = body.Content.ETag;
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, $"posts/{id}/data"));
+ res.Should().HaveStatusCode(200);
+ res.Headers.ETag.Should().NotBeNull();
+ res.Headers.ETag.ToString().Should().Be(etag);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..480d66cd --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,165 @@ +using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class TokenTest : IntegratedTestBase
+ {
+ private const string CreateTokenUrl = "token/create";
+ private const string VerifyTokenUrl = "token/verify";
+
+ private static async Task<CreateTokenResponse> CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null)
+ {
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset });
+ return response.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<CreateTokenResponse>().Which;
+ }
+
+ public static IEnumerable<object[]> CreateToken_InvalidModel_Data()
+ {
+ yield return new[] { null, "p", null };
+ yield return new[] { "u", null, null };
+ yield return new object[] { "u", "p", 2000 };
+ yield return new object[] { "u", "p", -1 };
+ }
+
+ [Theory]
+ [MemberData(nameof(CreateToken_InvalidModel_Data))]
+ public async Task CreateToken_InvalidModel(string username, string password, int expire)
+ {
+ using var client = await CreateDefaultClient();
+ (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest
+ {
+ Username = username,
+ Password = password,
+ Expire = expire
+ })).Should().BeInvalidModel();
+ }
+
+ public static IEnumerable<object[]> CreateToken_UserCredential_Data()
+ {
+ yield return new[] { "usernotexist", "p" };
+ yield return new[] { "user1", "???" };
+ }
+
+ [Theory]
+ [MemberData(nameof(CreateToken_UserCredential_Data))]
+ public async void CreateToken_UserCredential(string username, string password)
+ {
+ using var client = await CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(CreateTokenUrl,
+ new CreateTokenRequest { Username = username, Password = password });
+ response.Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential);
+ }
+
+ [Fact]
+ public async Task CreateToken_Success()
+ {
+ using var client = await CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(CreateTokenUrl,
+ new CreateTokenRequest { Username = "user1", Password = "user1pw" });
+ var body = response.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<CreateTokenResponse>().Which;
+ body.Token.Should().NotBeNullOrWhiteSpace();
+ body.User.Should().BeEquivalentTo(UserInfos[1]);
+ }
+
+ [Fact]
+ public async Task VerifyToken_InvalidModel()
+ {
+ using var client = await CreateDefaultClient();
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = null })).Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task VerifyToken_BadFormat()
+ {
+ using var client = await CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = "bad token hahaha" });
+ response.Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat);
+ }
+
+ [Fact]
+ public async Task VerifyToken_OldVersion()
+ {
+ using var client = await CreateDefaultClient();
+ var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token;
+
+ using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped.
+ {
+ // create a user for test
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ await userService.ModifyUser("user1", new User { Password = "user1pw" });
+ }
+
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = token }))
+ .Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion);
+ }
+
+ [Fact]
+ public async Task VerifyToken_UserNotExist()
+ {
+ using var client = await CreateDefaultClient();
+ var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token;
+
+ using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped.
+ {
+ var userService = scope.ServiceProvider.GetRequiredService<IUserDeleteService>();
+ await userService.DeleteUser("user1");
+ }
+
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = token }))
+ .Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist);
+ }
+
+ //[Fact]
+ //public async Task VerifyToken_Expired()
+ //{
+ // using (var client = await CreateClientWithNoAuth())
+ // {
+ // // I can only control the token expired time but not current time
+ // // because verify logic is encapsuled in other library.
+ // var mockClock = _factory.GetTestClock();
+ // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2);
+ // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token;
+ // var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ // new VerifyTokenRequest { Token = token });
+ // response.Should().HaveStatusCodeBadRequest()
+ // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired);
+ // mockClock.MockCurrentTime = null;
+ // }
+ //}
+
+ [Fact]
+ public async Task VerifyToken_Success()
+ {
+ using var client = await CreateDefaultClient();
+ var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw");
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = createTokenResult.Token });
+ response.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<VerifyTokenResponse>()
+ .Which.User.Should().BeEquivalentTo(UserInfos[1]);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs new file mode 100644 index 00000000..732232e2 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs @@ -0,0 +1,21 @@ +using FluentAssertions;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class UnknownEndpointTest : IntegratedTestBase
+ {
+ [Fact]
+ public async Task UnknownEndpoint()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync("unknownEndpoint");
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.Common.UnknownEndpoint);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs new file mode 100644 index 00000000..f2796005 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -0,0 +1,251 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Gif;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Png;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class UserAvatarTest : IntegratedTestBase
+ {
+ [Fact]
+ public async Task Test()
+ {
+ Avatar mockAvatar = new Avatar
+ {
+ Data = ImageHelper.CreatePngWithSize(100, 100),
+ Type = PngFormat.Instance.DefaultMimeType
+ };
+
+ using (var client = await CreateClientAsUser())
+ {
+ {
+ var res = await client.GetAsync("users/usernotexist/avatar");
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ }
+
+ var env = TestApp.Host.Services.GetRequiredService<IWebHostEnvironment>();
+ var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png"));
+
+ async Task GetReturnDefault(string username = "user1")
+ {
+ var res = await client.GetAsync($"users/{username}/avatar");
+ res.Should().HaveStatusCode(200);
+ res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
+ var body = await res.Content.ReadAsByteArrayAsync();
+ body.Should().Equal(defaultAvatarData);
+ }
+
+ {
+ var res = await client.GetAsync("users/user1/avatar");
+ res.Should().HaveStatusCode(200);
+ res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
+ var body = await res.Content.ReadAsByteArrayAsync();
+ body.Should().Equal(defaultAvatarData);
+ }
+
+ await CacheTestHelper.TestCache(client, "users/user1/avatar");
+
+ await GetReturnDefault("admin");
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00 });
+ content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00 });
+ content.Headers.ContentLength = 1;
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00 });
+ content.Headers.ContentLength = 0;
+ content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept");
+ res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType);
+ }
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00 });
+ content.Headers.ContentLength = 1000 * 1000 * 11;
+ content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig);
+ }
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00 });
+ content.Headers.ContentLength = 2;
+ content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 });
+ content.Headers.ContentLength = 1;
+ content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
+ var res = await client.PutAsync("users/user1/avatar", content);
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png");
+ res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode);
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg");
+ res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat);
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png");
+ res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize);
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type);
+ res.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ var res2 = await client.GetAsync("users/user1/avatar");
+ res2.Should().HaveStatusCode(200);
+ res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type);
+ var body = await res2.Content.ReadAsByteArrayAsync();
+ body.Should().Equal(mockAvatar.Data);
+ }
+
+ IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[]
+ {
+ ("image/jpeg", JpegFormat.Instance),
+ ("image/gif", GifFormat.Instance),
+ ("image/png", PngFormat.Instance),
+ };
+
+ foreach ((var mimeType, var format) in formats)
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType);
+ res.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png");
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid);
+ }
+
+ {
+ var res = await client.DeleteAsync("users/admin/avatar");
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid);
+ }
+
+ for (int i = 0; i < 2; i++) // double delete should work.
+ {
+ var res = await client.DeleteAsync("users/user1/avatar");
+ res.Should().HaveStatusCode(200);
+ await GetReturnDefault();
+ }
+ }
+
+ // Authorization check.
+ using (var client = await CreateClientAsAdministrator())
+ {
+ {
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type);
+ res.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ {
+ var res = await client.DeleteAsync("users/user1/avatar");
+ res.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png");
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ }
+
+ {
+ var res = await client.DeleteAsync("users/usernotexist/avatar");
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ }
+ }
+
+ // bad username check
+ using (var client = await CreateClientAsAdministrator())
+ {
+ {
+ var res = await client.GetAsync("users/u!ser/avatar");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.DeleteAsync("users/u!ser/avatar");
+ res.Should().BeInvalidModel();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task AvatarPutReturnETag()
+ {
+ using var client = await CreateClientAsUser();
+
+ EntityTagHeaderValue etag;
+
+ {
+ var image = ImageHelper.CreatePngWithSize(100, 100);
+ var res = await client.PutByteArrayAsync("users/user1/avatar", image, PngFormat.Instance.DefaultMimeType);
+ res.Should().HaveStatusCode(200);
+ etag = res.Headers.ETag;
+ etag.Should().NotBeNull();
+ etag.Tag.Should().NotBeNullOrEmpty();
+ }
+
+ {
+ var res = await client.GetAsync("users/user1/avatar");
+ res.Should().HaveStatusCode(200);
+ res.Headers.ETag.Should().Be(etag);
+ res.Headers.ETag.Tag.Should().Be(etag.Tag);
+ }
+ }
+ }
+}
\ No newline at end of file diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..9dfcc6a5 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,447 @@ +using FluentAssertions;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class UserTest : IntegratedTestBase
+ {
+ [Fact]
+ public void UserListShouldHaveUniqueId()
+ {
+ foreach (var user in UserInfos)
+ {
+ user.UniqueId.Should().NotBeNullOrWhiteSpace();
+ }
+ }
+
+ [Fact]
+ public async Task GetList_NoAuth()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync("users");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfos);
+ }
+
+ [Fact]
+ public async Task GetList_User()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("users");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfos);
+ }
+
+ [Fact]
+ public async Task GetList_Admin()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.GetAsync("users");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfos);
+ }
+
+ [Fact]
+ public async Task Get_NoAuth()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync($"users/admin");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfos[0]);
+ }
+
+ [Fact]
+ public async Task Get_User()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync($"users/admin");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfos[0]);
+ }
+
+ [Fact]
+ public async Task Get_Admin()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.GetAsync($"users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfos[1]);
+ }
+
+ [Fact]
+ public async Task Get_InvalidModel()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("users/aaa!a");
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Get_404()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("users/usernotexist");
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
+ }
+
+ [Fact]
+ public async Task Patch_User()
+ {
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.PatchAsJsonAsync("users/user1",
+ new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Nickname.Should().Be("aaa");
+ }
+
+ {
+ var res = await client.GetAsync("users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Nickname.Should().Be("aaa");
+ }
+ }
+
+ [Fact]
+ public async Task Patch_Admin()
+ {
+ using var client = await CreateClientAsAdministrator();
+ using var userClient = await CreateClientAsUser();
+
+ {
+ var res = await client.PatchAsJsonAsync("users/user1",
+ new UserPatchRequest
+ {
+ Username = "newuser",
+ Password = "newpw",
+ Administrator = true,
+ Nickname = "aaa"
+ });
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which;
+ body.Administrator.Should().Be(true);
+ body.Nickname.Should().Be("aaa");
+ }
+
+ {
+ var res = await client.GetAsync("users/newuser");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which;
+ body.Administrator.Should().Be(true);
+ body.Nickname.Should().Be("aaa");
+ }
+
+ {
+ // Token should expire.
+ var res = await userClient.GetAsync("testing/auth/Authorize");
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ {
+ // Check password.
+ (await CreateClientWithCredential("newuser", "newpw")).Dispose();
+ }
+ }
+
+ [Fact]
+ public async Task Patch_NotExist()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { });
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody()
+ .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ }
+
+ [Fact]
+ public async Task Patch_InvalidModel()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { });
+ res.Should().BeInvalidModel();
+ }
+
+ public static IEnumerable<object[]> Patch_InvalidModel_Body_Data()
+ {
+ yield return new[] { new UserPatchRequest { Username = "aaa!a" } };
+ yield return new[] { new UserPatchRequest { Password = "" } };
+ yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } };
+ }
+
+ [Theory]
+ [MemberData(nameof(Patch_InvalidModel_Body_Data))]
+ public async Task Patch_InvalidModel_Body(UserPatchRequest body)
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("users/user1", body);
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Patch_UsernameConflict()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "admin" });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict);
+ }
+
+ [Fact]
+ public async Task Patch_NoAuth_Unauthorized()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Patch_User_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("users/admin", new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Username_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Password_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Password = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Administrator_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Administrator = true });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Delete_Deleted()
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.DeleteAsync("users/user1");
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.GetAsync("users/user1");
+ res.Should().HaveStatusCode(404);
+ }
+ }
+
+ [Fact]
+ public async Task Delete_NotExist()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.DeleteAsync("users/usernotexist");
+ res.Should().BeDelete(false);
+ }
+
+ [Fact]
+ public async Task Delete_InvalidModel()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.DeleteAsync("users/aaa!a");
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Delete_NoAuth_Unauthorized()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.DeleteAsync("users/aaa!a");
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Delete_User_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.DeleteAsync("users/aaa!a");
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ private const string createUserUrl = "userop/createuser";
+
+ [Fact]
+ public async Task Op_CreateUser()
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = true,
+ Nickname = "ccc"
+ });
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>().Which;
+ body.Username.Should().Be("aaa");
+ body.Nickname.Should().Be("ccc");
+ body.Administrator.Should().BeTrue();
+ }
+ {
+ var res = await client.GetAsync("users/aaa");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>().Which;
+ body.Username.Should().Be("aaa");
+ body.Nickname.Should().Be("ccc");
+ body.Administrator.Should().BeTrue();
+ }
+ {
+ // Test password.
+ (await CreateClientWithCredential("aaa", "bbb")).Dispose();
+ }
+ }
+
+ public static IEnumerable<object[]> Op_CreateUser_InvalidModel_Data()
+ {
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } };
+ }
+
+ [Theory]
+ [MemberData(nameof(Op_CreateUser_InvalidModel_Data))]
+ public async Task Op_CreateUser_InvalidModel(CreateUserRequest body)
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, body);
+ res.Should().BeInvalidModel();
+ }
+ }
+
+ [Fact]
+ public async Task Op_CreateUser_UsernameConflict()
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "user1",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict);
+ }
+ }
+
+ [Fact]
+ public async Task Op_CreateUser_NoAuth_Unauthorized()
+ {
+ using var client = await CreateDefaultClient();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+ }
+
+ [Fact]
+ public async Task Op_CreateUser_User_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+ }
+
+ private const string changePasswordUrl = "userop/changepassword";
+
+ [Fact]
+ public async Task Op_ChangePassword()
+ {
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" });
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+ {
+ (await CreateClientWithCredential("user1", "newpw")).Dispose();
+ }
+ }
+
+ public static IEnumerable<object[]> Op_ChangePassword_InvalidModel_Data()
+ {
+ yield return new[] { null, "ppp" };
+ yield return new[] { "ppp", null };
+ }
+
+ [Theory]
+ [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))]
+ public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword)
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_BadOldPassword()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword);
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_NoAuth_Unauthorized()
+ {
+ using var client = await CreateDefaultClient();
+ var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/PasswordGenerator.cs b/BackEnd/Timeline.Tests/PasswordGenerator.cs new file mode 100644 index 00000000..863439b5 --- /dev/null +++ b/BackEnd/Timeline.Tests/PasswordGenerator.cs @@ -0,0 +1,23 @@ +using Timeline.Services;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests
+{
+ public class PasswordGenerator
+ {
+ private readonly ITestOutputHelper _output;
+
+ public PasswordGenerator(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void Generate()
+ {
+ var service = new PasswordService();
+ _output.WriteLine(service.HashPassword("crupest"));
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Properties/launchSettings.json b/BackEnd/Timeline.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..f3ee419d --- /dev/null +++ b/BackEnd/Timeline.Tests/Properties/launchSettings.json @@ -0,0 +1,2 @@ +{
+}
diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs new file mode 100644 index 00000000..5a774b78 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -0,0 +1,329 @@ +using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.Services
+{
+ public class TimelineServiceTest : IAsyncLifetime, IDisposable
+ {
+ private readonly TestDatabase _testDatabase = new TestDatabase();
+
+ private DatabaseContext _databaseContext;
+
+ private readonly PasswordService _passwordService = new PasswordService();
+
+ private readonly ETagGenerator _eTagGenerator = new ETagGenerator();
+
+ private readonly ImageValidator _imageValidator = new ImageValidator();
+
+ private readonly TestClock _clock = new TestClock();
+
+ private DataManager _dataManager;
+
+ private UserService _userService;
+
+ private TimelineService _timelineService;
+
+ private UserDeleteService _userDeleteService;
+
+ public TimelineServiceTest()
+ {
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _testDatabase.InitializeAsync();
+ _databaseContext = _testDatabase.CreateContext();
+ _dataManager = new DataManager(_databaseContext, _eTagGenerator);
+ _userService = new UserService(NullLogger<UserService>.Instance, _databaseContext, _passwordService, _clock);
+ _timelineService = new TimelineService(NullLogger<TimelineService>.Instance, _databaseContext, _dataManager, _userService, _imageValidator, _clock);
+ _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.Instance, _databaseContext, _timelineService);
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _testDatabase.DisposeAsync();
+ await _databaseContext.DisposeAsync();
+ }
+
+ public void Dispose()
+ {
+ _eTagGenerator.Dispose();
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task Timeline_GetLastModified(string timelineName)
+ {
+ var time = _clock.ForwardCurrentTime();
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"));
+
+ var t = await _timelineService.GetTimelineLastModifiedTime(timelineName);
+
+ t.Should().Be(time);
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task Timeline_GetUnqiueId(string timelineName)
+ {
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"));
+
+ var uniqueId = await _timelineService.GetTimelineUniqueId(timelineName);
+
+ uniqueId.Should().NotBeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task Timeline_LastModified(string timelineName)
+ {
+ var initTime = _clock.ForwardCurrentTime();
+
+ void Check(Models.Timeline timeline)
+ {
+ timeline.NameLastModified.Should().Be(initTime);
+ timeline.LastModified.Should().Be(_clock.GetCurrentTime());
+ }
+
+ async Task GetAndCheck()
+ {
+ Check(await _timelineService.GetTimeline(timelineName));
+ }
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ Check(await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")));
+
+ await GetAndCheck();
+
+ _clock.ForwardCurrentTime();
+ await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Visibility = TimelineVisibility.Public });
+ await GetAndCheck();
+
+ _clock.ForwardCurrentTime();
+ await _timelineService.ChangeMember(timelineName, new List<string> { "admin" }, null);
+ await GetAndCheck();
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince(string timelineName)
+ {
+ _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ DateTime testPoint = new DateTime();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var t = _clock.ForwardCurrentTime();
+ if (index == 1)
+ testPoint = t;
+ await _timelineService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelineService.GetPosts(timelineName, testPoint);
+ posts.Should().HaveCount(3)
+ .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1));
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_IncludeDeleted(string timelineName)
+ {
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var content in postContentList)
+ {
+ await _timelineService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelineService.GetPosts(timelineName);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(postContentList);
+
+ foreach (var id in new long[] { posts[0].Id, posts[2].Id })
+ {
+ await _timelineService.DeletePost(timelineName, id);
+ }
+
+ posts = await _timelineService.GetPosts(timelineName);
+ posts.Should().HaveCount(2);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" });
+
+ posts = await _timelineService.GetPosts(timelineName, includeDeleted: true);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false });
+ posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" });
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelineService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelineService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new User { Nickname = "haha" });
+ var posts = await _timelineService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new User { Username = "haha" });
+ var posts = await _timelineService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(4);
+ }
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UserDelete(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+ var adminId = await _userService.GetUserIdByUsername("admin");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, adminId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelineService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelineService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ await _userDeleteService.DeleteUser("user");
+
+ {
+ var posts = await _timelineService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ var posts = await _timelineService.GetPosts(timelineName, time2, true);
+ posts.Should().HaveCount(4);
+ }
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task Title(string timelineName)
+ {
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"));
+
+ {
+ var timeline = await _timelineService.GetTimeline(timelineName);
+ timeline.Title.Should().Be(timelineName);
+ }
+
+ {
+ await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = null });
+ var timeline = await _timelineService.GetTimeline(timelineName);
+ timeline.Title.Should().Be(timelineName);
+ }
+
+ {
+ await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = "atitle" });
+ var timeline = await _timelineService.GetTimeline(timelineName);
+ timeline.Title.Should().Be("atitle");
+ }
+ }
+
+ [Fact]
+ public async Task ChangeName()
+ {
+ _clock.ForwardCurrentTime();
+
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("!!!", "newtl")).Should().ThrowAsync<ArgumentException>();
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "!!!")).Should().ThrowAsync<ArgumentException>();
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "newtl")).Should().ThrowAsync<TimelineNotExistException>();
+
+ await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user"));
+ await _timelineService.CreateTimeline("tl2", await _userService.GetUserIdByUsername("user"));
+
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "tl2")).Should().ThrowAsync<EntityAlreadyExistException>();
+
+ var time = _clock.ForwardCurrentTime();
+
+ await _timelineService.ChangeTimelineName("tl", "newtl");
+
+ {
+ var timeline = await _timelineService.GetTimeline("newtl");
+ timeline.Name.Should().Be("newtl");
+ timeline.LastModified.Should().Be(time);
+ timeline.NameLastModified.Should().Be(time);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj new file mode 100644 index 00000000..973e0fc0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj @@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+
+ <LangVersion>8.0</LangVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="coverlet.collector" Version="1.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="FluentAssertions" Version="5.10.3" />
+ <PackageReference Include="JunitXml.TestLogger" Version="2.1.78" />
+ <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" 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.NET.Test.Sdk" Version="16.7.1" />
+ <PackageReference Include="Moq" Version="4.14.7" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline\Timeline.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs new file mode 100644 index 00000000..5b568adf --- /dev/null +++ b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -0,0 +1,78 @@ +using FluentAssertions;
+using Timeline.Models.Validation;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests
+{
+ public class UsernameValidatorUnitTest : IClassFixture<UsernameValidator>
+ {
+ private readonly UsernameValidator _validator;
+
+ public UsernameValidatorUnitTest(UsernameValidator validator)
+ {
+ _validator = validator;
+ }
+
+ private string FailAndMessage(string username)
+ {
+ var (result, message) = _validator.Validate(username);
+ result.Should().BeFalse();
+ return message;
+ }
+
+ [Fact]
+ public void NotString()
+ {
+ var (result, message) = _validator.Validate(123);
+ result.Should().BeFalse();
+ message.Should().ContainEquivalentOf("type");
+ }
+
+ [Fact]
+ public void Empty()
+ {
+ FailAndMessage("").Should().ContainEquivalentOf("empty");
+ }
+
+ [Theory]
+ [InlineData("!")]
+ [InlineData("!abc")]
+ [InlineData("ab c")]
+ [InlineData("ab c!")] // This is a chinese ! .
+ public void BadCharactor(string value)
+ {
+ FailAndMessage(value).Should().ContainEquivalentOf("invalid")
+ .And.ContainEquivalentOf("character");
+ }
+
+ [Fact]
+ public void TooLong()
+ {
+ FailAndMessage(new string('a', 40)).Should().ContainEquivalentOf("long");
+ }
+
+ [Fact(Skip = "Currently name can't be longer than 26. So this will print message of too long.")]
+ public void UniqueId()
+ {
+ FailAndMessage("e4c80127d092d9b2fc19c5e04612d4c0").Should().ContainEquivalentOf("unique id");
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("abc")]
+ [InlineData("-abc")]
+ [InlineData("_abc")]
+ [InlineData("abc-")]
+ [InlineData("abc_")]
+ [InlineData("a-bc")]
+ [InlineData("a-b-c")]
+ [InlineData("a-b_c")]
+ [InlineData("a-你好_c")]
+ public void Success(string value)
+ {
+ var (result, _) = _validator.Validate(value);
+ result.Should().BeTrue();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/coverletArgs.runsettings b/BackEnd/Timeline.Tests/coverletArgs.runsettings new file mode 100644 index 00000000..24cd1822 --- /dev/null +++ b/BackEnd/Timeline.Tests/coverletArgs.runsettings @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?>
+<RunSettings>
+ <DataCollectionRunSettings>
+ <DataCollectors>
+ <DataCollector friendlyName="XPlat code coverage">
+ <Configuration>
+ <!-- [Assembly-Filter]Type-Filter -->
+ <Exclude>[xunit.*]*,[Timeline]Timeline.Migrations.*</Exclude>
+ </Configuration>
+ </DataCollector>
+ </DataCollectors>
+ </DataCollectionRunSettings>
+</RunSettings>
diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json new file mode 100644 index 00000000..7150a222 --- /dev/null +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -0,0 +1,2040 @@ +{
+ "version": 1,
+ "dependencies": {
+ ".NETCoreApp,Version=v3.1": {
+ "coverlet.collector": {
+ "type": "Direct",
+ "requested": "[1.3.0, )",
+ "resolved": "1.3.0",
+ "contentHash": "t8pnf5SX2ya0RX4vjoxsbhDMQCZJcpPun2neHKJ4FouMmObylo25FvoOydvf3Bl+l+IzWw7u2vjEeCBHnleB9g=="
+ },
+ "FluentAssertions": {
+ "type": "Direct",
+ "requested": "[5.10.3, )",
+ "resolved": "5.10.3",
+ "contentHash": "gVPEVp1hLVqcv+7Q2wiDf7kqCNn7+bQcQ0jbJ2mcRT6CeRoZl1tNkqvzSIhvekyldDptk77j1b03MXTTRIqqpg==",
+ "dependencies": {
+ "System.Configuration.ConfigurationManager": "4.4.0"
+ }
+ },
+ "JunitXml.TestLogger": {
+ "type": "Direct",
+ "requested": "[2.1.78, )",
+ "resolved": "2.1.78",
+ "contentHash": "4y4FSfKWxlked8ilQdqBBSeRMf5jD/Hkvyp744hc54yQcABLt4rR2Q+4hNqAqrSo+mhwAlusj2rpXpN/5TICCA=="
+ },
+ "Microsoft.AspNet.WebApi.Client": {
+ "type": "Direct",
+ "requested": "[5.2.7, )",
+ "resolved": "5.2.7",
+ "contentHash": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==",
+ "dependencies": {
+ "Newtonsoft.Json": "10.0.1",
+ "Newtonsoft.Json.Bson": "1.0.1"
+ }
+ },
+ "Microsoft.AspNetCore.TestHost": {
+ "type": "Direct",
+ "requested": "[3.1.9, )",
+ "resolved": "3.1.9",
+ "contentHash": "0DBtfgmM2yS4h0v+gS4JHRX4nuyQmW7Yi5/G4yB5KelA2dDXPsAiipw9z47B1jVEs9QZdOwSqPQm2R/owl2TnA==",
+ "dependencies": {
+ "System.IO.Pipelines": "4.7.3"
+ }
+ },
+ "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.NET.Test.Sdk": {
+ "type": "Direct",
+ "requested": "[16.7.1, )",
+ "resolved": "16.7.1",
+ "contentHash": "7T3XYuLT2CRMZXwlp8p4cEEf6y7VifxTdKwYNzCYp31CN4iyrcDKneIJvNTo0YVnTxJn+CSlGVlUnZHUlAwt9A==",
+ "dependencies": {
+ "Microsoft.CodeCoverage": "16.7.1",
+ "Microsoft.TestPlatform.TestHost": "16.7.1"
+ }
+ },
+ "Moq": {
+ "type": "Direct",
+ "requested": "[4.14.7, )",
+ "resolved": "4.14.7",
+ "contentHash": "z1jwY3lL3d4l+92cdSnhRDUUco68HiRNfLKB9r9/PLP5lrN+ZL1Qtt3brVGVB8iY+ioBXhlFue2JtycBczE8Pw==",
+ "dependencies": {
+ "Castle.Core": "4.4.0",
+ "System.Threading.Tasks.Extensions": "4.5.1"
+ }
+ },
+ "xunit": {
+ "type": "Direct",
+ "requested": "[2.4.1, )",
+ "resolved": "2.4.1",
+ "contentHash": "XNR3Yz9QTtec16O0aKcO6+baVNpXmOnPUxDkCY97J+8krUYxPvXT1szYYEUdKk4sB8GOI2YbAjRIOm8ZnXRfzQ==",
+ "dependencies": {
+ "xunit.analyzers": "0.10.0",
+ "xunit.assert": "[2.4.1]",
+ "xunit.core": "[2.4.1]"
+ }
+ },
+ "xunit.runner.visualstudio": {
+ "type": "Direct",
+ "requested": "[2.4.3, )",
+ "resolved": "2.4.3",
+ "contentHash": "kZZSmOmKA8OBlAJaquPXnJJLM9RwQ27H7BMVqfMLUcTi9xHinWGJiWksa3D4NEtz0wZ/nxd2mogObvBgJKCRhQ=="
+ },
+ "AutoMapper": {
+ "type": "Transitive",
+ "resolved": "10.1.1",
+ "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.7.0",
+ "System.Reflection.Emit": "4.7.0"
+ }
+ },
+ "AutoMapper.Extensions.Microsoft.DependencyInjection": {
+ "type": "Transitive",
+ "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"
+ }
+ },
+ "Castle.Core": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1",
+ "System.Collections.Specialized": "4.3.0",
+ "System.ComponentModel": "4.3.0",
+ "System.ComponentModel.TypeConverter": "4.3.0",
+ "System.Diagnostics.TraceSource": "4.3.0",
+ "System.Dynamic.Runtime": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Emit": "4.3.0",
+ "System.Reflection.TypeExtensions": "4.3.0",
+ "System.Xml.XmlDocument": "4.3.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.SpaServices.Extensions": {
+ "type": "Transitive",
+ "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.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.CodeCoverage": {
+ "type": "Transitive",
+ "resolved": "16.7.1",
+ "contentHash": "PhSppbk+kvAyD9yGJIcBRJ/XYwY+21YK88l22PGTtixaxNdjnx1idVKh88LCGwKaTL8HhlnQ41VmBiBdZJzIQw=="
+ },
+ "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": {
+ "type": "Transitive",
+ "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.Abstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ=="
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "Transitive",
+ "resolved": "3.1.9",
+ "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "3.1.9"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Sqlite": {
+ "type": "Transitive",
+ "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.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.TestPlatform.ObjectModel": {
+ "type": "Transitive",
+ "resolved": "16.7.1",
+ "contentHash": "FL+VpAC/nCCzj80MwX6L8gJD06u2m1SKcQQLAymDLFqNtgtI9h3J5n0mVN+s18qcMzybsmO9GK7rMuHYx11KMg==",
+ "dependencies": {
+ "NuGet.Frameworks": "5.0.0"
+ }
+ },
+ "Microsoft.TestPlatform.TestHost": {
+ "type": "Transitive",
+ "resolved": "16.7.1",
+ "contentHash": "mv7MnBDtqwQAjoH+AphE+Tu0dsF6x/c7Zs8umkb2McbvNALJdfBuWJQbiXGWqhNq7k8eMmnkNO6klJz4pkgekw==",
+ "dependencies": {
+ "Microsoft.TestPlatform.ObjectModel": "16.7.1",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "Microsoft.Win32.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "Namotion.Reflection": {
+ "type": "Transitive",
+ "resolved": "1.0.14",
+ "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==",
+ "dependencies": {
+ "Microsoft.CSharp": "4.3.0"
+ }
+ },
+ "NETStandard.Library": {
+ "type": "Transitive",
+ "resolved": "1.6.1",
+ "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.Win32.Primitives": "4.3.0",
+ "System.AppContext": "4.3.0",
+ "System.Collections": "4.3.0",
+ "System.Collections.Concurrent": "4.3.0",
+ "System.Console": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Diagnostics.Tools": "4.3.0",
+ "System.Diagnostics.Tracing": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Globalization.Calendars": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.IO.Compression": "4.3.0",
+ "System.IO.Compression.ZipFile": "4.3.0",
+ "System.IO.FileSystem": "4.3.0",
+ "System.IO.FileSystem.Primitives": "4.3.0",
+ "System.Linq": "4.3.0",
+ "System.Linq.Expressions": "4.3.0",
+ "System.Net.Http": "4.3.0",
+ "System.Net.Primitives": "4.3.0",
+ "System.Net.Sockets": "4.3.0",
+ "System.ObjectModel": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Extensions": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Runtime.InteropServices.RuntimeInformation": "4.3.0",
+ "System.Runtime.Numerics": "4.3.0",
+ "System.Security.Cryptography.Algorithms": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Security.Cryptography.X509Certificates": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Text.Encoding.Extensions": "4.3.0",
+ "System.Text.RegularExpressions": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Threading.Tasks": "4.3.0",
+ "System.Threading.Timer": "4.3.0",
+ "System.Xml.ReaderWriter": "4.3.0",
+ "System.Xml.XDocument": "4.3.0"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "12.0.2",
+ "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA=="
+ },
+ "Newtonsoft.Json.Bson": {
+ "type": "Transitive",
+ "resolved": "1.0.1",
+ "contentHash": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1",
+ "Newtonsoft.Json": "10.0.1"
+ }
+ },
+ "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.AspNetCore": {
+ "type": "Transitive",
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "NuGet.Frameworks": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA=="
+ },
+ "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q=="
+ },
+ "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA=="
+ },
+ "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw=="
+ },
+ "runtime.native.System": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0"
+ }
+ },
+ "runtime.native.System.IO.Compression": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0"
+ }
+ },
+ "runtime.native.System.Net.Http": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0"
+ }
+ },
+ "runtime.native.System.Security.Cryptography.Apple": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==",
+ "dependencies": {
+ "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0"
+ }
+ },
+ "runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==",
+ "dependencies": {
+ "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
+ "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
+ }
+ },
+ "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A=="
+ },
+ "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ=="
+ },
+ "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ=="
+ },
+ "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g=="
+ },
+ "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg=="
+ },
+ "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ=="
+ },
+ "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A=="
+ },
+ "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg=="
+ },
+ "SixLabors.ImageSharp": {
+ "type": "Transitive",
+ "resolved": "1.0.1",
+ "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ=="
+ },
+ "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.3.0",
+ "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==",
+ "dependencies": {
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Buffers": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Diagnostics.Tracing": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Collections": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Collections.Concurrent": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Diagnostics.Tracing": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "System.Collections.Immutable": {
+ "type": "Transitive",
+ "resolved": "1.7.1",
+ "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q=="
+ },
+ "System.Collections.NonGeneric": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Collections.Specialized": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==",
+ "dependencies": {
+ "System.Collections.NonGeneric": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Globalization.Extensions": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.ComponentModel": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==",
+ "dependencies": {
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.ComponentModel.Annotations": {
+ "type": "Transitive",
+ "resolved": "4.7.0",
+ "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ=="
+ },
+ "System.ComponentModel.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==",
+ "dependencies": {
+ "System.ComponentModel": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.ComponentModel.TypeConverter": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Collections.NonGeneric": "4.3.0",
+ "System.Collections.Specialized": "4.3.0",
+ "System.ComponentModel": "4.3.0",
+ "System.ComponentModel.Primitives": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Linq": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Extensions": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Reflection.TypeExtensions": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Configuration.ConfigurationManager": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==",
+ "dependencies": {
+ "System.Security.Cryptography.ProtectedData": "4.4.0"
+ }
+ },
+ "System.Console": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.IO": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Text.Encoding": "4.3.0"
+ }
+ },
+ "System.Diagnostics.Contracts": {
+ "type": "Transitive",
+ "resolved": "4.0.1",
+ "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==",
+ "dependencies": {
+ "System.Runtime": "4.1.0"
+ }
+ },
+ "System.Diagnostics.Debug": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "4.7.1",
+ "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
+ },
+ "System.Diagnostics.Tools": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Diagnostics.TraceSource": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0",
+ "runtime.native.System": "4.3.0"
+ }
+ },
+ "System.Diagnostics.Tracing": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Dynamic.Runtime": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Linq": "4.3.0",
+ "System.Linq.Expressions": "4.3.0",
+ "System.ObjectModel": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Emit": "4.3.0",
+ "System.Reflection.Emit.ILGeneration": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Reflection.TypeExtensions": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Globalization": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Globalization.Calendars": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Globalization": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Globalization.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Globalization": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0"
+ }
+ },
+ "System.IdentityModel.Tokens.Jwt": {
+ "type": "Transitive",
+ "resolved": "6.8.0",
+ "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==",
+ "dependencies": {
+ "Microsoft.IdentityModel.JsonWebTokens": "6.8.0",
+ "Microsoft.IdentityModel.Tokens": "6.8.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.Compression": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Buffers": "4.3.0",
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Threading.Tasks": "4.3.0",
+ "runtime.native.System": "4.3.0",
+ "runtime.native.System.IO.Compression": "4.3.0"
+ }
+ },
+ "System.IO.Compression.ZipFile": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==",
+ "dependencies": {
+ "System.Buffers": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.IO.Compression": "4.3.0",
+ "System.IO.FileSystem": "4.3.0",
+ "System.IO.FileSystem.Primitives": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Text.Encoding": "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.IO.Pipelines": {
+ "type": "Transitive",
+ "resolved": "4.7.3",
+ "contentHash": "zykThu9scJyg2Yeg27GMZCbjzniIsmjtNP5x6kQCd/8rEeKXRy20fP2NOMS7xQ+0pS/E85LZQA+K1aoQLxiUdw=="
+ },
+ "System.Linq": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0"
+ }
+ },
+ "System.Linq.Expressions": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Linq": "4.3.0",
+ "System.ObjectModel": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Emit": "4.3.0",
+ "System.Reflection.Emit.ILGeneration": "4.3.0",
+ "System.Reflection.Emit.Lightweight": "4.3.0",
+ "System.Reflection.Extensions": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Reflection.TypeExtensions": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Memory": {
+ "type": "Transitive",
+ "resolved": "4.5.3",
+ "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
+ },
+ "System.Net.Http": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Diagnostics.DiagnosticSource": "4.3.0",
+ "System.Diagnostics.Tracing": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Globalization.Extensions": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.IO.FileSystem": "4.3.0",
+ "System.Net.Primitives": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Security.Cryptography.Algorithms": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.OpenSsl": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Security.Cryptography.X509Certificates": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Threading.Tasks": "4.3.0",
+ "runtime.native.System": "4.3.0",
+ "runtime.native.System.Net.Http": "4.3.0",
+ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
+ }
+ },
+ "System.Net.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Handles": "4.3.0"
+ }
+ },
+ "System.Net.Sockets": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.IO": "4.3.0",
+ "System.Net.Primitives": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "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.3.0",
+ "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Reflection": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.IO": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Runtime": "4.3.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.3.0",
+ "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
+ "dependencies": {
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Reflection.Emit.Lightweight": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
+ "dependencies": {
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Emit.ILGeneration": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Reflection.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Reflection": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Reflection.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Reflection.TypeExtensions": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==",
+ "dependencies": {
+ "System.Reflection": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Resources.ResourceManager": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Globalization": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Runtime": "4.3.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.3.0",
+ "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.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.3.0",
+ "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Handles": "4.3.0"
+ }
+ },
+ "System.Runtime.InteropServices.RuntimeInformation": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==",
+ "dependencies": {
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Extensions": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Threading": "4.3.0",
+ "runtime.native.System": "4.3.0"
+ }
+ },
+ "System.Runtime.Numerics": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==",
+ "dependencies": {
+ "System.Globalization": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.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.3.0",
+ "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Collections": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Runtime.Numerics": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "runtime.native.System.Security.Cryptography.Apple": "4.3.0",
+ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
+ }
+ },
+ "System.Security.Cryptography.Cng": {
+ "type": "Transitive",
+ "resolved": "4.5.0",
+ "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A=="
+ },
+ "System.Security.Cryptography.Csp": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.IO": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Security.Cryptography.Algorithms": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0"
+ }
+ },
+ "System.Security.Cryptography.Encoding": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Collections": "4.3.0",
+ "System.Collections.Concurrent": "4.3.0",
+ "System.Linq": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
+ }
+ },
+ "System.Security.Cryptography.OpenSsl": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Runtime.Numerics": "4.3.0",
+ "System.Security.Cryptography.Algorithms": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
+ }
+ },
+ "System.Security.Cryptography.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==",
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "System.Security.Cryptography.ProtectedData": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog=="
+ },
+ "System.Security.Cryptography.X509Certificates": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.Globalization.Calendars": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.IO.FileSystem": "4.3.0",
+ "System.IO.FileSystem.Primitives": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.Handles": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Runtime.Numerics": "4.3.0",
+ "System.Security.Cryptography.Algorithms": "4.3.0",
+ "System.Security.Cryptography.Cng": "4.3.0",
+ "System.Security.Cryptography.Csp": "4.3.0",
+ "System.Security.Cryptography.Encoding": "4.3.0",
+ "System.Security.Cryptography.OpenSsl": "4.3.0",
+ "System.Security.Cryptography.Primitives": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0",
+ "runtime.native.System": "4.3.0",
+ "runtime.native.System.Net.Http": "4.3.0",
+ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.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.3.0",
+ "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==",
+ "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.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.3.0",
+ "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==",
+ "dependencies": {
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Threading": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==",
+ "dependencies": {
+ "System.Runtime": "4.3.0",
+ "System.Threading.Tasks": "4.3.0"
+ }
+ },
+ "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.5.1",
+ "contentHash": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A=="
+ },
+ "System.Threading.Timer": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0",
+ "Microsoft.NETCore.Targets": "1.1.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
+ "System.Xml.ReaderWriter": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.IO.FileSystem": "4.3.0",
+ "System.IO.FileSystem.Primitives": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Runtime.InteropServices": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Text.Encoding.Extensions": "4.3.0",
+ "System.Text.RegularExpressions": "4.3.0",
+ "System.Threading.Tasks": "4.3.0",
+ "System.Threading.Tasks.Extensions": "4.3.0"
+ }
+ },
+ "System.Xml.XDocument": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Diagnostics.Tools": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Xml.ReaderWriter": "4.3.0"
+ }
+ },
+ "System.Xml.XmlDocument": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==",
+ "dependencies": {
+ "System.Collections": "4.3.0",
+ "System.Diagnostics.Debug": "4.3.0",
+ "System.Globalization": "4.3.0",
+ "System.IO": "4.3.0",
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0",
+ "System.Runtime.Extensions": "4.3.0",
+ "System.Text.Encoding": "4.3.0",
+ "System.Threading": "4.3.0",
+ "System.Xml.ReaderWriter": "4.3.0"
+ }
+ },
+ "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"
+ }
+ },
+ "xunit.abstractions": {
+ "type": "Transitive",
+ "resolved": "2.0.3",
+ "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
+ },
+ "xunit.analyzers": {
+ "type": "Transitive",
+ "resolved": "0.10.0",
+ "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg=="
+ },
+ "xunit.assert": {
+ "type": "Transitive",
+ "resolved": "2.4.1",
+ "contentHash": "O/Oe0BS5RmSsM+LQOb041TzuPo5MdH2Rov+qXGS37X+KFG1Hxz7kopYklM5+1Y+tRGeXrOx5+Xne1RuqLFQoyQ==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1"
+ }
+ },
+ "xunit.core": {
+ "type": "Transitive",
+ "resolved": "2.4.1",
+ "contentHash": "Zsj5OMU6JasNGERXZy8s72+pcheG6Q15atS5XpZXqAtULuyQiQ6XNnUsp1gyfC6WgqScqMvySiEHmHcOG6Eg0Q==",
+ "dependencies": {
+ "xunit.extensibility.core": "[2.4.1]",
+ "xunit.extensibility.execution": "[2.4.1]"
+ }
+ },
+ "xunit.extensibility.core": {
+ "type": "Transitive",
+ "resolved": "2.4.1",
+ "contentHash": "yKZKm/8QNZnBnGZFD9SewkllHBiK0DThybQD/G4PiAmQjKtEZyHi6ET70QPU9KtSMJGRYS6Syk7EyR2EVDU4Kg==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1",
+ "xunit.abstractions": "2.0.3"
+ }
+ },
+ "xunit.extensibility.execution": {
+ "type": "Transitive",
+ "resolved": "2.4.1",
+ "contentHash": "7e/1jqBpcb7frLkB6XDrHCGXAbKN4Rtdb88epYxCSRQuZDRW8UtTfdTEVpdTl8s4T56e07hOBVd4G0OdCxIY2A==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1",
+ "xunit.extensibility.core": "[2.4.1]"
+ }
+ },
+ "timeline": {
+ "type": "Project",
+ "dependencies": {
+ "AutoMapper": "10.1.1",
+ "AutoMapper.Extensions.Microsoft.DependencyInjection": "8.1.0",
+ "Microsoft.AspNetCore.SpaServices.Extensions": "3.1.9",
+ "Microsoft.EntityFrameworkCore": "3.1.9",
+ "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9",
+ "Microsoft.EntityFrameworkCore.Sqlite": "3.1.9",
+ "NSwag.AspNetCore": "13.8.2",
+ "SixLabors.ImageSharp": "1.0.1",
+ "System.IdentityModel.Tokens.Jwt": "6.8.0",
+ "Timeline.ErrorCodes": "1.0.0"
+ }
+ },
+ "timeline.errorcodes": {
+ "type": "Project"
+ }
+ }
+ }
+}
\ No newline at end of file diff --git a/BackEnd/Timeline.sln b/BackEnd/Timeline.sln new file mode 100644 index 00000000..40a32ee9 --- /dev/null +++ b/BackEnd/Timeline.sln @@ -0,0 +1,42 @@ +Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29709.97
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline", "Timeline\Timeline.csproj", "{A34D323C-5233-4754-B14F-4819CE9C27CA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes.CodeGenerator", "Timeline.ErrorCodes.CodeGenerator\Timeline.ErrorCodes.CodeGenerator.csproj", "{D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9A7526E5-E68F-465C-9E0F-88BF6E040F14}
+ EndGlobalSection
+EndGlobal
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 diff --git a/BackEnd/tools/convert-eol.py b/BackEnd/tools/convert-eol.py new file mode 100644 index 00000000..3ea8ed7c --- /dev/null +++ b/BackEnd/tools/convert-eol.py @@ -0,0 +1,35 @@ +# This is a python script that converts all text source codes into
+# CRLF (Windows line ending) eol format and UTF-8 with NO BOM encoding.
+
+import glob
+import os.path
+
+project_root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..'))
+
+
+def convert(file_path):
+ with open(file_path, 'r', encoding='utf-8') as open_file:
+ content = open_file.read()
+
+ #if there is BOM, remove BOM
+ if content[0] == '\ufeff':
+ content = content[1:]
+
+ with open(file_path, 'w', encoding='utf-8', newline='\r\n') as open_file:
+ open_file.write(content)
+
+
+glob_list = [
+ './nuget.config',
+ '**/*.sln',
+ '**/*.cs',
+ '**/*.csproj',
+ '**/appsettings*.json'
+]
+
+for glob_pattern in glob_list:
+ for f in glob.glob(glob_pattern, recursive=True):
+ print('Converting {}'.format(f))
+ convert(f)
+
+print('Done!!!')
|