Merge remote-tracking branch 'origin/master' into feature/oauth
# Conflicts: # src/Application/Common/Interfaces/IIdentityService.cs # src/Application/Users/Commands/CreateUser.cs # src/Infrastructure/Identity/IdentityService.cs # src/Infrastructure/Services/UserService.cs # src/Web/Program.cs
This commit is contained in:
@@ -6,10 +6,15 @@ namespace Hutopy.Application.Common.Interfaces;
|
||||
public interface IIdentityService
|
||||
{
|
||||
Task<string?> GetUserNameAsync(string userId);
|
||||
|
||||
Task<Result> CreateUserAsync(string email, string userName, string firstName, string lastName, string password);
|
||||
Task<UserModel?> FindUserByIdAsync(string id);
|
||||
Task<UserModel?> GetCurrentUserAsync();
|
||||
Task<UserModel?> FindUserByEmailAsync(string id);
|
||||
Task<UserModel?> GetUserByUserNameAsync(string userName);
|
||||
Task<bool> IsInRoleAsync(string userId, string role);
|
||||
|
||||
Task<bool> AuthorizeAsync(string userId, string policyName);
|
||||
Task<Result> AddRoleAsync(string userId, string role);
|
||||
Task<IList<string>> GetCurrentUserRolesAsync();
|
||||
|
||||
Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);
|
||||
|
||||
|
||||
10
src/Application/Common/Interfaces/IRoleService.cs
Normal file
10
src/Application/Common/Interfaces/IRoleService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IRoleService
|
||||
{
|
||||
public Task<Result> CreateRoleAsync(string roleName);
|
||||
public Task<Result> DeleteRoleAsync(string roleName);
|
||||
public Task<RoleModel?> FindRoleByIdAsync(string roleId);
|
||||
}
|
||||
7
src/Application/Common/Models/RoleModel.cs
Normal file
7
src/Application/Common/Models/RoleModel.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class RoleModel
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Domain.Models;
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class UserModel
|
||||
{
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Behaviours;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Hutopy.Application;
|
||||
@@ -15,7 +16,7 @@ public static class DependencyInjection
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
|
||||
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Mappings;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.Common.Security;
|
||||
|
||||
namespace Hutopy.Application.FutureCreators.Queries;
|
||||
|
||||
[Authorize(Roles = "Administrator")]
|
||||
public record GetFutureCreatorListQuery : IRequest<PaginatedList<FutureCreatorListDto>>
|
||||
{
|
||||
public int PageNumber { get; init; } = 1;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Dynamic;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.Users.Commands;
|
||||
public record CreateUserCommand : IRequest<Guid>
|
||||
@@ -15,24 +13,22 @@ public record CreateUserCommand : IRequest<Guid>
|
||||
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IIdentityService _identityService;
|
||||
|
||||
public CreateUserCommandHandler(IApplicationDbContext context, IUserService userService)
|
||||
public CreateUserCommandHandler(IApplicationDbContext context, IIdentityService identityService)
|
||||
{
|
||||
_context = context;
|
||||
_userService = userService;
|
||||
_identityService = identityService;
|
||||
}
|
||||
|
||||
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Dont really need the handler for the create. The get will work like this :
|
||||
var user = await _userService.FindUserByIdAsync("072ae7d5-8c4a-4a0f-b250-7d39941125cb");
|
||||
// var user2 = await _userService.FindUserByEmailAsync("test10@hotmail.com");
|
||||
await _identityService.CreateUserAsync(request.EmailAddress, request.UserName, request.FirstName, request.LastName, request.Password);
|
||||
|
||||
var tt = user?.FirstName;
|
||||
var user = await _identityService.FindUserByEmailAsync(request.EmailAddress);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Guid.NewGuid();
|
||||
return new Guid(user?.Id ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.Users.Queries;
|
||||
namespace Hutopy.Application.Users.Queries.GetCurrentUser;
|
||||
|
||||
public record GetCurrentUserQuery : IRequest<UserDto>;
|
||||
|
||||
public class GetCurrentUserQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
IMapper mapper,
|
||||
IUserService userService
|
||||
IIdentityService identityService
|
||||
)
|
||||
: IRequestHandler<GetCurrentUserQuery, UserDto>
|
||||
{
|
||||
public async Task<UserDto> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await userService.GetCurrentUserAsync();
|
||||
var identityUser = await identityService.GetCurrentUserAsync();
|
||||
var currentUserId = new Guid(identityUser?.Id ?? "");
|
||||
|
||||
var transactions = await context.UserTransactions
|
||||
.Where(x => x.ApplicationUserId == currentUserId.ToString())
|
||||
.OrderBy(x => x.LastModified)
|
||||
.ProjectTo<UserTransactionDto>(mapper.ConfigurationProvider)
|
||||
.Where(x => x.IsConfirmed == true)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var user = new UserDto()
|
||||
var roles = await identityService.GetCurrentUserRolesAsync();
|
||||
|
||||
var user = new UserDto
|
||||
{
|
||||
Id = currentUserId,
|
||||
FirstName = identityUser?.FirstName ?? "",
|
||||
LastName = identityUser?.LastName ?? "",
|
||||
UserTransactions = transactions
|
||||
UserName =identityUser?.UserName ?? "",
|
||||
UserTransactions = transactions,
|
||||
TotalBalance = transactions.Sum(x => x.Amount),
|
||||
UserRoles = roles
|
||||
};
|
||||
|
||||
return user;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
namespace Hutopy.Application.Users.Queries;
|
||||
namespace Hutopy.Application.Users.Queries.GetCurrentUser;
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
|
||||
public required string FirstName { get; init; }
|
||||
|
||||
public required string LastName { get; init; }
|
||||
public string UserName { get; init; } = String.Empty;
|
||||
|
||||
public List<UserTransactionDto> UserTransactions { get; init; } = [];
|
||||
public IList<string> UserRoles { get; init; } = [];
|
||||
|
||||
public required decimal TotalBalance { get; init; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.Users.Queries;
|
||||
namespace Hutopy.Application.Users.Queries.GetCurrentUser;
|
||||
|
||||
public class UserTransactionDto
|
||||
{
|
||||
public required decimal Amount { get; init; }
|
||||
|
||||
|
||||
public string Currency { get; init; } = "cad";
|
||||
|
||||
public string TipMessage { get; init; } = string.Empty;
|
||||
|
||||
public DateTimeOffset Created { get; init; }
|
||||
|
||||
public bool IsConfirmed { get; init; }
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.Users.Queries.GetMinimalUser;
|
||||
|
||||
@@ -8,15 +8,15 @@ public record GetMinimalUserQuery : IRequest<MinimalUserDto>
|
||||
};
|
||||
|
||||
public class GetMinimalUserQueryHandler(
|
||||
IUserService userService
|
||||
IIdentityService identityService
|
||||
)
|
||||
: IRequestHandler<GetMinimalUserQuery, MinimalUserDto>
|
||||
{
|
||||
public async Task<MinimalUserDto> Handle(GetMinimalUserQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await userService.FindUserByIdAsync(request.UserId);
|
||||
var identityUser = await identityService.FindUserByIdAsync(request.UserId);
|
||||
|
||||
var user = new MinimalUserDto()
|
||||
var user = new MinimalUserDto
|
||||
{
|
||||
FirstName = identityUser?.FirstName ?? "",
|
||||
LastName = identityUser?.LastName ?? "",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using Hutopy.Domain.Models;
|
||||
|
||||
namespace Hutopy.Domain.Interfaces;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password);
|
||||
|
||||
Task<UserModel?> FindUserByIdAsync(string id);
|
||||
Task<UserModel?> GetCurrentUserAsync();
|
||||
|
||||
Task<UserModel?> FindUserByEmailAsync(string id);
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Data.Interceptors;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Hutopy.Infrastructure.Services;
|
||||
using Hutopy.Infrastructure.Stripe;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -51,8 +48,6 @@ public static class DependencyInjection
|
||||
.AddBearerToken(IdentityConstants.BearerScheme);
|
||||
|
||||
services.AddAuthorizationBuilder();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
|
||||
// Might need to change and use AddIdentity<User, Role>() when we need to integrate connection via third party ( facebook, google )
|
||||
services
|
||||
@@ -62,7 +57,7 @@ public static class DependencyInjection
|
||||
.AddApiEndpoints();
|
||||
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddTransient<IIdentityService, IdentityService>();
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
services.AddTransient<IStripeService, StripeService>();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
|
||||
7
src/Infrastructure/Identity/ApplicationRole.cs
Normal file
7
src/Infrastructure/Identity/ApplicationRole.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class ApplicationRole : IdentityRole
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Google.Apis.Oauth2.v2.Data;
|
||||
using System.Security.Claims;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
@@ -9,7 +11,9 @@ namespace Hutopy.Infrastructure.Identity;
|
||||
public class IdentityService(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IHttpContextAccessor contextAccessor
|
||||
)
|
||||
: IIdentityService
|
||||
{
|
||||
public async Task<string?> GetUserNameAsync(string userId)
|
||||
@@ -18,6 +22,24 @@ public class IdentityService(
|
||||
|
||||
return user?.UserName;
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetUserByUserNameAsync(string userName)
|
||||
{
|
||||
var response = await userManager.FindByNameAsync(userName);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
var userModel = new UserModel()
|
||||
{
|
||||
Id = response.Id,
|
||||
UserName = response.UserName,
|
||||
FirstName = response.FirstName,
|
||||
LastName = response.LastName,
|
||||
Email = response.Email,
|
||||
};
|
||||
|
||||
return userModel;
|
||||
}
|
||||
|
||||
public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password)
|
||||
{
|
||||
@@ -48,6 +70,68 @@ public class IdentityService(
|
||||
|
||||
return (result.ToApplicationResult(), user.Id);
|
||||
}
|
||||
|
||||
public async Task<Result> CreateUserAsync(string email, string userName, string firstName, string lastName, string password)
|
||||
{
|
||||
var applicationUser = new ApplicationUser
|
||||
{
|
||||
UserName = userName,
|
||||
Email = email,
|
||||
FirstName = firstName,
|
||||
LastName = lastName
|
||||
};
|
||||
|
||||
var response = await userManager.CreateAsync(applicationUser, password);
|
||||
|
||||
return response.ToApplicationResult();
|
||||
}
|
||||
|
||||
public async Task<UserModel?> FindUserByIdAsync(string id)
|
||||
{
|
||||
var response = await userManager.FindByIdAsync(id);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
var userModel = new UserModel()
|
||||
{
|
||||
Id = response.Id,
|
||||
UserName = response.UserName,
|
||||
FirstName = response.FirstName,
|
||||
LastName = response.LastName,
|
||||
Email = response.Email,
|
||||
};
|
||||
|
||||
return userModel;
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetCurrentUserAsync()
|
||||
{
|
||||
var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(currentUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await FindUserByIdAsync(currentUserId);
|
||||
}
|
||||
|
||||
public async Task<UserModel?> FindUserByEmailAsync(string email)
|
||||
{
|
||||
var response = await userManager.FindByEmailAsync(email);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
var userModel = new UserModel
|
||||
{
|
||||
Id = response.Id,
|
||||
UserName = response.UserName,
|
||||
FirstName = response.FirstName,
|
||||
LastName = response.LastName,
|
||||
Email = response.Email
|
||||
};
|
||||
|
||||
return userModel;
|
||||
}
|
||||
|
||||
public async Task<bool> IsInRoleAsync(string userId, string role)
|
||||
{
|
||||
@@ -55,6 +139,14 @@ public class IdentityService(
|
||||
|
||||
return user != null && await userManager.IsInRoleAsync(user, role);
|
||||
}
|
||||
|
||||
public async Task<bool> CurrentUserIsInRoleAsync(string role)
|
||||
{
|
||||
var currentUserModel = await GetCurrentUserAsync();
|
||||
var currentUser = await userManager.FindByIdAsync(currentUserModel?.Id ?? "");
|
||||
|
||||
return currentUser != null && await userManager.IsInRoleAsync(currentUser, role);
|
||||
}
|
||||
|
||||
public async Task<bool> AuthorizeAsync(string userId, string policyName)
|
||||
{
|
||||
@@ -85,4 +177,32 @@ public class IdentityService(
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
|
||||
public async Task<Result> AddRoleAsync(string userId, string role)
|
||||
{
|
||||
var hasAdminAccess = await CurrentUserIsInRoleAsync("Administrator");
|
||||
|
||||
if (!hasAdminAccess) return Result.Failure(new []{"Only administrator can assign new roles to a user."});
|
||||
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
|
||||
if (user is null) return Result.Failure(new []{"User not found."});
|
||||
|
||||
var result = await userManager.AddToRoleAsync(user, role);
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetCurrentUserRolesAsync()
|
||||
{
|
||||
var currentUserModel = await GetCurrentUserAsync();
|
||||
|
||||
var currentUser = await userManager.FindByIdAsync(currentUserModel?.Id ?? "");
|
||||
|
||||
if (currentUser is null) return [];
|
||||
|
||||
var userRoles = await userManager.GetRolesAsync(currentUser);
|
||||
|
||||
return userRoles;
|
||||
}
|
||||
}
|
||||
|
||||
49
src/Infrastructure/Identity/RoleService.cs
Normal file
49
src/Infrastructure/Identity/RoleService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class RoleService(
|
||||
RoleManager<ApplicationRole> roleManager
|
||||
)
|
||||
: IRoleService
|
||||
{
|
||||
public async Task<Result> CreateRoleAsync(string roleName)
|
||||
{
|
||||
var role = new ApplicationRole { Name = roleName, Id = Guid.NewGuid().ToString()};
|
||||
var result = await roleManager.CreateAsync(role);
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteRoleAsync(string roleName)
|
||||
{
|
||||
var role = new ApplicationRole { Name = roleName };
|
||||
var result = await roleManager.DeleteAsync(role);
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
|
||||
public async Task<RoleModel?> FindRoleByIdAsync(string roleId)
|
||||
{
|
||||
var result = await roleManager.FindByIdAsync(roleId);
|
||||
|
||||
if (result is null) return null;
|
||||
|
||||
var roleModel = new RoleModel { Id = result.Id, Name = result.Name };
|
||||
|
||||
return roleModel;
|
||||
}
|
||||
|
||||
public async Task<RoleModel?> FindRoleByNameAsync(string roleName)
|
||||
{
|
||||
var result = await roleManager.FindByNameAsync(roleName);
|
||||
|
||||
if (result is null) return null;
|
||||
|
||||
var roleModel = new RoleModel { Id = result.Id, Name = result.Name };
|
||||
|
||||
return roleModel;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using Azure.Identity;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Services;
|
||||
using Hutopy.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSwag;
|
||||
@@ -17,8 +15,6 @@ public static class DependencyInjection
|
||||
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
services.AddScoped<IUser, CurrentUser>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Hutopy.Application.Users.Queries;
|
||||
using Hutopy.Application.Users.Queries.GetCurrentUser;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Hutopy.Application.Users.Commands;
|
||||
using Hutopy.Application.Users.Queries.GetMinimalUser;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
@@ -15,9 +14,8 @@ public class Users : EndpointGroupBase
|
||||
.MapIdentityApi<ApplicationUser>();
|
||||
}
|
||||
|
||||
private static async Task<Guid> CreateUser(ISender sender, CreateUserCommand command, IUserService userService)
|
||||
private static async Task<Guid> CreateUser(ISender sender, CreateUserCommand command)
|
||||
{
|
||||
await userService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password);
|
||||
return await sender.Send(command);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Hutopy.Application;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Hutopy.Infrastructure;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Services;
|
||||
using Hutopy.Web;
|
||||
using Azure.Identity;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
|
||||
@@ -736,6 +736,16 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserTransactionDto"
|
||||
}
|
||||
},
|
||||
"userRoles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"totalBalance": {
|
||||
"type": "number",
|
||||
"format": "decimal"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -752,6 +762,13 @@
|
||||
},
|
||||
"tipMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"isConfirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user