aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline.Tests/Helpers/TestClock.cs4
-rw-r--r--Timeline.Tests/IntegratedTests/TimelineTest.cs4
-rw-r--r--Timeline.Tests/IntegratedTests/TokenTest.cs4
-rw-r--r--Timeline.Tests/Services/TimelineServiceTest.cs84
-rw-r--r--Timeline/Controllers/UserController.cs6
-rw-r--r--Timeline/Entities/DatabaseContext.cs2
-rw-r--r--Timeline/Entities/TimelinePostEntity.cs4
-rw-r--r--Timeline/Entities/UserEntity.cs6
-rw-r--r--Timeline/Entities/UtcDateAnnotation.cs44
-rw-r--r--Timeline/Helpers/DateTimeExtensions.cs14
-rw-r--r--Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs337
-rw-r--r--Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs78
-rw-r--r--Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs337
-rw-r--r--Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs17
-rw-r--r--Timeline/Migrations/DatabaseContextModelSnapshot.cs12
-rw-r--r--Timeline/Models/Converters/JsonDateTimeConverter.cs7
-rw-r--r--Timeline/Models/Converters/MyDateTimeConverter.cs51
-rw-r--r--Timeline/Models/Http/Timeline.cs2
-rw-r--r--Timeline/Models/Timeline.cs4
-rw-r--r--Timeline/Models/User.cs6
-rw-r--r--Timeline/Services/Clock.cs2
-rw-r--r--Timeline/Services/TimelineService.cs51
-rw-r--r--Timeline/Services/UserDeleteService.cs69
-rw-r--r--Timeline/Services/UserService.cs46
-rw-r--r--Timeline/Services/UserTokenManager.cs3
-rw-r--r--Timeline/Startup.cs4
26 files changed, 1118 insertions, 80 deletions
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.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs
index d1c31606..480d66cd 100644
--- a/Timeline.Tests/IntegratedTests/TokenTest.cs
+++ b/Timeline.Tests/IntegratedTests/TokenTest.cs
@@ -119,9 +119,9 @@ namespace Timeline.Tests.IntegratedTests
using var client = await CreateDefaultClient();
var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token;
- using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped.
+ using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped.
{
- var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ var userService = scope.ServiceProvider.GetRequiredService<IUserDeleteService>();
await userService.DeleteUser("user1");
}
diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs
index e129b49d..36e5ed0c 100644
--- a/Timeline.Tests/Services/TimelineServiceTest.cs
+++ b/Timeline.Tests/Services/TimelineServiceTest.cs
@@ -32,6 +32,8 @@ namespace Timeline.Tests.Services
private TimelineService _timelineService;
+ private UserDeleteService _userDeleteService;
+
public TimelineServiceTest()
{
}
@@ -43,6 +45,7 @@ namespace Timeline.Tests.Services
_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()
@@ -187,5 +190,86 @@ namespace Timeline.Tests.Services
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);
+ }
+ }
}
}
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs
index c8c1e610..3986bb5b 100644
--- a/Timeline/Controllers/UserController.cs
+++ b/Timeline/Controllers/UserController.cs
@@ -22,12 +22,14 @@ namespace Timeline.Controllers
{
private readonly ILogger<UserController> _logger;
private readonly IUserService _userService;
+ private readonly IUserDeleteService _userDeleteService;
private readonly IMapper _mapper;
- public UserController(ILogger<UserController> logger, IUserService userService, IMapper mapper)
+ public UserController(ILogger<UserController> logger, IUserService userService, IUserDeleteService userDeleteService, IMapper mapper)
{
_logger = logger;
_userService = userService;
+ _userDeleteService = userDeleteService;
_mapper = mapper;
}
@@ -102,7 +104,7 @@ namespace Timeline.Controllers
[HttpDelete("users/{username}"), AdminAuthorize]
public async Task<ActionResult<CommonDeleteResponse>> Delete([FromRoute][Username] string username)
{
- var delete = await _userService.DeleteUser(username);
+ var delete = await _userDeleteService.DeleteUser(username);
if (delete)
return Ok(CommonDeleteResponse.Delete());
else
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<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!;
diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs
index 24bfc7a3..07367fba 100644
--- a/Timeline/Entities/TimelinePostEntity.cs
+++ b/Timeline/Entities/TimelinePostEntity.cs
@@ -20,10 +20,10 @@ namespace Timeline.Entities
public TimelineEntity Timeline { get; set; } = default!;
[Column("author")]
- public long AuthorId { get; set; }
+ public long? AuthorId { get; set; }
[ForeignKey(nameof(AuthorId))]
- public UserEntity Author { get; set; } = default!;
+ public UserEntity? Author { get; set; } = default!;
[Column("content_type"), Required]
public string ContentType { get; set; } = default!;
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/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<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/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/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs b/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs
new file mode 100644
index 00000000..80598fdf
--- /dev/null
+++ b/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/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs b/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs
new file mode 100644
index 00000000..b0f0bca7
--- /dev/null
+++ b/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/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 @@
+// <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/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 41fcc2e6..3066dcc0 100644
--- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs
+++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs
@@ -146,7 +146,7 @@ namespace Timeline.Migrations
.HasColumnName("id")
.HasColumnType("INTEGER");
- b.Property<long>("AuthorId")
+ b.Property<long?>("AuthorId")
.HasColumnName("author")
.HasColumnType("INTEGER");
@@ -226,13 +226,13 @@ namespace Timeline.Migrations
.HasColumnName("id")
.HasColumnType("INTEGER");
- b.Property<DateTimeOffset>("CreateTime")
+ b.Property<DateTime>("CreateTime")
.ValueGeneratedOnAdd()
.HasColumnName("create_time")
.HasColumnType("TEXT")
.HasDefaultValueSql("datetime('now', 'utc')");
- b.Property<DateTimeOffset>("LastModified")
+ b.Property<DateTime>("LastModified")
.ValueGeneratedOnAdd()
.HasColumnName("last_modified")
.HasColumnType("TEXT")
@@ -264,7 +264,7 @@ namespace Timeline.Migrations
.HasColumnName("username")
.HasColumnType("TEXT");
- b.Property<DateTimeOffset>("UsernameChangeTime")
+ b.Property<DateTime>("UsernameChangeTime")
.ValueGeneratedOnAdd()
.HasColumnName("username_change_time")
.HasColumnType("TEXT")
@@ -312,9 +312,7 @@ namespace Timeline.Migrations
{
b.HasOne("Timeline.Entities.UserEntity", "Author")
.WithMany("TimelinePosts")
- .HasForeignKey("AuthorId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
+ .HasForeignKey("AuthorId");
b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
.WithMany("Posts")
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<DateTime>
@@ -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/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; }
}
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/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 eafb0088..0070fe3e 100644
--- a/Timeline/Services/TimelineService.cs
+++ b/Timeline/Services/TimelineService.cs
@@ -220,6 +220,12 @@ namespace Timeline.Services
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>
@@ -413,9 +419,7 @@ namespace Timeline.Services
private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
{
-
-
- var author = await _userService.GetUserById(entity.AuthorId);
+ User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(entity.AuthorId.Value) : null;
ITimelinePostContent? content = null;
@@ -561,11 +565,13 @@ namespace Timeline.Services
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);
- var query = _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId);
+ IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
if (!includeDeleted)
{
@@ -574,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<TimelinePost>();
@@ -659,6 +667,8 @@ namespace Timeline.Services
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)
@@ -700,6 +710,8 @@ namespace Timeline.Services
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)
@@ -778,6 +790,35 @@ namespace Timeline.Services
}
}
+ 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)
diff --git a/Timeline/Services/UserDeleteService.cs b/Timeline/Services/UserDeleteService.cs
new file mode 100644
index 00000000..845de573
--- /dev/null
+++ b/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/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 4e56c86a..d9b3da26 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -127,22 +127,6 @@ namespace Timeline.Services
Task<User> ModifyUser(string username, User? info);
/// <summary>
- /// Delete a user of given id.
- /// </summary>
- /// <param name="id">Id of the user to delete.</param>
- /// <returns>True if user is deleted, false if user not exist.</returns>
- Task<bool> DeleteUser(long id);
-
- /// <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);
-
- /// <summary>
/// Try to change a user's password with old password.
/// </summary>
/// <param name="id">The id of user to change password of.</param>
@@ -348,7 +332,7 @@ namespace Timeline.Services
{
if (info != null)
{
- DateTimeOffset now = _clock.GetCurrentTime();
+ var now = _clock.GetCurrentTime();
bool updateLastModified = false;
var username = info.Username;
@@ -428,34 +412,6 @@ namespace Timeline.Services
return CreateUserFromEntity(entity);
}
- public async Task<bool> DeleteUser(long id)
- {
- var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
- if (user == null)
- return false;
-
- _databaseContext.Users.Remove(user);
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username)));
- return true;
- }
-
- public async Task<bool> DeleteUser(string username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username, nameof(username));
-
- var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
- if (user == null)
- return false;
-
- _databaseContext.Users.Remove(user);
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username)));
- return true;
- }
-
public async Task ChangePassword(long id, string oldPassword, string newPassword)
{
if (oldPassword == 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<UserTokenCreateResult> 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 84f0e8ba..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());
@@ -71,6 +74,7 @@ namespace Timeline
services.AddTransient<IPasswordService, PasswordService>();
services.AddScoped<IUserService, UserService>();
+ services.AddScoped<IUserDeleteService, UserDeleteService>();
services.AddScoped<IUserTokenService, JwtUserTokenService>();
services.AddScoped<IUserTokenManager, UserTokenManager>();