Compare commits
8 Commits
a757749767
...
1b515ea904
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b515ea904 | ||
|
|
4cabaf3191 | ||
|
|
8cfa28a863 | ||
|
|
3955a3232d | ||
|
|
b93ba6be17 | ||
|
|
39ff4b8867 | ||
|
|
7b7aba6efd | ||
|
|
a42e4287ff |
@@ -0,0 +1,54 @@
|
|||||||
|
using Dapper;
|
||||||
|
using EnvelopeGenerator.Application.SQL;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static class Extension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="executor"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="title"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="tfaEnabled"></param>
|
||||||
|
/// <param name="cancellation"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<Envelope?> CreateEnvelopeAsync(this ISQLExecutor executor, int userId, string title = "", string message = "", bool tfaEnabled = false, CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("@UserId", userId);
|
||||||
|
parameters.Add("@Title", title);
|
||||||
|
parameters.Add("@TfaEnabled", tfaEnabled ? 1 : 0);
|
||||||
|
parameters.Add("@Message", message);
|
||||||
|
var envelopes = await executor.Execute<Envelope, EnvelopeCreateReadSQL>(parameters, cancellation);
|
||||||
|
return envelopes.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="executor"></param>
|
||||||
|
/// <param name="envelope_uuid"></param>
|
||||||
|
/// <param name="emailAdress"></param>
|
||||||
|
/// <param name="salutation"></param>
|
||||||
|
/// <param name="phone"></param>
|
||||||
|
/// <param name="cancellation"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<EnvelopeReceiver?> AddReceiver(this ISQLExecutor executor, string envelope_uuid, string emailAdress, string salutation, string? phone = null, CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("@ENV_UID", envelope_uuid);
|
||||||
|
parameters.Add("@EMAIL_ADRESS", emailAdress);
|
||||||
|
parameters.Add("@SALUTATION", salutation);
|
||||||
|
parameters.Add("@PHONE", phone);
|
||||||
|
|
||||||
|
var envelopeReceivers = await executor.Execute<EnvelopeReceiver, EnvelopeReceiverAddReadSQL>(parameters, cancellation);
|
||||||
|
return envelopeReceivers.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Dapper;
|
|
||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
@@ -12,18 +9,18 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, CreateEnvelopeResponse?>
|
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, CreateEnvelopeResponse?>
|
||||||
{
|
{
|
||||||
private readonly ISQLExecutor<Envelope> _sqlExecutor;
|
private readonly ISQLExecutor _executor;
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sqlExecutor"></param>
|
/// <param name="executor"></param>
|
||||||
/// <param name="mapper"></param>
|
/// <param name="mapper"></param>
|
||||||
public CreateEnvelopeCommandHandler(ISQLExecutor<Envelope> sqlExecutor, IMapper mapper)
|
public CreateEnvelopeCommandHandler(ISQLExecutor executor, IMapper mapper)
|
||||||
{
|
{
|
||||||
_sqlExecutor = sqlExecutor;
|
_executor = executor;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +32,10 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<CreateEnvelopeResponse?> Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken)
|
public async Task<CreateEnvelopeResponse?> Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var parameters = new DynamicParameters();
|
int userId = request.UserId ?? throw new InvalidOperationException("UserId cannot be null when creating an envelope.");
|
||||||
parameters.Add("@UserId", request.UserId);
|
|
||||||
parameters.Add("@Title", request.Title);
|
|
||||||
parameters.Add("@TfaEnabled", request.TFAEnabled ? 1 : 0);
|
|
||||||
parameters.Add("@Message", request.Message);
|
|
||||||
|
|
||||||
var envelopes = await _sqlExecutor.Execute<Envelope, CreateEnvelopeSQL>(parameters, cancellationToken);
|
var envelope = await _executor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancellationToken);
|
||||||
|
|
||||||
return _mapper.Map<CreateEnvelopeResponse>(envelopes.FirstOrDefault());
|
return _mapper.Map<CreateEnvelopeResponse>(envelope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.SQL;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CreateEnvelopeSQL : ISQL<Envelope>
|
public class EnvelopeCreateReadSQL : ISQL<Envelope>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.SQL;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string Raw => @"
|
||||||
|
USE [DD_ECM]
|
||||||
|
GO
|
||||||
|
|
||||||
|
DECLARE @OUT_RECEIVER_ID int
|
||||||
|
|
||||||
|
DECLARE @ENV_UID varchar(36) = @ENV_UID
|
||||||
|
|
||||||
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
|
@ENV_UID = @ENV_UID,
|
||||||
|
@EMAIL_ADRESS = @EMAIL_ADRESS ,
|
||||||
|
@SALUTATION = @SALUTATION,
|
||||||
|
@PHONE = @PHONE,
|
||||||
|
@OUT_RECEIVER_ID = @OUT_RECEIVER_ID OUTPUT
|
||||||
|
|
||||||
|
SELECT TOP(1) *
|
||||||
|
FROM TBSIG_ENVELOPE_RECEIVER
|
||||||
|
WHERE [GUID] = @OUT_RECEIVER_ID;
|
||||||
|
";
|
||||||
|
}
|
||||||
@@ -159,7 +159,7 @@ builder.Services.AddCookieBasedLocalizer() ;
|
|||||||
|
|
||||||
// Envelope generator serives
|
// Envelope generator serives
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddEnvelopeGeneratorRepositories()
|
.AddEnvelopeGeneratorInfrastructureServices(sqlExecutorConfigureOptions: executor => executor.ConnectionString = connStr)
|
||||||
.AddEnvelopeGeneratorServices(config);
|
.AddEnvelopeGeneratorServices(config);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ using DigitalData.Core.Infrastructure;
|
|||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using DigitalData.Core.Infrastructure.AutoMapper;
|
using DigitalData.Core.Infrastructure.AutoMapper;
|
||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
using Dapper;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Reflection;
|
||||||
|
using DigitalData.UserManager.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure;
|
namespace EnvelopeGenerator.Infrastructure;
|
||||||
|
|
||||||
@@ -26,7 +32,10 @@ public static class DIExtensions
|
|||||||
/// This method ensures that the repositories are registered as scoped services, meaning that a new instance of each repository
|
/// This method ensures that the repositories are registered as scoped services, meaning that a new instance of each repository
|
||||||
/// will be created per HTTP request (or per scope) within the dependency injection container.
|
/// will be created per HTTP request (or per scope) within the dependency injection container.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static IServiceCollection AddEnvelopeGeneratorRepositories(this IServiceCollection services, Action<DbContextOptionsBuilder>? dbContextOptions = null)
|
public static IServiceCollection AddEnvelopeGeneratorInfrastructureServices(this IServiceCollection services,
|
||||||
|
Action<DbContextOptionsBuilder>? dbContextOptions = null,
|
||||||
|
IConfiguration? sqlExecutorConfiguration = null,
|
||||||
|
Action<SQLExecutorParams>? sqlExecutorConfigureOptions = null)
|
||||||
{
|
{
|
||||||
if(dbContextOptions is not null)
|
if(dbContextOptions is not null)
|
||||||
services.AddDbContext<EGDbContext>(dbContextOptions);
|
services.AddDbContext<EGDbContext>(dbContextOptions);
|
||||||
@@ -66,11 +75,57 @@ public static class DIExtensions
|
|||||||
services.AddSQLExecutor<DocumentReceiverElement>();
|
services.AddSQLExecutor<DocumentReceiverElement>();
|
||||||
services.AddSQLExecutor<DocumentStatus>();
|
services.AddSQLExecutor<DocumentStatus>();
|
||||||
|
|
||||||
|
SetDapperTypeMap<Envelope>();
|
||||||
|
SetDapperTypeMap<User>();
|
||||||
|
SetDapperTypeMap<Receiver>();
|
||||||
|
SetDapperTypeMap<EnvelopeDocument>();
|
||||||
|
SetDapperTypeMap<DocumentReceiverElement>();
|
||||||
|
SetDapperTypeMap<DocumentStatus>();
|
||||||
|
|
||||||
|
if (sqlExecutorConfiguration is not null || sqlExecutorConfigureOptions is not null)
|
||||||
|
services.AddSQLExecutor(sqlExecutorConfiguration, sqlExecutorConfigureOptions);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddSQLExecutor<T>(this IServiceCollection services) where T : class
|
public static IServiceCollection AddSQLExecutor(this IServiceCollection services, IConfiguration? configuration = null, Action<SQLExecutorParams>? configureOptions = null)
|
||||||
{
|
{
|
||||||
|
if(configuration is not null && configureOptions is not null)
|
||||||
|
throw new InvalidOperationException("Cannot use both 'configuration' and 'configureOptions'. Only one should be provided.");
|
||||||
|
|
||||||
|
if (configuration is not null)
|
||||||
|
services.Configure<SQLExecutorParams>(configuration);
|
||||||
|
|
||||||
|
if(configureOptions is not null)
|
||||||
|
services.Configure(configureOptions);
|
||||||
|
|
||||||
|
return services.AddSingleton<ISQLExecutor, SQLExecutor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetDapperTypeMap<TModel>()
|
||||||
|
{
|
||||||
|
Dapper.SqlMapper.SetTypeMap(typeof(TModel), new CustomPropertyTypeMap(
|
||||||
|
typeof(TModel),
|
||||||
|
(type, columnName) =>
|
||||||
|
{
|
||||||
|
return type.GetProperties().FirstOrDefault(prop =>
|
||||||
|
{
|
||||||
|
var attr = prop.GetCustomAttribute<ColumnAttribute>();
|
||||||
|
return attr != null && string.Equals(attr.Name, columnName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(prop.Name, columnName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
})!;
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddSQLExecutor<T>(this IServiceCollection services, IConfiguration? configuration = null, Action<SQLExecutorParams>? configureOptions = null) where T : class
|
||||||
|
{
|
||||||
|
if (configuration is not null && configureOptions is not null)
|
||||||
|
throw new InvalidOperationException("Cannot use both 'configuration' and 'configureOptions'. Only one should be provided.");
|
||||||
|
|
||||||
|
if (configuration is not null)
|
||||||
|
services.Configure<SQLExecutorParams>(configuration);
|
||||||
|
|
||||||
services.AddScoped<ISQLExecutor<T>, SQLExecutor<T>>();
|
services.AddScoped<ISQLExecutor<T>, SQLExecutor<T>>();
|
||||||
|
|
||||||
var interfaceType = typeof(ISQL<>);
|
var interfaceType = typeof(ISQL<>);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using AngleSharp.Dom;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure;
|
namespace EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
|
||||||
public sealed record Query<TEntity> : IQuery<TEntity>
|
public sealed record Query<TEntity> : IQuery<TEntity>
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.Infrastructure;
|
namespace EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
|
||||||
public static class QueryExtension
|
public static class QueryExtension
|
||||||
{
|
{
|
||||||
@@ -2,30 +2,32 @@
|
|||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure;
|
namespace EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
|
||||||
public class SQLExecutor : ISQLExecutor
|
public class SQLExecutor : ISQLExecutor
|
||||||
{
|
{
|
||||||
private readonly string _cnnStr = "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;";
|
protected readonly SQLExecutorParams Params;
|
||||||
|
|
||||||
private readonly IServiceProvider _provider;
|
protected readonly IServiceProvider Provider;
|
||||||
|
|
||||||
public SQLExecutor(IServiceProvider provider)
|
public SQLExecutor(IServiceProvider provider, IOptions<SQLExecutorParams> sqlExecutorParamsOptions)
|
||||||
{
|
{
|
||||||
_provider = provider;
|
Provider = provider;
|
||||||
|
Params = sqlExecutorParamsOptions.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default)
|
public async Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(_cnnStr);
|
using var connection = new SqlConnection(Params.ConnectionString);
|
||||||
await connection.OpenAsync(cancellation);
|
await connection.OpenAsync(cancellation);
|
||||||
return await connection.QueryAsync<TEntity>(sql, parameters);
|
return await connection.QueryAsync<TEntity>(sql, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL
|
public Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL
|
||||||
{
|
{
|
||||||
var sql = _provider.GetRequiredService<TSQL>();
|
var sql = Provider.GetRequiredService<TSQL>();
|
||||||
return Execute<TEntity>(sql.Raw, parameters, cancellation);
|
return Execute<TEntity>(sql.Raw, parameters, cancellation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure;
|
namespace EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
|
||||||
public sealed class SQLExecutor<T> : SQLExecutor, ISQLExecutor<T> where T : class
|
public sealed class SQLExecutor<T> : SQLExecutor, ISQLExecutor<T> where T : class
|
||||||
{
|
{
|
||||||
@@ -10,7 +11,7 @@ public sealed class SQLExecutor<T> : SQLExecutor, ISQLExecutor<T> where T : clas
|
|||||||
|
|
||||||
private readonly IServiceProvider _provider;
|
private readonly IServiceProvider _provider;
|
||||||
|
|
||||||
public SQLExecutor(EGDbContext context, IServiceProvider provider) : base(provider)
|
public SQLExecutor(EGDbContext context, IServiceProvider provider, IOptions<SQLExecutorParams> options) : base(provider, options)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.Infrastructure.Executor;
|
||||||
|
|
||||||
|
public class SQLExecutorParams
|
||||||
|
{
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
}
|
||||||
@@ -27,11 +27,11 @@ public static class DependencyInjection
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add envelope generator services
|
// Add envelope generator services
|
||||||
services.AddEnvelopeGeneratorRepositories(options => options.UseSqlServer(connStr));
|
services.AddEnvelopeGeneratorInfrastructureServices(options => options.UseSqlServer(connStr));
|
||||||
|
|
||||||
return services
|
return services
|
||||||
.AddSingleton<CommandManager>()
|
.AddSingleton<CommandManager>()
|
||||||
.AddEnvelopeGeneratorRepositories()
|
.AddEnvelopeGeneratorInfrastructureServices()
|
||||||
.AddEnvelopeGeneratorServices(configuration)
|
.AddEnvelopeGeneratorServices(configuration)
|
||||||
.AddSingleton(sp =>
|
.AddSingleton(sp =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class Mock
|
|||||||
builder.Configuration.AddJsonFile(configPath, optional: true, reloadOnChange: true);
|
builder.Configuration.AddJsonFile(configPath, optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddEnvelopeGeneratorRepositories(opt =>
|
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||||
{
|
{
|
||||||
if (useRealDb)
|
if (useRealDb)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ try
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add envelope generator services
|
// Add envelope generator services
|
||||||
builder.Services.AddEnvelopeGeneratorRepositories(options => options.UseSqlServer(connStr));
|
builder.Services.AddEnvelopeGeneratorInfrastructureServices(options => options.UseSqlServer(connStr));
|
||||||
|
|
||||||
builder.Services.AddEnvelopeGeneratorServices(config);
|
builder.Services.AddEnvelopeGeneratorServices(config);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user