diff options
author | 杨宇千 <crupest@outlook.com> | 2019-08-22 15:29:03 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-22 15:29:03 +0800 |
commit | 11f01c56b4ea1dbb09d04258bec89f800c6ee2b6 (patch) | |
tree | af83f8596b4fa78713733c0db6b4b6d1695d0ff0 | |
parent | fd95f9abc017575b13a31dd16ac72ef663e984d6 (diff) | |
parent | 96c18fb2e17c94ff04094608c705db087400f510 (diff) | |
download | timeline-11f01c56b4ea1dbb09d04258bec89f800c6ee2b6.tar.gz timeline-11f01c56b4ea1dbb09d04258bec89f800c6ee2b6.tar.bz2 timeline-11f01c56b4ea1dbb09d04258bec89f800c6ee2b6.zip |
Merge pull request #48 from crupest/user-details
Add user details.
-rw-r--r-- | Timeline.Tests/DatabaseTest.cs | 17 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/UserDetailTest.cs | 146 | ||||
-rw-r--r-- | Timeline.Tests/UserDetailServiceTest.cs | 275 | ||||
-rw-r--r-- | Timeline.Tests/UserDetailValidatorTest.cs | 97 | ||||
-rw-r--r-- | Timeline.Tests/UsernameValidatorUnitTest.cs | 1 | ||||
-rw-r--r-- | Timeline/Controllers/UserDetailController.cs | 96 | ||||
-rw-r--r-- | Timeline/Entities/DatabaseContext.cs | 4 | ||||
-rw-r--r-- | Timeline/Entities/UserDetail.cs | 29 | ||||
-rw-r--r-- | Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs | 136 | ||||
-rw-r--r-- | Timeline/Migrations/20190822072156_AddUserDetail.cs | 47 | ||||
-rw-r--r-- | Timeline/Migrations/DatabaseContextModelSnapshot.cs | 43 | ||||
-rw-r--r-- | Timeline/Models/UserDetail.cs | 43 | ||||
-rw-r--r-- | Timeline/Models/Validation/UserDetailValidator.cs | 116 | ||||
-rw-r--r-- | Timeline/Services/DatabaseExtensions.cs | 30 | ||||
-rw-r--r-- | Timeline/Services/UserAvatarService.cs | 21 | ||||
-rw-r--r-- | Timeline/Services/UserDetailService.cs | 135 | ||||
-rw-r--r-- | Timeline/Startup.cs | 1 |
17 files changed, 1216 insertions, 21 deletions
diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index e280637c..f75ab71b 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -1,5 +1,4 @@ using FluentAssertions;
-using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using Timeline.Entities;
@@ -33,5 +32,21 @@ namespace Timeline.Tests _context.SaveChanges();
_context.UserAvatars.Count().Should().Be(1);
}
+
+ [Fact]
+ public void DeleteUserShouldAlsoDeleteDetail()
+ {
+ var user = _context.Users.First();
+ _context.UserDetails.Add(new UserDetailEntity
+ {
+ UserId = user.Id
+ });
+ _context.SaveChanges();
+ _context.UserDetails.Count().Should().Be(1);
+
+ _context.Users.Remove(user);
+ _context.SaveChanges();
+ _context.UserDetails.Count().Should().Be(0);
+ }
}
}
diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs new file mode 100644 index 00000000..4923cd06 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -0,0 +1,146 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Timeline.Controllers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Timeline.Tests.Mock.Data;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class UserDetailTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
+ {
+ private readonly WebApplicationFactory<Startup> _factory;
+ private readonly Action _disposeAction;
+
+ public UserDetailTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
+ {
+ _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
+ }
+
+ public void Dispose()
+ {
+ _disposeAction();
+ }
+
+ [Fact]
+ public async Task TestAsUser()
+ {
+ using (var client = await _factory.CreateClientAsUser())
+ {
+ {
+ var res = await client.GetAsync($"users/usernotexist/nickname");
+ res.Should().HaveStatusCodeNotFound()
+ .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.GetNickname_UserNotExist);
+ }
+
+ {
+ var res = await client.GetAsync($"users/usernotexist/details");
+ res.Should().HaveStatusCodeNotFound()
+ .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Get_UserNotExist);
+ }
+
+ async Task GetAndTest(UserDetail d)
+ {
+ var res = await client.GetAsync($"users/{MockUsers.UserUsername}/details");
+ res.Should().HaveStatusCodeOk()
+ .And.Should().HaveBodyAsJson<UserDetail>()
+ .Which.Should().BeEquivalentTo(d);
+ }
+
+ await GetAndTest(new UserDetail());
+
+ {
+ var res = await client.PatchAsJsonAsync($"users/{MockUsers.AdminUsername}/details", new UserDetail());
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
+ .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_Forbid);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail
+ {
+ Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ QQ = "aaaaaaa",
+ EMail = "aaaaaa",
+ PhoneNumber = "aaaaaaaa"
+ });
+ var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
+ .And.Should().HaveBodyAsCommonResponse().Which;
+ body.Code.Should().Be(CommonResponse.ErrorCodes.InvalidModel);
+ foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" })
+ {
+ body.Message.Should().ContainEquivalentOf(key);
+ }
+ }
+
+
+ var detail = new UserDetail
+ {
+ Nickname = "aaa",
+ QQ = "1234567",
+ EMail = "aaaa@aaa.net",
+ Description = "aaaaaaaaa"
+ };
+
+ {
+ var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail);
+ res.Should().HaveStatusCodeOk();
+ await GetAndTest(detail);
+ }
+
+ {
+ var res = await client.GetAsync($"users/{MockUsers.UserUsername}/nickname");
+ res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson<UserDetail>()
+ .Which.Should().BeEquivalentTo(new UserDetail
+ {
+ Nickname = detail.Nickname
+ });
+ }
+
+ var detail2 = new UserDetail
+ {
+ QQ = "",
+ PhoneNumber = "12345678910",
+ Description = "bbbbbbbb"
+ };
+
+ {
+ var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail2);
+ res.Should().HaveStatusCodeOk();
+ await GetAndTest(new UserDetail
+ {
+ Nickname = detail.Nickname,
+ QQ = null,
+ EMail = detail.EMail,
+ PhoneNumber = detail2.PhoneNumber,
+ Description = detail2.Description
+ });
+ }
+ }
+ }
+
+ [Fact]
+ public async Task TestAsAdmin()
+ {
+ using (var client = await _factory.CreateClientAsAdmin())
+ {
+ {
+ var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail());
+ res.Should().HaveStatusCodeOk();
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail());
+ res.Should().HaveStatusCodeNotFound()
+ .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_UserNotExist);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs new file mode 100644 index 00000000..f9170c42 --- /dev/null +++ b/Timeline.Tests/UserDetailServiceTest.cs @@ -0,0 +1,275 @@ +using FluentAssertions;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Mock.Data;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests
+{
+ public class UserDetailServiceTest : IDisposable
+ {
+ private readonly LoggerFactory _loggerFactory;
+ private readonly TestDatabase _database;
+
+ private readonly UserDetailService _service;
+
+ public UserDetailServiceTest(ITestOutputHelper outputHelper)
+ {
+ _loggerFactory = MyTestLoggerFactory.Create(outputHelper);
+ _database = new TestDatabase();
+
+ _service = new UserDetailService(_loggerFactory.CreateLogger<UserDetailService>(), _database.DatabaseContext);
+ }
+
+ public void Dispose()
+ {
+ _loggerFactory.Dispose();
+ _database.Dispose();
+ }
+
+ [Fact]
+ public void GetNickname_ShouldThrow_ArgumentException()
+ {
+ // no need to await because arguments are checked syncronizedly.
+ _service.Invoking(s => s.GetUserNickname(null)).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.GetUserNickname("")).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GetNickname_ShouldThrow_UserNotExistException()
+ {
+ const string username = "usernotexist";
+ _service.Awaiting(s => s.GetUserNickname(username)).Should().Throw<UserNotExistException>()
+ .Where(e => e.Username == username);
+ }
+
+ [Fact]
+ public async Task GetNickname_Should_Create_And_ReturnDefault()
+ {
+ {
+ var nickname = await _service.GetUserNickname(MockUsers.UserUsername);
+ nickname.Should().BeNull();
+ }
+
+ {
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var detail = context.UserDetails.Where(e => e.UserId == userId).Single();
+ detail.Nickname.Should().BeNullOrEmpty();
+ detail.QQ.Should().BeNullOrEmpty();
+ detail.EMail.Should().BeNullOrEmpty();
+ detail.PhoneNumber.Should().BeNullOrEmpty();
+ detail.Description.Should().BeNullOrEmpty();
+ }
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("nickname")]
+ public async Task GetNickname_Should_ReturnData(string nickname)
+ {
+ {
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var entity = new UserDetailEntity
+ {
+ Nickname = nickname,
+ UserId = userId
+ };
+ context.Add(entity);
+ await context.SaveChangesAsync();
+ }
+
+ {
+ var n = await _service.GetUserNickname(MockUsers.UserUsername);
+ n.Should().Equals(string.IsNullOrEmpty(nickname) ? null : nickname);
+ }
+ }
+
+ [Fact]
+ public void GetDetail_ShouldThrow_ArgumentException()
+ {
+ // no need to await because arguments are checked syncronizedly.
+ _service.Invoking(s => s.GetUserDetail(null)).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.GetUserDetail("")).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GetDetail_ShouldThrow_UserNotExistException()
+ {
+ const string username = "usernotexist";
+ _service.Awaiting(s => s.GetUserDetail(username)).Should().Throw<UserNotExistException>()
+ .Where(e => e.Username == username);
+ }
+
+ [Fact]
+ public async Task GetDetail_Should_Create_And_ReturnDefault()
+ {
+ {
+ var detail = await _service.GetUserDetail(MockUsers.UserUsername);
+ detail.Should().BeEquivalentTo(new UserDetail());
+ }
+
+ {
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var detail = context.UserDetails.Where(e => e.UserId == userId).Single();
+ detail.Nickname.Should().BeNullOrEmpty();
+ detail.QQ.Should().BeNullOrEmpty();
+ detail.EMail.Should().BeNullOrEmpty();
+ detail.PhoneNumber.Should().BeNullOrEmpty();
+ detail.Description.Should().BeNullOrEmpty();
+ }
+ }
+
+ [Fact]
+ public async Task GetDetail_Should_ReturnData()
+ {
+ const string email = "ha@aaa.net";
+ const string description = "hahaha";
+
+
+ {
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var entity = new UserDetailEntity
+ {
+ EMail = email,
+ Description = description,
+ UserId = userId
+ };
+ context.Add(entity);
+ await context.SaveChangesAsync();
+ }
+
+ {
+ var detail = await _service.GetUserDetail(MockUsers.UserUsername);
+ detail.Should().BeEquivalentTo(new UserDetail
+ {
+ EMail = email,
+ Description = description
+ });
+ }
+ }
+
+ [Fact]
+ public void UpdateDetail_ShouldThrow_ArgumentException()
+ {
+ // no need to await because arguments are checked syncronizedly.
+ _service.Invoking(s => s.UpdateUserDetail(null, new UserDetail())).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "detail");
+ }
+
+ [Fact]
+ public void UpdateDetail_ShouldThrow_UserNotExistException()
+ {
+ const string username = "usernotexist";
+ _service.Awaiting(s => s.UpdateUserDetail(username, new UserDetail())).Should().Throw<UserNotExistException>()
+ .Where(e => e.Username == username);
+ }
+
+ [Fact]
+ public async Task UpdateDetail_Empty_Should_Work()
+ {
+ await _service.UpdateUserDetail(MockUsers.UserUsername, new UserDetail());
+
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var entity = context.UserDetails.Where(e => e.UserId == userId).Single();
+ entity.Nickname.Should().BeNullOrEmpty();
+ entity.QQ.Should().BeNullOrEmpty();
+ entity.EMail.Should().BeNullOrEmpty();
+ entity.PhoneNumber.Should().BeNullOrEmpty();
+ entity.Description.Should().BeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData(nameof(UserDetail.Nickname), nameof(UserDetailEntity.Nickname), "aaaa", "bbbb")]
+ [InlineData(nameof(UserDetail.QQ), nameof(UserDetailEntity.QQ), "12345678910", "987654321")]
+ [InlineData(nameof(UserDetail.EMail), nameof(UserDetailEntity.EMail), "aaa@aaa.aaa", "bbb@bbb.bbb")]
+ [InlineData(nameof(UserDetail.PhoneNumber), nameof(UserDetailEntity.PhoneNumber), "12345678910", "987654321")]
+ [InlineData(nameof(UserDetail.Description), nameof(UserDetailEntity.Description), "descriptionA", "descriptionB")]
+ public async Task UpdateDetail_Single_Should_Work(string propertyName, string entityPropertyName, string mockData1, string mockData2)
+ {
+
+ UserDetail CreateWith(string propertyValue)
+ {
+ var detail = new UserDetail();
+ typeof(UserDetail).GetProperty(propertyName).SetValue(detail, propertyValue);
+ return detail;
+ }
+
+ await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith(mockData1));
+
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var entity = context.UserDetails.Where(e => e.UserId == userId).Single();
+
+ void TestWith(string propertyValue)
+ {
+ typeof(UserDetailEntity).GetProperty(entityPropertyName).GetValue(entity).Should().Equals(propertyValue);
+ foreach (var p in typeof(UserDetailEntity).GetProperties().Where(p => p.Name != entityPropertyName))
+ (p.GetValue(entity) as string).Should().BeNullOrEmpty();
+ }
+
+ TestWith(mockData1);
+
+ await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith(mockData2));
+ TestWith(mockData2);
+ await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith(""));
+ TestWith("");
+ }
+
+ [Fact]
+ public async Task UpdateDetail_Multiple_Should_Work()
+ {
+ var detail = new UserDetail
+ {
+ QQ = "12345678",
+ EMail = "aaa@aaa.aaa",
+ PhoneNumber = "11111111111",
+ Description = "aaaaaaaaaa"
+ };
+
+ await _service.UpdateUserDetail(MockUsers.UserUsername, detail);
+
+ var context = _database.DatabaseContext;
+ var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
+ var entity = context.UserDetails.Where(e => e.UserId == userId).Single();
+ entity.QQ.Should().Equals(detail.QQ);
+ entity.EMail.Should().Equals(detail.EMail);
+ entity.PhoneNumber.Should().Equals(detail.PhoneNumber);
+ entity.Description.Should().Equals(detail.Description);
+
+ var detail2 = new UserDetail
+ {
+ QQ = null,
+ EMail = "bbb@bbb.bbb",
+ PhoneNumber = "",
+ Description = "bbbbbbbbb"
+ };
+
+ await _service.UpdateUserDetail(MockUsers.UserUsername, detail2);
+ entity.QQ.Should().Equals(detail.QQ);
+ entity.EMail.Should().Equals(detail2.EMail);
+ entity.PhoneNumber.Should().BeNullOrEmpty();
+ entity.Description.Should().Equals(detail2.Description);
+ }
+ }
+}
diff --git a/Timeline.Tests/UserDetailValidatorTest.cs b/Timeline.Tests/UserDetailValidatorTest.cs new file mode 100644 index 00000000..9b112946 --- /dev/null +++ b/Timeline.Tests/UserDetailValidatorTest.cs @@ -0,0 +1,97 @@ +using FluentAssertions;
+using System.Collections.Generic;
+using Timeline.Models.Validation;
+using Xunit;
+
+namespace Timeline.Tests
+{
+ public static class UserDetailValidatorsTest
+ {
+ private static void SucceedWith<TValidator>(object value) where TValidator : class, IValidator, new()
+ {
+ var result = new TValidator().Validate(value, out var message);
+ result.Should().BeTrue();
+ message.Should().Equals(ValidationConstants.SuccessMessage);
+ }
+
+ private static void FailWith<TValidator>(object value, params string[] messageContains) where TValidator : class, IValidator, new()
+ {
+ var result = new TValidator().Validate(value, out var message);
+ result.Should().BeFalse();
+
+ foreach (var m in messageContains)
+ {
+ message.Should().ContainEquivalentOf(m);
+ }
+ }
+
+ public class QQ
+ {
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("12345678")]
+ public void Success(object qq)
+ {
+ SucceedWith<UserDetailValidators.QQValidator>(qq);
+ }
+
+ [Theory]
+ [InlineData(123, "type")]
+ [InlineData("123", "short")]
+ [InlineData("111111111111111111111111111111111111", "long")]
+ [InlineData("aaaaaaaa", "digit")]
+ public void Fail(object qq, string messageContains)
+ {
+ FailWith<UserDetailValidators.QQValidator>(qq, messageContains);
+ }
+ }
+
+ public class EMail
+ {
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("aaa@aaa.net")]
+ public void Success(object email)
+ {
+ SucceedWith<UserDetailValidators.EMailValidator>(email);
+ }
+
+ public static IEnumerable<object[]> FailTestData()
+ {
+ yield return new object[] { 123, "type" };
+ yield return new object[] { new string('a', 100), "long" };
+ yield return new object[] { "aaaaaaaa", "format" };
+ }
+
+ [Theory]
+ [MemberData(nameof(FailTestData))]
+ public void Fail(object email, string messageContains)
+ {
+ FailWith<UserDetailValidators.EMailValidator>(email, messageContains);
+ }
+ }
+
+ public class PhoneNumber
+ {
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("12345678910")]
+ public void Success(object phoneNumber)
+ {
+ SucceedWith<UserDetailValidators.PhoneNumberValidator>(phoneNumber);
+ }
+
+ [Theory]
+ [InlineData(123, "type")]
+ [InlineData("111111111111111111111111111111111111", "long")]
+ [InlineData("aaaaaaaa", "digit")]
+ public void Fail(object phoneNumber, string messageContains)
+ {
+ FailWith<UserDetailValidators.PhoneNumberValidator>(phoneNumber, messageContains);
+ }
+ }
+ }
+}
diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 20558d0e..6a635ba1 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -1,5 +1,4 @@ using FluentAssertions;
-using System;
using Timeline.Models.Validation;
using Xunit;
diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs new file mode 100644 index 00000000..5e1183c1 --- /dev/null +++ b/Timeline/Controllers/UserDetailController.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+using Timeline.Authenticate;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Services;
+
+namespace Timeline.Controllers
+{
+ [Route("users/{username}")]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ [ApiController]
+ public class UserDetailController : Controller
+ {
+ public static class ErrorCodes
+ {
+ public const int Get_UserNotExist = -1001;
+
+ public const int Patch_Forbid = -2001;
+ public const int Patch_UserNotExist = -2002;
+
+ public const int GetNickname_UserNotExist = -3001;
+ }
+
+ private readonly ILogger<UserDetailController> _logger;
+ private readonly IUserDetailService _service;
+
+ public UserDetailController(ILogger<UserDetailController> logger, IUserDetailService service)
+ {
+ _logger = logger;
+ _service = service;
+ }
+
+ [HttpGet("nickname")]
+ [UserAuthorize]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> GetNickname([FromRoute] string username)
+ {
+ try
+ {
+ var nickname = await _service.GetUserNickname(username);
+ return Ok(new UserDetail
+ {
+ Nickname = nickname
+ });
+ }
+ catch (UserNotExistException)
+ {
+ return NotFound(new CommonResponse(ErrorCodes.GetNickname_UserNotExist, "The user does not exist."));
+ }
+ }
+
+ [HttpGet("details")]
+ [UserAuthorize]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> Get([FromRoute] string username)
+ {
+ try
+ {
+ var detail = await _service.GetUserDetail(username);
+ return Ok(detail);
+ }
+ catch (UserNotExistException)
+ {
+ return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "The user does not exist."));
+ }
+ }
+
+ [HttpPatch("details")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> Patch([FromRoute] string username, [FromBody] UserDetail detail)
+ {
+ if (!User.IsAdmin() && User.Identity.Name != username)
+ return StatusCode(StatusCodes.Status403Forbidden, new CommonResponse(ErrorCodes.Patch_Forbid, "You can't change other's details unless you are admin."));
+
+ try
+ {
+ await _service.UpdateUserDetail(username, detail);
+ return Ok();
+ }
+ catch (UserNotExistException)
+ {
+ return NotFound(new CommonResponse(ErrorCodes.Patch_UserNotExist, "The user does not exist."));
+ }
+ }
+ }
+}
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 6e1fc638..d9815660 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -28,8 +28,9 @@ namespace Timeline.Entities [Column("version"), Required]
public long Version { get; set; }
- [Required]
public UserAvatar Avatar { get; set; }
+
+ public UserDetailEntity Detail { get; set; }
}
public class DatabaseContext : DbContext
@@ -48,5 +49,6 @@ namespace Timeline.Entities public DbSet<User> Users { get; set; }
public DbSet<UserAvatar> UserAvatars { get; set; }
+ public DbSet<UserDetailEntity> UserDetails { get; set; }
}
}
diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs new file mode 100644 index 00000000..6e582234 --- /dev/null +++ b/Timeline/Entities/UserDetail.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("user_details")]
+ public class UserDetailEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("nickname"), MaxLength(15)]
+ public string Nickname { get; set; }
+
+ [Column("qq"), MaxLength(15)]
+ public string QQ { get; set; }
+
+ [Column("email"), MaxLength(50)]
+ public string EMail { get; set; }
+
+ [Column("phone_number"), MaxLength(15)]
+ public string PhoneNumber { get; set; }
+
+ [Column("description")]
+ public string Description { get; set; }
+
+ public long UserId { get; set; }
+ }
+}
diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs b/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs new file mode 100644 index 00000000..2bbcf673 --- /dev/null +++ b/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs @@ -0,0 +1,136 @@ +// <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("20190822072156_AddUserDetail")]
+ partial class AddUserDetail
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Timeline.Entities.User", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("EncryptedPassword")
+ .IsRequired()
+ .HasColumnName("password");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnName("name")
+ .HasMaxLength(26);
+
+ b.Property<string>("RoleString")
+ .IsRequired()
+ .HasColumnName("roles");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .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");
+
+ b.Property<byte[]>("Data")
+ .HasColumnName("data");
+
+ b.Property<string>("ETag")
+ .HasColumnName("etag")
+ .HasMaxLength(30);
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified");
+
+ b.Property<string>("Type")
+ .HasColumnName("type");
+
+ b.Property<long>("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("Description")
+ .HasColumnName("description");
+
+ b.Property<string>("EMail")
+ .HasColumnName("email")
+ .HasMaxLength(50);
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasMaxLength(15);
+
+ b.Property<string>("PhoneNumber")
+ .HasColumnName("phone_number")
+ .HasMaxLength(15);
+
+ b.Property<string>("QQ")
+ .HasColumnName("qq")
+ .HasMaxLength(15);
+
+ b.Property<long>("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_details");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
+ {
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Detail")
+ .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.cs b/Timeline/Migrations/20190822072156_AddUserDetail.cs new file mode 100644 index 00000000..4aa6446b --- /dev/null +++ b/Timeline/Migrations/20190822072156_AddUserDetail.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddUserDetail : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "user_details",
+ columns: table => new
+ {
+ id = table.Column<long>(nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ nickname = table.Column<string>(maxLength: 15, nullable: true),
+ qq = table.Column<string>(maxLength: 15, nullable: true),
+ email = table.Column<string>(maxLength: 50, nullable: true),
+ phone_number = table.Column<string>(maxLength: 15, nullable: true),
+ description = 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.CreateIndex(
+ name: "IX_user_details_UserId",
+ table: "user_details",
+ column: "UserId",
+ unique: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "user_details");
+ }
+ }
+}
diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 4941321c..1328b855 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -78,6 +78,41 @@ namespace Timeline.Migrations b.ToTable("user_avatars");
});
+ modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("Description")
+ .HasColumnName("description");
+
+ b.Property<string>("EMail")
+ .HasColumnName("email")
+ .HasMaxLength(50);
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasMaxLength(15);
+
+ b.Property<string>("PhoneNumber")
+ .HasColumnName("phone_number")
+ .HasMaxLength(15);
+
+ b.Property<string>("QQ")
+ .HasColumnName("qq")
+ .HasMaxLength(15);
+
+ b.Property<long>("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_details");
+ });
+
modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
{
b.HasOne("Timeline.Entities.User")
@@ -85,6 +120,14 @@ namespace Timeline.Migrations .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
+
+ modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.User")
+ .WithOne("Detail")
+ .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
#pragma warning restore 612, 618
}
}
diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs new file mode 100644 index 00000000..86866d8b --- /dev/null +++ b/Timeline/Models/UserDetail.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations;
+using Timeline.Entities;
+using Timeline.Models.Validation;
+
+namespace Timeline.Models
+{
+ public class UserDetail
+ {
+ [MaxLength(10)]
+ public string Nickname { get; set; }
+
+ [ValidateWith(typeof(UserDetailValidators.QQValidator))]
+ public string QQ { get; set; }
+
+ [ValidateWith(typeof(UserDetailValidators.EMailValidator))]
+ public string EMail { get; set; }
+
+ [ValidateWith(typeof(UserDetailValidators.PhoneNumberValidator))]
+ public string PhoneNumber { get; set; }
+
+ public string Description { get; set; }
+
+ private static string CoerceEmptyToNull(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return null;
+ else
+ return value;
+ }
+
+ public static UserDetail From(UserDetailEntity entity)
+ {
+ return new UserDetail
+ {
+ Nickname = CoerceEmptyToNull(entity.Nickname),
+ QQ = CoerceEmptyToNull(entity.QQ),
+ EMail = CoerceEmptyToNull(entity.EMail),
+ PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber),
+ Description = CoerceEmptyToNull(entity.Description)
+ };
+ }
+ }
+}
diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs new file mode 100644 index 00000000..19c82edb --- /dev/null +++ b/Timeline/Models/Validation/UserDetailValidator.cs @@ -0,0 +1,116 @@ +using System;
+using System.Net.Mail;
+
+namespace Timeline.Models.Validation
+{
+ public abstract class OptionalStringValidator : IValidator
+ {
+ public bool Validate(object value, out string message)
+ {
+ if (value == null)
+ {
+ message = ValidationConstants.SuccessMessage;
+ return true;
+ }
+
+ if (value is string s)
+ {
+ if (s.Length == 0)
+ {
+ message = ValidationConstants.SuccessMessage;
+ return true;
+ }
+ return DoValidate(s, out message);
+ }
+ else
+ {
+ message = "Value is not of type string.";
+ return false;
+ }
+ }
+
+ protected abstract bool DoValidate(string value, out string message);
+ }
+
+ public static class UserDetailValidators
+ {
+
+ public class QQValidator : OptionalStringValidator
+ {
+ protected override bool DoValidate(string value, out string message)
+ {
+ if (value.Length < 5)
+ {
+ message = "QQ is too short.";
+ return false;
+ }
+
+ if (value.Length > 11)
+ {
+ message = "QQ is too long.";
+ return false;
+ }
+
+ foreach (var c in value)
+ {
+ if (!char.IsDigit(c))
+ {
+ message = "QQ must only contain digit.";
+ return false;
+ }
+ }
+
+ message = ValidationConstants.SuccessMessage;
+ return true;
+ }
+ }
+
+ public class EMailValidator : OptionalStringValidator
+ {
+ protected override bool DoValidate(string value, out string message)
+ {
+ if (value.Length > 50)
+ {
+ message = "E-Mail is too long.";
+ return false;
+ }
+
+ try
+ {
+ var _ = new MailAddress(value);
+ }
+ catch (FormatException)
+ {
+ message = "The format of E-Mail is bad.";
+ return false;
+ }
+ message = ValidationConstants.SuccessMessage;
+ return true;
+ }
+ }
+
+ public class PhoneNumberValidator : OptionalStringValidator
+ {
+ protected override bool DoValidate(string value, out string message)
+ {
+ if (value.Length > 14)
+ {
+ message = "Phone number is too long.";
+ return false;
+ }
+
+ foreach (var c in value)
+ {
+ if (!char.IsDigit(c))
+ {
+ message = "Phone number can only contain digit.";
+ return false;
+ }
+ }
+
+ message = ValidationConstants.SuccessMessage;
+ return true;
+ }
+ }
+ }
+}
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs new file mode 100644 index 00000000..a37cf05b --- /dev/null +++ b/Timeline/Services/DatabaseExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ public static class DatabaseExtensions
+ {
+ /// <summary>
+ /// Check the existence and get the id of the user.
+ /// </summary>
+ /// <param name="username">The username of the user.</param>
+ /// <returns>The user id.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
+ public static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string username)
+ {
+ if (string.IsNullOrEmpty(username))
+ throw new ArgumentException("Username is null or empty.", nameof(username));
+
+ var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
+ if (userId == 0)
+ throw new UserNotExistException(username);
+ return userId;
+ }
+ }
+}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 7b1f405c..5c380dd8 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -219,12 +219,7 @@ namespace Timeline.Services public async Task<string> GetAvatarETag(string username)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
-
- var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
- if (userId == 0)
- throw new UserNotExistException(username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag;
if (eTag == null)
@@ -235,12 +230,7 @@ namespace Timeline.Services public async Task<AvatarInfo> GetAvatar(string username)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
-
- var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
- if (userId == 0)
- throw new UserNotExistException(username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync();
@@ -272,9 +262,6 @@ namespace Timeline.Services public async Task SetAvatar(string username, Avatar avatar)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
-
if (avatar != null)
{
if (string.IsNullOrEmpty(avatar.Type))
@@ -283,9 +270,7 @@ namespace Timeline.Services throw new ArgumentException("Data of avatar is null.", nameof(avatar));
}
- var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
- if (userId == 0)
- throw new UserNotExistException(username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync();
diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs new file mode 100644 index 00000000..a8ed662b --- /dev/null +++ b/Timeline/Services/UserDetailService.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+
+namespace Timeline.Services
+{
+ public interface IUserDetailService
+ {
+ /// <summary>
+ /// Get the nickname of user.
+ /// </summary>
+ /// <param name="username">The username to get nickname of.</param>
+ /// <returns>The user's nickname. Null if not set.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
+ Task<string> GetUserNickname(string username);
+
+ /// <summary>
+ /// Get the detail of user.
+ /// </summary>
+ /// <param name="username">The username to get user detail of.</param>
+ /// <returns>The user detail.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
+ Task<UserDetail> GetUserDetail(string username);
+
+ /// <summary>
+ /// Update the detail of user. This function does not do data check.
+ /// </summary>
+ /// <param name="username">The username to get user detail of.</param>
+ /// <param name="detail">The detail to update. Can't be null. Any null member means not set.</param>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty or <paramref name="detail"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
+ Task UpdateUserDetail(string username, UserDetail detail);
+ }
+
+ public class UserDetailService : IUserDetailService
+ {
+ private readonly ILogger<UserDetailService> _logger;
+
+ private readonly DatabaseContext _databaseContext;
+
+ public UserDetailService(ILogger<UserDetailService> logger, DatabaseContext databaseContext)
+ {
+ _logger = logger;
+ _databaseContext = databaseContext;
+ }
+
+ private async Task<UserDetailEntity> CreateEntity(long userId)
+ {
+ var entity = new UserDetailEntity()
+ {
+ UserId = userId
+ };
+ _databaseContext.UserDetails.Add(entity);
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation("An entity is created in user_details.");
+ return entity;
+ }
+
+ // Check the existence of user detail entry
+ private async Task<UserDetailEntity> CheckAndInit(long userId)
+ {
+ var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync();
+ if (detail == null)
+ {
+ detail = await CreateEntity(userId);
+ }
+ return detail;
+ }
+
+ public async Task<string> GetUserNickname(string username)
+ {
+ var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
+ var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).Select(e => new { e.Nickname }).SingleOrDefaultAsync();
+ if (detail == null)
+ {
+ var entity = await CreateEntity(userId);
+ return null;
+ }
+ else
+ {
+ var nickname = detail.Nickname;
+ return string.IsNullOrEmpty(nickname) ? null : nickname;
+ }
+ }
+
+ public async Task<UserDetail> GetUserDetail(string username)
+ {
+ var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
+ var detailEntity = await CheckAndInit(userId);
+ return UserDetail.From(detailEntity);
+ }
+
+ public async Task UpdateUserDetail(string username, UserDetail detail)
+ {
+ if (detail == null)
+ throw new ArgumentNullException(nameof(detail));
+
+ var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username);
+ var detailEntity = await CheckAndInit(userId);
+
+ if (detail.Nickname != null)
+ detailEntity.Nickname = detail.Nickname;
+
+ if (detail.QQ != null)
+ detailEntity.QQ = detail.QQ;
+
+ if (detail.EMail != null)
+ detailEntity.EMail = detail.EMail;
+
+ if (detail.PhoneNumber != null)
+ detailEntity.PhoneNumber = detail.PhoneNumber;
+
+ if (detail.Description != null)
+ detailEntity.Description = detail.Description;
+
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation("An entity is updated in user_details.");
+ }
+ }
+
+ public static class UserDetailServiceCollectionExtensions
+ {
+ public static void AddUserDetailService(this IServiceCollection services)
+ {
+ services.AddScoped<IUserDetailService, UserDetailService>();
+ }
+ }
+}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 66f648c3..b5a5106b 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -46,6 +46,7 @@ namespace Timeline services.AddTransient<IClock, Clock>();
services.AddUserAvatarService();
+ services.AddUserDetailService();
var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>();
|