diff options
author | crupest <crupest@outlook.com> | 2020-01-29 00:17:45 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-01-29 00:17:45 +0800 |
commit | b6043126fae039c58512f60a576b10925b06df4c (patch) | |
tree | 480e4e0fa03f0736cfee97876603fdb87d3fd3bd | |
parent | 1a653fca9e4e3371dd65782c987a736e2259d66a (diff) | |
download | timeline-b6043126fae039c58512f60a576b10925b06df4c.tar.gz timeline-b6043126fae039c58512f60a576b10925b06df4c.tar.bz2 timeline-b6043126fae039c58512f60a576b10925b06df4c.zip |
...
36 files changed, 1086 insertions, 407 deletions
diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 61fbe950..43e1a413 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -42,7 +42,7 @@ namespace Timeline.Tests.Controllers var mockCreateResult = new UserTokenCreateResult
{
Token = "mocktokenaaaaa",
- User = new UserInfo
+ User = new Models.User
{
Id = 1,
Username = MockUser.User.Username,
@@ -99,7 +99,7 @@ namespace Timeline.Tests.Controllers public async Task Verify_Ok()
{
const string token = "aaaaaaaaaaaaaa";
- _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new UserInfo
+ _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new Models.User
{
Id = 1,
Username = MockUser.User.Username,
diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index a1035675..192d53dd 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -36,9 +36,9 @@ namespace Timeline.Tests.Controllers [Fact]
public async Task GetList_Success()
{
- var mockUserList = new UserInfo[] {
- new UserInfo { Id = 1, Username = "aaa", Administrator = true, Version = 1 },
- new UserInfo { Id = 2, Username = "bbb", Administrator = false, Version = 1 }
+ var mockUserList = new Models.User[] {
+ new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 },
+ new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 }
};
_mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList);
var action = await _controller.List();
@@ -51,7 +51,7 @@ namespace Timeline.Tests.Controllers public async Task Get_Success()
{
const string username = "aaa";
- _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new UserInfo
+ _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new Models.User
{
Id = 1,
Username = MockUser.User.Username,
diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs index 3163279a..e29a71fa 100644 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -18,9 +18,9 @@ namespace Timeline.Tests.Helpers { return new UserEntity { - Name = user.Username, - EncryptedPassword = PasswordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), + Username = user.Username, + Password = PasswordService.HashPassword(user.Password), + Roles = UserRoleConvert.ToString(user.Administrator), Avatar = null }; } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index d4371c48..2dca7ccf 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -171,7 +171,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa");
{
var context = _database.Context;
- var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
+ var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
user.Avatar = mockAvatarEntity;
await context.SaveChangesAsync();
}
@@ -205,7 +205,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa");
{
var context = _database.Context;
- var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
+ var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
user.Avatar = mockAvatarEntity;
await context.SaveChangesAsync();
}
@@ -236,7 +236,7 @@ namespace Timeline.Tests.Services {
string username = MockUser.User.Username;
- var user = await _database.Context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
+ var user = await _database.Context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
var avatar1 = CreateMockAvatar("aaa");
var avatar2 = CreateMockAvatar("bbb");
diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index e6eabadf..dbff2705 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -51,7 +51,7 @@ namespace Timeline.Tests.Services const string nickname = "aaaaaa";
{
var context = _testDatabase.Context;
- var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id;
+ var userId = (await context.Users.Where(u => u.Username == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id;
context.UserDetails.Add(new UserDetailEntity
{
Nickname = nickname,
@@ -83,7 +83,7 @@ namespace Timeline.Tests.Services public async Task SetNickname_ShouldWork()
{
var username = MockUser.User.Username;
- var user = await _testDatabase.Context.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync();
+ var user = await _testDatabase.Context.Users.Where(u => u.Username == username).Include(u => u.Detail).SingleAsync();
var nickname1 = "nickname1";
var nickname2 = "nickname2";
diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs index 19122d31..e649fbab 100644 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Services;
@@ -58,7 +56,7 @@ namespace Timeline.Tests.Services const string username = "uuu";
const string password = "ppp";
var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null;
- var mockUserInfo = new UserInfo
+ var mockUserInfo = new User
{
Id = 1,
Username = username,
@@ -126,7 +124,7 @@ namespace Timeline.Tests.Services ExpireAt = mockTime.AddDays(1)
};
_mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo);
- _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new UserInfo
+ _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new User
{
Id = 1,
Username = "aaa",
@@ -149,7 +147,7 @@ namespace Timeline.Tests.Services Version = 1,
ExpireAt = mockTime.AddDays(1)
};
- var mockUserInfo = new UserInfo
+ var mockUserInfo = new User
{
Id = 1,
Username = "aaa",
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index a96b6fa9..9724c1a6 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -20,9 +20,9 @@ namespace Timeline.Controllers private readonly ILogger<TokenController> _logger;
private readonly IClock _clock;
- private static User CreateUserFromUserInfo(UserInfo userInfo)
+ private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo)
{
- return new User
+ return new Models.Http.User
{
Username = userInfo.Username,
Administrator = userInfo.Administrator
diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs deleted file mode 100644 index 9de9899e..00000000 --- a/Timeline/Controllers/UserDetailController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.AspNetCore.Mvc;
-using System.Threading.Tasks;
-using Timeline.Filters;
-using Timeline.Models.Validation;
-using Timeline.Services;
-using System.ComponentModel.DataAnnotations;
-using Microsoft.AspNetCore.Authorization;
-
-namespace Timeline.Controllers
-{
- [ApiController]
- public class UserDetailController : Controller
- {
- private readonly IUserDetailService _service;
-
- public UserDetailController(IUserDetailService service)
- {
- _service = service;
- }
-
- [HttpGet("users/{username}/nickname")]
- [CatchUserNotExistException]
- public async Task<ActionResult<string>> GetNickname([FromRoute][Username] string username)
- {
- return Ok(await _service.GetNickname(username));
- }
-
- [HttpPut("users/{username}/nickname")]
- [Authorize]
- [SelfOrAdmin]
- [CatchUserNotExistException]
- public async Task<ActionResult> PutNickname([FromRoute][Username] string username,
- [FromBody][StringLength(10, MinimumLength = 1)] string body)
- {
- await _service.SetNickname(username, body);
- return Ok();
- }
-
- [HttpDelete("users/{username}/nickname")]
- [Authorize]
- [SelfOrAdmin]
- [CatchUserNotExistException]
- public async Task<ActionResult> DeleteNickname([FromRoute][Username] string username)
- {
- await _service.SetNickname(username, null);
- return Ok();
- }
- }
-}
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 738440b2..ac4ad7b2 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -14,7 +14,7 @@ namespace Timeline.Entities protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserEntity>().Property(e => e.Version).HasDefaultValue(0);
- modelBuilder.Entity<UserEntity>().HasIndex(e => e.Name).IsUnique();
+ modelBuilder.Entity<UserEntity>().HasIndex(e => e.Username).IsUnique();
}
public DbSet<UserEntity> Users { get; set; } = default!;
diff --git a/Timeline/Entities/UserDetailEntity.cs b/Timeline/Entities/UserDetailEntity.cs deleted file mode 100644 index 1d9957f9..00000000 --- a/Timeline/Entities/UserDetailEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -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(26)]
- public string? Nickname { get; set; }
-
- [Column("user")]
- public long UserId { get; set; }
-
- [ForeignKey(nameof(UserId))]
- public UserEntity User { get; set; } = default!;
- }
-}
diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index 83ef5621..dae6979f 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -17,21 +17,22 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
- [Column("name"), MaxLength(26), Required]
- public string Name { get; set; } = default!;
+ [Column("username"), MaxLength(26), Required]
+ public string Username { get; set; } = default!;
[Column("password"), Required]
- public string EncryptedPassword { get; set; } = default!;
+ public string Password { get; set; } = default!;
[Column("roles"), Required]
- public string RoleString { get; set; } = default!;
+ public string Roles { get; set; } = default!;
[Column("version"), Required]
public long Version { get; set; }
- public UserAvatarEntity? Avatar { get; set; }
+ [Column("nickname"), MaxLength(40)]
+ public string? Nickname { get; set; }
- public UserDetailEntity? Detail { get; set; }
+ public UserAvatarEntity? Avatar { get; set; }
public List<TimelineEntity> Timelines { get; set; } = default!;
diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index c0754071..d27b3c16 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -10,3 +10,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 69bfacf2..b3812f48 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -1,14 +1,10 @@ +using System;
using System.ComponentModel.DataAnnotations;
using Timeline.Models.Validation;
namespace Timeline.Models.Http
{
- public class User
- {
- public string Username { get; set; } = default!;
- public bool Administrator { get; set; }
- }
-
+ [Obsolete("Remove this.")]
public class UserPutRequest
{
[Required]
@@ -17,6 +13,7 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; }
}
+ [Obsolete("Remove this.")]
public class UserPatchRequest
{
public string? Password { get; set; }
diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs new file mode 100644 index 00000000..05395022 --- /dev/null +++ b/Timeline/Models/User.cs @@ -0,0 +1,19 @@ +using Timeline.Models.Validation;
+
+namespace Timeline.Models
+{
+ public class User
+ {
+ [Username]
+ public string? Username { get; set; }
+ public bool? Administrator { get; set; }
+ public string? Nickname { get; set; }
+ public string? AvatarUrl { get; set; }
+
+
+ #region secret
+ public string? Password { get; set; }
+ public long? Version { get; set; }
+ #endregion secret
+ }
+}
diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs deleted file mode 100644 index eff47329..00000000 --- a/Timeline/Models/UserInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Timeline.Models
-{
- public class UserInfo
- {
- public long Id { get; set; }
- public long Version { get; set; }
- public string Username { get; set; } = default!;
- public bool Administrator { get; set; }
- }
-}
diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a16f6f81..ead7dbef 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -20,24 +20,46 @@ namespace Timeline.Models.Validation (bool, string) Validate(object? value);
}
+ public static class ValidatorExtensions
+ {
+ public static bool Validate(this IValidator validator, object? value, out string message)
+ {
+ if (validator == null)
+ throw new ArgumentNullException(nameof(validator));
+
+ var (r, m) = validator.Validate(value);
+ message = m;
+ return r;
+ }
+ }
+
/// <summary>
/// Convenient base class for validator.
/// </summary>
/// <typeparam name="T">The type of accepted value.</typeparam>
/// <remarks>
/// Subclass should override <see cref="DoValidate(T, out string)"/> to do the real validation.
- /// This class will check the nullity and type of value. If value is null or not of type <typeparamref name="T"/>
- /// it will return false and not call <see cref="DoValidate(T, out string)"/>.
+ /// This class will check the nullity and type of value.
+ /// If value is null, it will pass or fail depending on <see cref="PermitNull"/>.
+ /// If value is not null and not of type <typeparamref name="T"/>
+ /// it will fail and not call <see cref="DoValidate(T, out string)"/>.
+ ///
+ /// <see cref="PermitNull"/> is true by default.
///
/// If you want some other behaviours, write the validator from scratch.
/// </remarks>
public abstract class Validator<T> : IValidator
{
+ protected bool PermitNull { get; set; } = true;
+
public (bool, string) Validate(object? value)
{
if (value == null)
{
- return (false, ValidatorMessageNull);
+ if (PermitNull)
+ return (true, GetSuccessMessage());
+ else
+ return (false, ValidatorMessageNull);
}
if (value is T v)
diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs new file mode 100644 index 00000000..e7630d26 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class PasswordValidator {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal PasswordValidator() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.PasswordValidator", typeof(PasswordValidator).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password can't be empty..
+ /// </summary>
+ internal static string MessageEmptyString {
+ get {
+ return ResourceManager.GetString("MessageEmptyString", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.resx b/Timeline/Resources/Models/Validation/PasswordValidator.resx new file mode 100644 index 00000000..f445cc75 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageEmptyString" xml:space="preserve">
+ <value>Password can't be empty.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx new file mode 100644 index 00000000..9eab7b4e --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageEmptyString" xml:space="preserve">
+ <value>密码不能为空。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 0a3325d4..671c4b93 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -241,6 +241,15 @@ namespace Timeline.Resources.Services { }
/// <summary>
+ /// Looks up a localized string similar to Password is of bad format..
+ /// </summary>
+ internal static string PasswordBadFormatException {
+ get {
+ return ResourceManager.GetString("PasswordBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The timeline with that name already exists..
/// </summary>
internal static string TimelineAlreadyExistException {
@@ -304,15 +313,6 @@ namespace Timeline.Resources.Services { }
/// <summary>
- /// Looks up a localized string similar to The username is of bad format..
- /// </summary>
- internal static string UsernameBadFormatException {
- get {
- return ResourceManager.GetString("UsernameBadFormatException", resourceCulture);
- }
- }
-
- /// <summary>
/// Looks up a localized string similar to The username already exists..
/// </summary>
internal static string UsernameConfictException {
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index bc96248d..3ae14d4e 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -177,6 +177,9 @@ <data name="JwtUserTokenBadFormatExceptionVersionMissing" xml:space="preserve">
<value>version claim does not exist.</value>
</data>
+ <data name="PasswordBadFormatException" xml:space="preserve">
+ <value>Password is of bad format.</value>
+ </data>
<data name="TimelineAlreadyExistException" xml:space="preserve">
<value>The timeline with that name already exists.</value>
</data>
@@ -198,9 +201,6 @@ <data name="TimelineUserNotMemberException" xml:space="preserve">
<value>The use is not a member of the timeline.</value>
</data>
- <data name="UsernameBadFormatException" xml:space="preserve">
- <value>The username is of bad format.</value>
- </data>
<data name="UsernameConfictException" xml:space="preserve">
<value>The username already exists.</value>
</data>
diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs new file mode 100644 index 00000000..28a74a6c --- /dev/null +++ b/Timeline/Resources/Services/UserCache.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserCache {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserCache() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Found user info from cache. Entry: {0} ..
+ /// </summary>
+ internal static string LogGetCacheExist {
+ get {
+ return ResourceManager.GetString("LogGetCacheExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User info not exist in cache. Id: {0} ..
+ /// </summary>
+ internal static string LogGetCacheNotExist {
+ get {
+ return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User info remove in cache. Id: {0} ..
+ /// </summary>
+ internal static string LogRemoveCache {
+ get {
+ return ResourceManager.GetString("LogRemoveCache", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User info set in cache. Entry: {0} ..
+ /// </summary>
+ internal static string LogSetCache {
+ get {
+ return ResourceManager.GetString("LogSetCache", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx new file mode 100644 index 00000000..1102108b --- /dev/null +++ b/Timeline/Resources/Services/UserCache.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogGetCacheExist" xml:space="preserve">
+ <value>Found user info from cache. Entry: {0} .</value>
+ </data>
+ <data name="LogGetCacheNotExist" xml:space="preserve">
+ <value>User info not exist in cache. Id: {0} .</value>
+ </data>
+ <data name="LogRemoveCache" xml:space="preserve">
+ <value>User info remove in cache. Id: {0} .</value>
+ </data>
+ <data name="LogSetCache" xml:space="preserve">
+ <value>User info set in cache. Entry: {0} .</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/UserManager.Designer.cs b/Timeline/Resources/Services/UserManager.Designer.cs new file mode 100644 index 00000000..424499f8 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class UserManager {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserManager() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserManager", typeof(UserManager).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has been created..
+ /// </summary>
+ internal static string LogUserCreate {
+ get {
+ return ResourceManager.GetString("LogUserCreate", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Services/UserManager.resx b/Timeline/Resources/Services/UserManager.resx new file mode 100644 index 00000000..ecb89179 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogUserCreate" xml:space="preserve">
+ <value>A user has been created.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs index 2a04dded..1b85546d 100644 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -79,6 +79,42 @@ namespace Timeline.Resources.Services { }
/// <summary>
+ /// Looks up a localized string similar to Password can't be empty..
+ /// </summary>
+ internal static string ExceptionPasswordEmpty {
+ get {
+ return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password can't be null or empty..
+ /// </summary>
+ internal static string ExceptionPasswordNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ExceptionPasswordNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Username is of bad format, because {}..
+ /// </summary>
+ internal static string ExceptionUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Username can't be null or empty..
+ /// </summary>
+ internal static string ExceptionUsernameNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ExceptionUsernameNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to A cache entry is created..
/// </summary>
internal static string LogCacheCreate {
diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx index 3670d8f9..26221770 100644 --- a/Timeline/Resources/Services/UserService.resx +++ b/Timeline/Resources/Services/UserService.resx @@ -123,6 +123,18 @@ <data name="ExceptionOldUsernameBadFormat" xml:space="preserve">
<value>Old username is of bad format.</value>
</data>
+ <data name="ExceptionPasswordEmpty" xml:space="preserve">
+ <value>Password can't be empty.</value>
+ </data>
+ <data name="ExceptionPasswordNullOrEmpty" xml:space="preserve">
+ <value>Password can't be null or empty.</value>
+ </data>
+ <data name="ExceptionUsernameBadFormat" xml:space="preserve">
+ <value>Username is of bad format, because {}.</value>
+ </data>
+ <data name="ExceptionUsernameNullOrEmpty" xml:space="preserve">
+ <value>Username can't be null or empty.</value>
+ </data>
<data name="LogCacheCreate" xml:space="preserve">
<value>A cache entry is created.</value>
</data>
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index c5c96d8c..e77dd01a 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -27,7 +27,7 @@ namespace Timeline.Services if (!result)
throw new UsernameBadFormatException(username, message);
- var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
+ var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync();
if (userId == 0)
throw new UserNotExistException(username);
return userId;
diff --git a/Timeline/Services/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs new file mode 100644 index 00000000..2029ebb4 --- /dev/null +++ b/Timeline/Services/PasswordBadFormatException.cs @@ -0,0 +1,27 @@ +using System;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class PasswordBadFormatException : Exception
+ {
+ public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { }
+ public PasswordBadFormatException(string message) : base(message) { }
+ public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public PasswordBadFormatException(string password, string validationMessage) : this()
+ {
+ Password = password;
+ ValidationMessage = validationMessage;
+ }
+
+ protected PasswordBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Password { get; set; } = "";
+
+ public string ValidationMessage { get; set; } = "";
+ }
+}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f7b0e0e9..f43d2de5 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -356,7 +356,7 @@ namespace Timeline.Services {
Id = entity.Id,
Content = entity.Content,
- Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
+ Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name,
Time = entity.Time
});
}
@@ -382,7 +382,7 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name);
- var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
+ var authorEntity = Database.Users.Where(u => u.Username == author).Select(u => new { u.Id }).SingleOrDefault();
if (authorEntity == null)
{
throw new UserNotExistException(author);
@@ -508,7 +508,7 @@ namespace Timeline.Services List<long> result = new List<long>();
foreach (var (username, index) in map)
{
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
throw new TimelineMemberOperationUserException(index, operation, username,
@@ -550,7 +550,7 @@ namespace Timeline.Services throw new UsernameBadFormatException(username);
}
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
@@ -596,7 +596,7 @@ namespace Timeline.Services }
}
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
@@ -632,7 +632,7 @@ namespace Timeline.Services }
}
- var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
@@ -672,7 +672,7 @@ namespace Timeline.Services }
}
- var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (userEntity == null)
{
@@ -715,7 +715,7 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
- var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray();
+ var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray();
var memberUsernames = await Task.WhenAll(memberUsernameTasks);
diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs deleted file mode 100644 index 4f4a7942..00000000 --- a/Timeline/Services/UserDetailService.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.Extensions.Logging;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using static Timeline.Resources.Services.UserDetailService;
-
-namespace Timeline.Services
-{
- public interface IUserDetailService
- {
- /// <summary>
- /// Get the nickname of the user with given username.
- /// If the user does not set a nickname, the username is returned as the nickname.
- /// </summary>
- /// <param name="username">The username of the user to get nickname of.</param>
- /// <returns>The nickname of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user does not exist.</exception>
- Task<string> GetNickname(string username);
-
- /// <summary>
- /// Set the nickname of the user with given username.
- /// </summary>
- /// <param name="username">The username of the user to set nickname of.</param>
- /// <param name="nickname">The nickname. Pass null to unset.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="nickname"/> is not null but its length is bigger than 10.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user does not exist.</exception>
- Task SetNickname(string username, string? nickname);
- }
-
- public class UserDetailService : IUserDetailService
- {
- private readonly DatabaseContext _database;
-
- private readonly ILogger<UserDetailService> _logger;
-
- public UserDetailService(DatabaseContext database, ILogger<UserDetailService> logger)
- {
- _database = database;
- _logger = logger;
- }
-
- public async Task<string> GetNickname(string username)
- {
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var nickname = _database.UserDetails.Where(d => d.UserId == userId).Select(d => new { d.Nickname }).SingleOrDefault()?.Nickname;
- return nickname ?? username;
- }
-
- public async Task SetNickname(string username, string? nickname)
- {
- if (nickname != null && nickname.Length > 10)
- {
- throw new ArgumentException(ExceptionNicknameTooLong, nameof(nickname));
- }
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var userDetail = _database.UserDetails.Where(d => d.UserId == userId).SingleOrDefault();
- if (nickname == null)
- {
- if (userDetail == null || userDetail.Nickname == null)
- {
- return;
- }
- else
- {
- userDetail.Nickname = null;
- await _database.SaveChangesAsync();
- _logger.LogInformation(LogEntityNicknameSetToNull, userId);
- }
- }
- else
- {
- var create = userDetail == null;
- if (create)
- {
- userDetail = new UserDetailEntity
- {
- UserId = userId
- };
- }
- userDetail!.Nickname = nickname;
- if (create)
- {
- _database.UserDetails.Add(userDetail);
- }
- await _database.SaveChangesAsync();
- if (create)
- {
- _logger.LogInformation(LogEntityNicknameCreate, userId, nickname);
- }
- else
- {
- _logger.LogInformation(LogEntityNicknameSetNotNull, userId, nickname);
- }
- }
- }
- }
-}
diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs index c7317f56..fd0b5ecf 100644 --- a/Timeline/Services/UserNotExistException.cs +++ b/Timeline/Services/UserNotExistException.cs @@ -31,11 +31,11 @@ namespace Timeline.Services /// <summary>
/// The username of the user that does not exist.
/// </summary>
- public string? Username { get; set; }
+ public string Username { get; set; } = "";
/// <summary>
/// The id of the user that does not exist.
/// </summary>
- public long? Id { get; set; }
+ public long Id { get; set; }
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 104db1b0..c5595c99 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,13 +1,14 @@ using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
+using static Timeline.Resources.Services.UserService;
namespace Timeline.Services
{
@@ -18,12 +19,12 @@ namespace Timeline.Services /// </summary>
/// <param name="username">The username of the user to verify.</param>
/// <param name="password">The password of the user to verify.</param>
- /// <returns>The user info.</returns>
+ /// <returns>The user info and auth info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when username is of bad format.</exception>
+ /// <exception cref="ArgumentException">Thrown when username is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<UserInfo> VerifyCredential(string username, string password);
+ Task<User> VerifyCredential(string username, string password);
/// <summary>
/// Try to get a user by id.
@@ -31,7 +32,7 @@ namespace Timeline.Services /// <param name="id">The id of the user.</param>
/// <returns>The user info.</returns>
/// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
- Task<UserInfo> GetUserById(long id);
+ Task<User> GetUserById(long id);
/// <summary>
/// Get the user info of given username.
@@ -39,30 +40,51 @@ namespace Timeline.Services /// <param name="username">Username of the user.</param>
/// <returns>The info of the user.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<UserInfo> GetUserByUsername(string username);
+ Task<User> GetUserByUsername(string username);
/// <summary>
/// List all users.
/// </summary>
/// <returns>The user info of users.</returns>
- Task<UserInfo[]> ListUsers();
+ Task<User[]> ListUsers();
/// <summary>
- /// Create or modify a user with given username.
- /// Username must be match with [a-zA-z0-9-_].
+ /// Create a user with given info.
/// </summary>
- /// <param name="username">Username of user.</param>
- /// <param name="password">Password of user.</param>
- /// <param name="administrator">Whether the user is administrator.</param>
- /// <returns>
- /// Return <see cref="PutResult.Create"/> if a new user is created.
- /// Return <see cref="PutResult.Modify"/> if a existing user is modified.
- /// </returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- Task<PutResult> PutUser(string username, string password, bool administrator);
+ /// <param name="info">The info of new user.</param>
+ /// <param name="password">The password, can't be null or empty.</param>
+ /// <returns>The id of the new user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="info"/>is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UsernameConfictException">Thrown when a user with given username already exists.</exception>
+ /// <remarks>
+ /// <see cref="User.Username"/> must not be null and must be a valid username.
+ /// <see cref="User.Password"/> must not be null or empty.
+ /// <see cref="User.Administrator"/> is false by default (null).
+ /// Other fields are ignored.
+ /// </remarks>
+ Task<long> CreateUser(User info);
+
+ /// <summary>
+ /// Modify a user's info.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <param name="info">The new info. May be null.</param>
+ /// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <remarks>
+ /// Only <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
+ /// If null, then not change.
+ /// Other fields are ignored.
+ /// After modified, even if nothing is changed, version will increase.
+ ///
+ /// <see cref="User.Password"/> can't be empty.
+ ///
+ /// Note: Whether <see cref="User.Version"/> is set or not, version will increase and not set to the specified value if there is one.
+ /// </remarks>
+ Task ModifyUser(long id, User? info);
/// <summary>
/// Partially modify a user of given username.
@@ -116,181 +138,164 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext;
- private readonly IMemoryCache _memoryCache;
private readonly IPasswordService _passwordService;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
- public UserService(ILogger<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService)
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService)
{
_logger = logger;
- _memoryCache = memoryCache;
_databaseContext = databaseContext;
_passwordService = passwordService;
}
- private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
-
- private void RemoveCache(long id)
+ private void CheckUsernameFormat(string username, string? paramName, Func<string, string>? messageBuilder = null)
{
- var key = GenerateCacheKeyByUserId(id);
- _memoryCache.Remove(key);
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key)));
- }
-
- private void CheckUsernameFormat(string username, string? message = null)
- {
- var (result, validationMessage) = _usernameValidator.Validate(username);
- if (!result)
+ if (!_usernameValidator.Validate(username, out var message))
{
- if (message == null)
- throw new UsernameBadFormatException(username, validationMessage);
+ if (messageBuilder == null)
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName);
else
- throw new UsernameBadFormatException(username, validationMessage, message);
+ throw new ArgumentException(messageBuilder(message), paramName);
}
}
- private static UserInfo CreateUserInfoFromEntity(UserEntity user)
+ private static User CreateUserFromEntity(UserEntity entity)
{
- return new UserInfo
+ return new User
{
- Id = user.Id,
- Username = user.Name,
- Administrator = UserRoleConvert.ToBool(user.RoleString),
- Version = user.Version
+ Username = entity.Username,
+ Administrator = UserRoleConvert.ToBool(entity.Roles),
+ Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
+ Version = entity.Version
};
}
- public async Task<UserInfo> VerifyCredential(string username, string password)
+ public async Task<User> VerifyCredential(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
- CheckUsernameFormat(username);
+ CheckUsernameFormat(username, nameof(username));
- // We need password info, so always check the database.
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
- if (user == null)
+ if (entity == null)
throw new UserNotExistException(username);
- if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
+ if (!_passwordService.VerifyPassword(entity.Password, password))
throw new BadPasswordException(password);
- return CreateUserInfoFromEntity(user);
+ return CreateUserFromEntity(entity);
}
- public async Task<UserInfo> GetUserById(long id)
+ public async Task<User> GetUserById(long id)
{
- var key = GenerateCacheKeyByUserId(id);
- if (!_memoryCache.TryGetValue<UserInfo>(key, out var cache))
- {
- // no cache, check the database
- var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
-
- if (user == null)
- throw new UserNotExistException(id);
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
- // create cache
- cache = CreateUserInfoFromEntity(user);
- _memoryCache.CreateEntry(key).SetValue(cache);
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
- }
+ if (user == null)
+ throw new UserNotExistException(id);
- return cache;
+ return CreateUserFromEntity(user);
}
- public async Task<UserInfo> GetUserByUsername(string username)
+ public async Task<User> GetUserByUsername(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username);
+ CheckUsernameFormat(username, nameof(username));
- var entity = await _databaseContext.Users
- .Where(user => user.Name == username)
- .SingleOrDefaultAsync();
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync();
if (entity == null)
throw new UserNotExistException(username);
- return CreateUserInfoFromEntity(entity);
+ return CreateUserFromEntity(entity);
}
- public async Task<UserInfo[]> ListUsers()
+ public async Task<User[]> ListUsers()
{
var entities = await _databaseContext.Users.ToArrayAsync();
- return entities.Select(user => CreateUserInfoFromEntity(user)).ToArray();
+ return entities.Select(user => CreateUserFromEntity(user)).ToArray();
}
- public async Task<PutResult> PutUser(string username, string password, bool administrator)
+ public async Task<long> CreateUser(User info)
{
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- if (password == null)
- throw new ArgumentNullException(nameof(password));
- CheckUsernameFormat(username);
+ if (info == null)
+ throw new ArgumentNullException(nameof(info));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ if (string.IsNullOrEmpty(info.Username))
+ throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info));
- if (user == null)
- {
- var newUser = new UserEntity
- {
- Name = username,
- EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = UserRoleConvert.ToString(administrator),
- Avatar = null
- };
- await _databaseContext.AddAsync(newUser);
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate,
- ("Id", newUser.Id), ("Username", username), ("Administrator", administrator)));
- return PutResult.Create;
- }
+ CheckUsernameFormat(info.Username, nameof(info));
- user.EncryptedPassword = _passwordService.HashPassword(password);
- user.RoleString = UserRoleConvert.ToString(administrator);
- user.Version += 1;
+ if (string.IsNullOrEmpty(info.Password))
+ throw new ArgumentException(ExceptionPasswordNullOrEmpty);
+
+ var username = info.Username;
+
+ var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+
+ if (conflict)
+ throw new UsernameConfictException(username);
+
+ var administrator = info.Administrator ?? false;
+ var password = info.Password;
+
+ var newEntity = new UserEntity
+ {
+ Username = username,
+ Password = _passwordService.HashPassword(password),
+ Roles = UserRoleConvert.ToString(administrator),
+ Version = 1
+ };
+ _databaseContext.Users.Add(newEntity);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
- ("Id", user.Id), ("Username", username), ("Administrator", administrator)));
- //clear cache
- RemoveCache(user.Id);
+ _logger.LogInformation(Log.Format(LogDatabaseCreate,
+ ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator)));
- return PutResult.Modify;
+ return newEntity.Id;
}
- public async Task PatchUser(string username, string? password, bool? administrator)
+ public async Task ModifyUser(long id, User? info)
{
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- CheckUsernameFormat(username);
+ if (info != null && info.Password != null && info.Password.Length == 0)
+ throw new ArgumentException(ExceptionPasswordEmpty, nameof(info));
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
- if (user == null)
- throw new UserNotExistException(username);
+ var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ if (entity == null)
+ throw new UserNotExistException(id);
- if (password != null)
+ if (info != null)
{
- user.EncryptedPassword = _passwordService.HashPassword(password);
- }
+ var password = info.Password;
+ if (password != null)
+ {
+ entity.Password = _passwordService.HashPassword(password);
+ }
- if (administrator != null)
- {
- user.RoleString = UserRoleConvert.ToString(administrator.Value);
+ var administrator = info.Administrator;
+ if (administrator.HasValue)
+ {
+ entity.Roles = UserRoleConvert.ToString(administrator.Value);
+ }
+
+ var nickname = info.Nickname;
+ if (nickname != null)
+ {
+ entity.Nickname = nickname;
+ }
}
- user.Version += 1;
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id));
+ entity.Version += 1;
- //clear cache
- RemoveCache(user.Id);
+ await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(LogDatabaseUpdate, ("Id", id));
}
public async Task DeleteUser(string username)
@@ -299,7 +304,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username));
CheckUsernameFormat(username);
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
if (user == null)
throw new UserNotExistException(username);
@@ -309,7 +314,7 @@ namespace Timeline.Services ("Id", user.Id)));
//clear cache
- RemoveCache(user.Id);
+ await _cache.RemoveCache(user.Id);
}
public async Task ChangePassword(string username, string oldPassword, string newPassword)
@@ -322,21 +327,21 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(newPassword));
CheckUsernameFormat(username);
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
if (user == null)
throw new UserNotExistException(username);
- var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword);
+ var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword);
if (!verifyResult)
throw new BadPasswordException(oldPassword);
- user.EncryptedPassword = _passwordService.HashPassword(newPassword);
+ user.Password = _passwordService.HashPassword(newPassword);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
_logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
("Id", user.Id), ("Operation", "Change password")));
//clear cache
- RemoveCache(user.Id);
+ await _cache.RemoveCache(user.Id);
}
public async Task ChangeUsername(string oldUsername, string newUsername)
@@ -348,20 +353,22 @@ namespace Timeline.Services CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat);
CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat);
- var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync();
+ var user = await _databaseContext.Users.Where(u => u.Username == oldUsername).SingleOrDefaultAsync();
if (user == null)
throw new UserNotExistException(oldUsername);
- var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync();
+ var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync();
if (conflictUser != null)
throw new UsernameConfictException(newUsername);
- user.Name = newUsername;
+ user.Username = newUsername;
user.Version += 1;
await _databaseContext.SaveChangesAsync();
_logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername)));
- RemoveCache(user.Id);
+ await _cache.RemoveCache(user.Id);
}
+
+
}
}
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index c3cb51c9..a2c2980d 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -8,7 +8,7 @@ namespace Timeline.Services public class UserTokenCreateResult
{
public string Token { get; set; } = default!;
- public UserInfo User { get; set; } = default!;
+ public User User { get; set; } = default!;
}
public interface IUserTokenManager
@@ -36,7 +36,7 @@ namespace Timeline.Services /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
/// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
- public Task<UserInfo> VerifyToken(string token);
+ public Task<User> VerifyToken(string token);
}
public class UserTokenManager : IUserTokenManager
@@ -68,7 +68,7 @@ namespace Timeline.Services }
- public async Task<UserInfo> VerifyToken(string token)
+ public async Task<User> VerifyToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs deleted file mode 100644 index ad0350b5..00000000 --- a/Timeline/Services/UsernameBadFormatException.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Thrown when username is of bad format.
- /// </summary>
- [Serializable]
- public class UsernameBadFormatException : Exception
- {
- public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { }
- public UsernameBadFormatException(string message) : base(message) { }
- public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { }
-
- public UsernameBadFormatException(string username, string validationMessage) : this() { Username = username; ValidationMessage = validationMessage; }
-
- public UsernameBadFormatException(string username, string validationMessage, string message) : this(message) { Username = username; ValidationMessage = validationMessage; }
-
- protected UsernameBadFormatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- /// <summary>
- /// Username of bad format.
- /// </summary>
- public string Username { get; private set; } = "";
-
- public string ValidationMessage { get; private set; } = "";
- }
-}
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 90588f70..195252d9 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -82,6 +82,11 @@ <AutoGen>True</AutoGen>
<DependentUpon>Common.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Models\Validation\PasswordValidator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>PasswordValidator.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Models\Validation\UsernameValidator.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -102,11 +107,21 @@ <AutoGen>True</AutoGen>
<DependentUpon>UserAvatarService.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Services\UserCache.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserCache.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Services\UserDetailService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>UserDetailService.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Services\UserManager.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserManager.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Services\UserService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -152,6 +167,10 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Common.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\PasswordValidator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>PasswordValidator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UsernameValidator.Designer.cs</LastGenOutput>
@@ -168,10 +187,18 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserCache.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserCache.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Services\UserDetailService.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserDetailService.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserManager.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserManager.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Services\UserService.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UserService.Designer.cs</LastGenOutput>
|