From e02e1e120693d466679cae2b602085f57b60971f Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 11 Aug 2020 01:16:56 +0800 Subject: Make author column of post nullable. --- Timeline/Models/Http/Timeline.cs | 2 +- Timeline/Models/Timeline.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'Timeline/Models') diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 5404d561..52e26190 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -21,7 +21,7 @@ namespace Timeline.Models.Http public TimelinePostContentInfo? Content { get; set; } public bool Deleted { get; set; } public DateTime Time { get; set; } - public UserInfo Author { get; set; } = default!; + public UserInfo? Author { get; set; } = default!; public DateTime LastUpdated { get; set; } = default!; } diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 7afb1984..34c253a0 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -48,7 +48,7 @@ namespace Timeline.Models public class TimelinePost { - public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User author, DateTime lastUpdated, string timelineName) + public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) { Id = id; Content = content; @@ -62,7 +62,7 @@ namespace Timeline.Models public ITimelinePostContent? Content { get; set; } public bool Deleted => Content == null; public DateTime Time { get; set; } - public User Author { get; set; } + public User? Author { get; set; } public DateTime LastUpdated { get; set; } public string TimelineName { get; set; } } -- cgit v1.2.3 From 4685d69fe965b32438c399a4ad4a3deee9fb0a55 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 11 Aug 2020 16:19:46 +0800 Subject: Migrate DateTimeOffset to DateTime. --- Timeline/Entities/UserEntity.cs | 6 +- ...0808_ChangeDateTimeOffsetToDateTime.Designer.cs | 337 +++++++++++++++++++++ ...0200811080808_ChangeDateTimeOffsetToDateTime.cs | 17 ++ .../Migrations/DatabaseContextModelSnapshot.cs | 6 +- Timeline/Models/User.cs | 6 +- Timeline/Services/UserService.cs | 2 +- 6 files changed, 364 insertions(+), 10 deletions(-) create mode 100644 Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs create mode 100644 Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs (limited to 'Timeline/Models') diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index d6b55ab6..0cfaa335 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -25,7 +25,7 @@ namespace Timeline.Entities public string Username { get; set; } = default!; [Column("username_change_time")] - public DateTimeOffset UsernameChangeTime { get; set; } + public DateTime UsernameChangeTime { get; set; } [Column("password"), Required] public string Password { get; set; } = default!; @@ -40,10 +40,10 @@ namespace Timeline.Entities public string? Nickname { get; set; } [Column("create_time")] - public DateTimeOffset CreateTime { get; set; } + public DateTime CreateTime { get; set; } [Column("last_modified")] - public DateTimeOffset LastModified { get; set; } + public DateTime LastModified { get; set; } public UserAvatarEntity? Avatar { get; set; } diff --git a/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs b/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs new file mode 100644 index 00000000..58238557 --- /dev/null +++ b/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs @@ -0,0 +1,337 @@ +// +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("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("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/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs b/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs new file mode 100644 index 00000000..eb6b44f3 --- /dev/null +++ b/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/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 8ae4aeea..3066dcc0 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -226,13 +226,13 @@ namespace Timeline.Migrations .HasColumnName("id") .HasColumnType("INTEGER"); - b.Property("CreateTime") + b.Property("CreateTime") .ValueGeneratedOnAdd() .HasColumnName("create_time") .HasColumnType("TEXT") .HasDefaultValueSql("datetime('now', 'utc')"); - b.Property("LastModified") + b.Property("LastModified") .ValueGeneratedOnAdd() .HasColumnName("last_modified") .HasColumnType("TEXT") @@ -264,7 +264,7 @@ namespace Timeline.Migrations .HasColumnName("username") .HasColumnType("TEXT"); - b.Property("UsernameChangeTime") + b.Property("UsernameChangeTime") .ValueGeneratedOnAdd() .HasColumnName("username_change_time") .HasColumnType("TEXT") diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs index 3d0b2f1a..f08a62db 100644 --- a/Timeline/Models/User.cs +++ b/Timeline/Models/User.cs @@ -13,9 +13,9 @@ namespace Timeline.Models public long? Id { get; set; } public string? Password { get; set; } public long? Version { get; set; } - public DateTimeOffset? UsernameChangeTime { get; set; } - public DateTimeOffset? CreateTime { get; set; } - public DateTimeOffset? LastModified { get; set; } + public DateTime? UsernameChangeTime { get; set; } + public DateTime? CreateTime { get; set; } + public DateTime? LastModified { get; set; } #endregion secret } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index c186c170..d9b3da26 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -332,7 +332,7 @@ namespace Timeline.Services { if (info != null) { - DateTimeOffset now = _clock.GetCurrentTime(); + var now = _clock.GetCurrentTime(); bool updateLastModified = false; var username = info.Username; -- cgit v1.2.3 From 7df5e63e8b2eaa5b4f83ee72a32eeedd480cde47 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 11 Aug 2020 17:59:44 +0800 Subject: Post list modified since now consider username change. And make all datetime utc. --- Timeline.Tests/Helpers/TestClock.cs | 4 +- Timeline.Tests/IntegratedTests/TimelineTest.cs | 4 +- Timeline/Entities/DatabaseContext.cs | 2 + Timeline/Entities/UtcDateAnnotation.cs | 44 +++++++++++++++++++ Timeline/Helpers/DateTimeExtensions.cs | 14 ++++++ .../Models/Converters/JsonDateTimeConverter.cs | 7 +-- Timeline/Models/Converters/MyDateTimeConverter.cs | 51 ++++++++++++++++++++++ Timeline/Services/Clock.cs | 2 +- Timeline/Services/TimelineService.cs | 12 ++++- Timeline/Services/UserTokenManager.cs | 3 ++ Timeline/Startup.cs | 3 ++ 11 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 Timeline/Entities/UtcDateAnnotation.cs create mode 100644 Timeline/Helpers/DateTimeExtensions.cs create mode 100644 Timeline/Models/Converters/MyDateTimeConverter.cs (limited to 'Timeline/Models') diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs index 0cbf236d..ed2d65a6 100644 --- a/Timeline.Tests/Helpers/TestClock.cs +++ b/Timeline.Tests/Helpers/TestClock.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.Helpers public DateTime GetCurrentTime() { - return _currentTime ?? DateTime.Now; + return _currentTime ?? DateTime.UtcNow; } public void SetCurrentTime(DateTime? mockTime) @@ -22,7 +22,7 @@ namespace Timeline.Tests.Helpers public DateTime SetMockCurrentTime() { - var time = new DateTime(2000, 1, 1, 1, 1, 1); + var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc); _currentTime = time; return time; } diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 49672f29..16b3c7e4 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -952,7 +952,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); } const string mockContent2 = "bbb"; - var mockTime2 = DateTime.Now.AddDays(-1); + var mockTime2 = DateTime.UtcNow.AddDays(-1); TimelinePostInfo createRes2; { var res = await client.PostAsJsonAsync(generator(1, "posts"), @@ -1009,7 +1009,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Id; } - var now = DateTime.Now; + var now = DateTime.UtcNow; var id0 = await CreatePost(now.AddDays(1)); var id1 = await CreatePost(now.AddDays(-1)); var id2 = await CreatePost(now); diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ba2566bd..ecadd703 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -19,6 +19,8 @@ namespace Timeline.Entities modelBuilder.Entity().Property(e => e.LastModified).HasDefaultValueSql("datetime('now', 'utc')"); modelBuilder.Entity().HasIndex(e => e.Tag).IsUnique(); modelBuilder.Entity().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))"); + + modelBuilder.ApplyUtcDateTimeConverter(); } public DbSet Users { get; set; } = default!; diff --git a/Timeline/Entities/UtcDateAnnotation.cs b/Timeline/Entities/UtcDateAnnotation.cs new file mode 100644 index 00000000..6600e701 --- /dev/null +++ b/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 UtcConverter = + new ValueConverter(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + + public static PropertyBuilder IsUtc(this PropertyBuilder builder, bool isUtc = true) => + builder.HasAnnotation(IsUtcAnnotation, isUtc); + + public static bool IsUtc(this IMutableProperty property) => + ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true; + + /// + /// Make sure this is called after configuring all your entities. + /// + 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/Timeline/Helpers/DateTimeExtensions.cs b/Timeline/Helpers/DateTimeExtensions.cs new file mode 100644 index 00000000..374f3bc9 --- /dev/null +++ b/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/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs index ef129a01..865b6251 100644 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ b/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -3,7 +3,8 @@ 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 @@ -11,12 +12,12 @@ namespace Timeline.Models.Converters public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(typeToConvert == typeof(DateTime)); - return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture); + return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z"); + writer.WriteStringValue(value.MyToUtc().ToString("s", CultureInfo.InvariantCulture) + "Z"); } } } diff --git a/Timeline/Models/Converters/MyDateTimeConverter.cs b/Timeline/Models/Converters/MyDateTimeConverter.cs new file mode 100644 index 00000000..f125cd5c --- /dev/null +++ b/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/Timeline/Services/Clock.cs b/Timeline/Services/Clock.cs index 040f9304..4395edcd 100644 --- a/Timeline/Services/Clock.cs +++ b/Timeline/Services/Clock.cs @@ -23,7 +23,7 @@ namespace Timeline.Services public DateTime GetCurrentTime() { - return DateTime.Now; + return DateTime.UtcNow; } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 283938fb..0070fe3e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -565,11 +565,13 @@ namespace Timeline.Services public async Task> 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); - var query = _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId); + IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); if (!includeDeleted) { @@ -578,9 +580,11 @@ namespace Timeline.Services if (modifiedSince.HasValue) { - query = query.Where(p => p.LastUpdated >= modifiedSince); + 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(); @@ -663,6 +667,8 @@ namespace Timeline.Services public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) { + time = time?.MyToUtc(); + if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); if (text == null) @@ -704,6 +710,8 @@ namespace Timeline.Services public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) { + time = time?.MyToUtc(); + if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); if (data == null) diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index a016ff96..813dae67 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; +using Timeline.Helpers; using Timeline.Models; using Timeline.Services.Exceptions; @@ -57,6 +58,8 @@ namespace Timeline.Services public async Task CreateToken(string username, string password, DateTime? expireAt = null) { + expireAt = expireAt?.MyToUtc(); + if (username == null) throw new ArgumentNullException(nameof(username)); if (password == null) diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 7a813ac7..be2377b9 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using System; +using System.ComponentModel; using System.Text.Json.Serialization; using Timeline.Auth; using Timeline.Configs; @@ -40,6 +41,8 @@ namespace Timeline // 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()); -- cgit v1.2.3