Merge branch 'develop' of github.com:ONLYOFFICE/DocSpace into feature/branding

This commit is contained in:
Viktor Fomin 2022-08-23 13:12:58 +03:00
commit 720b98c894
126 changed files with 2981 additions and 1884 deletions

14
build/Jenkinsfile vendored
View File

@ -43,20 +43,20 @@ pipeline {
stages {
stage('Components') {
steps {
sh "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}/packages/asc-web-components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
sh "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}/packages/components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
}
post {
success {
junit 'packages/asc-web-components/junit.xml'
junit 'packages/components/junit.xml'
publishHTML target: [
allowMissing : false,
alwaysLinkToLastBuild: false,
keepAll : true,
reportDir : 'packages/asc-web-components/coverage/lcov-report',
reportDir : 'packages/components/coverage/lcov-report',
reportFiles : 'index.html',
reportName : 'Unix Test Report'
]
publishCoverage adapters: [coberturaAdapter('packages/asc-web-components/coverage/cobertura-coverage.xml')]
publishCoverage adapters: [coberturaAdapter('packages/components/coverage/cobertura-coverage.xml')]
}
}
}
@ -72,16 +72,16 @@ pipeline {
stages {
stage('Components') {
steps {
bat "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}\\packages\\asc-web-components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
bat "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}\\packages\\components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
}
post {
success {
junit 'packages\\asc-web-components\\junit.xml'
junit 'packages\\components\\junit.xml'
publishHTML target: [
allowMissing : false,
alwaysLinkToLastBuild: false,
keepAll : true,
reportDir : 'packages\\asc-web-components\\coverage\\lcov-report',
reportDir : 'packages\\components\\coverage\\lcov-report',
reportFiles : 'index.html',
reportName : 'Windows Test Report'
]

View File

@ -10,26 +10,31 @@ if %errorlevel% == 0 (
call start\stop.bat nopause
dotnet build ..\asc.web.slnf /fl1 /flp1:logfile=asc.web.log;verbosity=normal
echo.
)
if %errorlevel% == 0 (
echo install nodejs projects dependencies...
echo.
for /R "scripts\" %%f in (*.bat) do (
echo Run script %%~nxf...
echo.
call scripts\%%~nxf
)
)
echo.
if %errorlevel% == 0 (
call start\start.bat nopause
)
echo.
if "%1"=="nopause" goto end
pause
)
:end

View File

@ -1,5 +1,6 @@
@echo "MIGRATIONS"
@echo off
PUSHD %~dp0..\common\Tools\Migration.Creator
dotnet run --project Migration.Creator.csproj
PUSHD %~dp0..\common\Tools\ASC.Migration.Creator
dotnet run --project ASC.Migration.Creator.csproj
pause

View File

@ -1,9 +1,11 @@
@echo off
PUSHD %~dp0..\..
setlocal EnableDelayedExpansion
PUSHD %~dp0..
call runasadmin.bat "%~dpnx0"
if %errorlevel% == 0 (
PUSHD %~dp0..\..
setlocal EnableDelayedExpansion
for /R "build\run\" %%f in (*.bat) do (
call build\run\%%~nxf
echo service create "Onlyoffice%%~nf"

View File

@ -18,6 +18,7 @@
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="6.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.1.0" />

View File

@ -0,0 +1,103 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text;
using System.Text.RegularExpressions;
using SecurityContext = ASC.Core.SecurityContext;
namespace ASC.Api.Core.Auth;
[Scope]
public class BasicAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly UserManager _userManager;
private readonly SecurityContext _securityContext;
private readonly PasswordHasher _passwordHasher;
public BasicAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock
) : base(options, logger, encoder, clock)
{
}
public BasicAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
UserManager userManager,
SecurityContext securityContext,
PasswordHasher passwordHasher) : this(options, logger, encoder, clock)
{
_userManager = userManager;
_securityContext = securityContext;
_passwordHasher = passwordHasher;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
Response.Headers.Add("WWW-Authenticate", "Basic");
if (!Request.Headers.ContainsKey("Authorization"))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
}
// Get authorization key
var authorizationHeader = Request.Headers["Authorization"].ToString();
var authHeaderRegex = new Regex(@"Basic (.*)");
if (!authHeaderRegex.IsMatch(authorizationHeader))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
}
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
var authSplit = authBase64.Split(Convert.ToChar(":"), 2);
var authUsername = authSplit[0];
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
try
{
var userInfo = _userManager.GetUserByEmail(authUsername);
var passwordHash = _passwordHasher.GetClientPassword(authPassword);
_securityContext.AuthenticateMe(userInfo.Email, passwordHash);
}
catch (Exception)
{
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
}
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, Scheme.Name)));
}
}

View File

@ -31,9 +31,9 @@ namespace ASC.Api.Core.Auth;
[Scope]
public class CookieAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly AuthorizationHelper _authorizationHelper;
private readonly SecurityContext _securityContext;
private readonly CookiesManager _cookiesManager;
private readonly IHttpContextAccessor _httpContextAccessor;
public CookieAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
@ -42,31 +42,59 @@ public class CookieAuthHandler : AuthenticationHandler<AuthenticationSchemeOptio
ISystemClock clock)
: base(options, logger, encoder, clock) { }
public CookieAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock,
AuthorizationHelper authorizationHelper,
public CookieAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
SecurityContext securityContext,
CookiesManager cookiesManager)
CookiesManager cookiesManager,
IHttpContextAccessor httpContextAccessor)
: this(options, logger, encoder, clock)
{
_authorizationHelper = authorizationHelper;
_securityContext = securityContext;
_cookiesManager = cookiesManager;
_httpContextAccessor = httpContextAccessor;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var result = _authorizationHelper.ProcessBasicAuthorization(out _);
if (!result)
try
{
_securityContext.Logout();
_cookiesManager.ClearCookies(CookiesType.AuthKey);
_cookiesManager.ClearCookies(CookiesType.SocketIO);
var authorization = _httpContextAccessor.HttpContext.Request.Cookies["asc_auth_key"] ?? _httpContextAccessor.HttpContext.Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(authorization))
{
throw new AuthenticationException(nameof(HttpStatusCode.Unauthorized));
}
authorization = authorization.Trim();
if (0 <= authorization.IndexOf("Bearer", 0))
{
authorization = authorization.Substring("Bearer ".Length);
}
if (!_securityContext.AuthenticateMe(authorization))
{
throw new AuthenticationException(nameof(HttpStatusCode.Unauthorized));
}
}
catch (Exception)
{
return Task.FromResult(AuthenticateResult.Fail(new AuthenticationException(nameof(HttpStatusCode.Unauthorized))));
}
finally
{
if (!_securityContext.IsAuthenticated)
{
_securityContext.Logout();
_cookiesManager.ClearCookies(CookiesType.AuthKey);
_cookiesManager.ClearCookies(CookiesType.SocketIO);
}
}
return Task.FromResult(
result ?
AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)) :
AuthenticateResult.Fail(new AuthenticationException(nameof(HttpStatusCode.Unauthorized))));
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, Scheme.Name)));
}
}

View File

@ -24,6 +24,11 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Net.Http.Headers;
using JsonConverter = System.Text.Json.Serialization.JsonConverter;
namespace ASC.Api.Core;
@ -102,13 +107,13 @@ public abstract class BaseStartup
services.AddSingleton(jsonOptions);
DIHelper.AddControllers();
DIHelper.TryAdd<DisposeMiddleware>();
DIHelper.TryAdd<CultureMiddleware>();
DIHelper.TryAdd<IpSecurityFilter>();
DIHelper.TryAdd<PaymentFilter>();
DIHelper.TryAdd<ProductSecurityFilter>();
DIHelper.TryAdd<TenantStatusFilter>();
DIHelper.TryAdd<ConfirmAuthHandler>();
DIHelper.TryAdd<BasicAuthHandler>();
DIHelper.TryAdd<CookieAuthHandler>();
DIHelper.TryAdd<WebhooksGlobalFilterAttribute>();
@ -147,16 +152,72 @@ public abstract class BaseStartup
config.OutputFormatters.Add(new XmlOutputFormatter());
});
var authBuilder = services.AddAuthentication("cookie")
.AddScheme<AuthenticationSchemeOptions, CookieAuthHandler>("cookie", a => { });
if (ConfirmAddScheme)
var authBuilder = services.AddAuthentication(options =>
{
authBuilder.AddScheme<AuthenticationSchemeOptions, ConfirmAuthHandler>("confirm", a => { });
}
options.DefaultScheme = "MultiAuthSchemes";
options.DefaultChallengeScheme = "MultiAuthSchemes";
}).AddScheme<AuthenticationSchemeOptions, CookieAuthHandler>(CookieAuthenticationDefaults.AuthenticationScheme, a => { })
.AddScheme<AuthenticationSchemeOptions, BasicAuthHandler>("Basic", a => { })
.AddScheme<AuthenticationSchemeOptions, ConfirmAuthHandler>("confirm", a => { })
.AddJwtBearer("Bearer", options =>
{
options.Authority = _configuration["core:oidc:authority"];
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
using var scope = ctx.HttpContext.RequestServices.CreateScope();
var securityContext = scope.ServiceProvider.GetService<ASC.Core.SecurityContext>();
var claimUserId = ctx.Principal.FindFirstValue("userId");
if (String.IsNullOrEmpty(claimUserId))
{
throw new Exception("Claim 'UserId' is not present in claim list");
}
var userId = new Guid(claimUserId);
securityContext.AuthenticateMeWithoutCookie(userId, ctx.Principal.Claims.ToList());
return Task.CompletedTask;
}
};
})
.AddPolicyScheme("MultiAuthSchemes", JwtBearerDefaults.AuthenticationScheme, options =>
{
options.ForwardDefaultSelector = context =>
{
var authorizationHeader = context.Request.Headers[HeaderNames.Authorization].FirstOrDefault();
if (String.IsNullOrEmpty(authorizationHeader)) return CookieAuthenticationDefaults.AuthenticationScheme;
if (authorizationHeader.StartsWith("Basic ")) return "Basic";
if (authorizationHeader.StartsWith("Bearer "))
{
var token = authorizationHeader.Substring("Bearer ".Length).Trim();
var jwtHandler = new JwtSecurityTokenHandler();
return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals(_configuration["core:oidc:authority"]))
? JwtBearerDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
}
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
services.AddAutoMapper(GetAutoMapperProfileAssemblies());
if (!_hostEnvironment.IsDevelopment())
{
services.AddStartupTask<WarmupServicesStartupTask>()
@ -189,8 +250,6 @@ public abstract class BaseStartup
app.UseCultureMiddleware();
app.UseDisposeMiddleware();
app.UseEndpoints(endpoints =>
{
endpoints.MapCustom();

View File

@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using System.Collections.Concurrent;
global using System.ComponentModel;
global using System.Globalization;
global using System.Linq.Expressions;
@ -36,6 +34,7 @@ global using System.Runtime.Serialization;
global using System.Security;
global using System.Security.Authentication;
global using System.Security.Claims;
global using System.Text;
global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Serialization;
@ -56,8 +55,8 @@ global using ASC.AuditTrail.Types;
global using ASC.Common;
global using ASC.Common.Caching;
global using ASC.Common.DependencyInjection;
global using ASC.Common.Log;
global using ASC.Common.Logging;
global using ASC.Common.Mapping;
global using ASC.Common.Notify.Engine;
global using ASC.Common.Threading;
global using ASC.Common.Utils;
@ -119,11 +118,14 @@ global using Microsoft.AspNetCore.WebUtilities;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Microsoft.Extensions.Primitives;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.IdentityModel.Tokens;
global using Newtonsoft.Json;
global using Newtonsoft.Json.Serialization;

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using CallContext = ASC.Common.Notify.Engine.CallContext;
namespace ASC.Api.Core.Middleware;
[Scope]

View File

@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text;
namespace ASC.Api.Core.Middleware;
[Scope]
@ -34,12 +32,14 @@ public class WebhooksGlobalFilterAttribute : ResultFilterAttribute, IDisposable
private readonly MemoryStream _stream;
private Stream _bodyStream;
private readonly IWebhookPublisher _webhookPublisher;
private readonly ILogger<WebhooksGlobalFilterAttribute> _logger;
private static readonly List<string> _methodList = new List<string> { "POST", "UPDATE", "DELETE" };
public WebhooksGlobalFilterAttribute(IWebhookPublisher webhookPublisher)
public WebhooksGlobalFilterAttribute(IWebhookPublisher webhookPublisher, ILogger<WebhooksGlobalFilterAttribute> logger)
{
_stream = new MemoryStream();
_webhookPublisher = webhookPublisher;
_logger = logger;
}
public override void OnResultExecuting(ResultExecutingContext context)
@ -68,11 +68,18 @@ public class WebhooksGlobalFilterAttribute : ResultFilterAttribute, IDisposable
await _stream.CopyToAsync(_bodyStream);
context.HttpContext.Response.Body = _bodyStream;
var (method, routePattern) = GetData(context.HttpContext);
try
{
var (method, routePattern) = GetData(context.HttpContext);
var resultContent = Encoding.UTF8.GetString(_stream.ToArray());
var eventName = $"method: {method}, route: {routePattern}";
_webhookPublisher.Publish(eventName, resultContent);
var resultContent = Encoding.UTF8.GetString(_stream.ToArray());
await _webhookPublisher.PublishAsync(method, routePattern, resultContent);
}
catch (Exception e)
{
_logger.ErrorWithException(e);
}
}
}

View File

@ -77,7 +77,6 @@ public class DistributedTask
Publication(this);
}
[Obsolete("GetProperty<T> is deprecated, please use indexer this[propName] instead.")]
public T GetProperty<T>(string propName)
{
if (!_props.TryGetValue(propName, out var propValue))
@ -88,8 +87,7 @@ public class DistributedTask
return JsonSerializer.Deserialize<T>(propValue);
}
[Obsolete("SetProperty is deprecated, please use indexer this[propName] = propValue instead.")]
public void SetProperty(string propName, object propValue)
public void SetProperty<T>(string propName, T propValue)
{
_props[propName] = JsonSerializer.Serialize(propValue);
}

View File

@ -1,93 +0,0 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Common.Web;
public class DisposableHttpContext : IDisposable
{
private const string Key = "disposable.key";
public object this[string key]
{
get => Items.ContainsKey(key) ? Items[key] : null;
set
{
if (value == null)
{
throw new ArgumentNullException();
}
if (value is not IDisposable)
{
throw new ArgumentException("Only IDisposable may be added!");
}
Items[key] = (IDisposable)value;
}
}
private Dictionary<string, IDisposable> Items
{
get
{
var table = (Dictionary<string, IDisposable>)_context.Items[Key];
if (table == null)
{
table = new Dictionary<string, IDisposable>(1);
_context.Items.Add(Key, table);
}
return table;
}
}
private readonly HttpContext _context;
private bool _isDisposed;
public DisposableHttpContext(HttpContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
_context = ctx;
}
public void Dispose()
{
if (!_isDisposed)
{
foreach (var item in Items.Values)
{
try
{
item.Dispose();
}
catch { }
}
_isDisposed = true;
}
}
}

View File

@ -97,9 +97,10 @@ public class SecurityContext
public bool AuthenticateMe(string cookie)
{
if (!string.IsNullOrEmpty(cookie))
{
if (string.IsNullOrEmpty(cookie)) return false;
if (!_cookieStorage.DecryptCookie(cookie, out var tenant, out var userid, out var indexTenant, out var expire, out var indexUser, out var loginEventId))
{
if (cookie.Equals("Bearer", StringComparison.InvariantCulture))
{
var ipFrom = string.Empty;
@ -115,54 +116,6 @@ public class SecurityContext
}
_logger.InformationEmptyBearer(ipFrom, address);
}
else if (_cookieStorage.DecryptCookie(cookie, out var tenant, out var userid, out var indexTenant, out var expire, out var indexUser, out var loginEventId))
{
if (tenant != _tenantManager.GetCurrentTenant().Id)
{
return false;
}
var settingsTenant = _tenantCookieSettingsHelper.GetForTenant(tenant);
if (indexTenant != settingsTenant.Index)
{
return false;
}
if (expire != DateTime.MaxValue && expire < DateTime.UtcNow)
{
return false;
}
try
{
var settingsUser = _tenantCookieSettingsHelper.GetForUser(userid);
if (indexUser != settingsUser.Index)
{
return false;
}
var settingLoginEvents = _dbLoginEventsManager.GetLoginEventIds(tenant, userid).Result; // remove Result
if (loginEventId != 0 && !settingLoginEvents.Contains(loginEventId))
{
return false;
}
AuthenticateMeWithoutCookie(new UserAccount(new UserInfo { Id = userid }, tenant, _userFormatter));
return true;
}
catch (InvalidCredentialException ice)
{
_logger.AuthenticateDebug(cookie, tenant, userid, ice);
}
catch (SecurityException se)
{
_logger.AuthenticateDebug(cookie, tenant, userid, se);
}
catch (Exception err)
{
_logger.AuthenticateError(cookie, tenant, userid, err);
}
}
else
{
var ipFrom = string.Empty;
@ -179,7 +132,57 @@ public class SecurityContext
_logger.WarningCanNotDecrypt(cookie, ipFrom, address);
}
return false;
}
if (tenant != _tenantManager.GetCurrentTenant().Id)
{
return false;
}
var settingsTenant = _tenantCookieSettingsHelper.GetForTenant(tenant);
if (indexTenant != settingsTenant.Index)
{
return false;
}
if (expire != DateTime.MaxValue && expire < DateTime.UtcNow)
{
return false;
}
try
{
var settingsUser = _tenantCookieSettingsHelper.GetForUser(userid);
if (indexUser != settingsUser.Index)
{
return false;
}
var settingLoginEvents = _dbLoginEventsManager.GetLoginEventIds(tenant, userid).Result; // remove Result
if (loginEventId != 0 && !settingLoginEvents.Contains(loginEventId))
{
return false;
}
AuthenticateMeWithoutCookie(new UserAccount(new UserInfo { Id = userid }, tenant, _userFormatter));
return true;
}
catch (InvalidCredentialException ice)
{
_logger.AuthenticateDebug(cookie, tenant, userid, ice);
}
catch (SecurityException se)
{
_logger.AuthenticateDebug(cookie, tenant, userid, se);
}
catch (Exception err)
{
_logger.AuthenticateError(cookie, tenant, userid, err);
}
return false;
}

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ASC.Core.Common.EF.Model;
public class ModelBuilderWrapper
@ -75,6 +77,11 @@ public class ModelBuilderWrapper
return this;
}
public EntityTypeBuilder<T> Entity<T>() where T : class
{
return ModelBuilder.Entity<T>();
}
public void AddDbFunction()
{
ModelBuilder

View File

@ -30,117 +30,186 @@ namespace ASC.Webhooks.Core;
public class DbWorker
{
private readonly IDbContextFactory<WebhooksDbContext> _dbContextFactory;
private readonly TenantManager _tenantManager;
public DbWorker(IDbContextFactory<WebhooksDbContext> dbContextFactory, TenantManager tenantManager)
private readonly TenantManager _tenantManager;
private readonly AuthContext _authContext;
private int Tenant
{
get
{
return _tenantManager.GetCurrentTenant().Id;
}
}
public DbWorker(IDbContextFactory<WebhooksDbContext> dbContextFactory, TenantManager tenantManager, AuthContext authContext)
{
_dbContextFactory = dbContextFactory;
_tenantManager = tenantManager;
}
public void AddWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
_tenantManager = tenantManager;
_authContext = authContext;
}
var addObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
if (addObj != null)
{
return;
}
webhooksDbContext.WebhooksConfigs.Add(webhooksConfig);
webhooksDbContext.SaveChanges();
}
public int ConfigsNumber()
public async Task<WebhooksConfig> AddWebhookConfig(string name, string uri, string secretKey)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs.Count();
var toAdd = new WebhooksConfig { TenantId = Tenant, Uri = uri, SecretKey = secretKey, Name = name };
toAdd = await webhooksDbContext.AddOrUpdateAsync(r => r.WebhooksConfigs, toAdd);
await webhooksDbContext.SaveChangesAsync();
return toAdd;
}
public List<WebhooksLog> GetTenantWebhooks()
public async IAsyncEnumerable<WebhooksConfig> GetTenantWebhooks()
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var q = webhooksDbContext.WebhooksConfigs
.AsNoTracking()
.Where(it => it.TenantId == Tenant)
.AsAsyncEnumerable();
await foreach (var webhook in q)
{
yield return webhook;
}
}
public IAsyncEnumerable<WebhooksConfig> GetWebhookConfigs()
{
var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs
.Where(t => t.TenantId == Tenant)
.AsAsyncEnumerable();
}
public async Task<WebhooksConfig> UpdateWebhookConfig(int id, string name, string uri, string key, bool? enabled)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var updateObj = await webhooksDbContext.WebhooksConfigs
.Where(it => it.TenantId == Tenant && it.Id == id)
.FirstOrDefaultAsync();
if (updateObj != null)
{
if (!string.IsNullOrEmpty(uri))
{
updateObj.Uri = uri;
}
if (!string.IsNullOrEmpty(name))
{
updateObj.Name = name;
}
if (!string.IsNullOrEmpty(key))
{
updateObj.SecretKey = key;
}
if (enabled.HasValue)
{
updateObj.Enabled = enabled.Value;
}
webhooksDbContext.WebhooksConfigs.Update(updateObj);
await webhooksDbContext.SaveChangesAsync();
}
return updateObj;
}
public async Task<WebhooksConfig> RemoveWebhookConfig(int id)
{
var tenant = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksLogs.Where(it => it.TenantId == tenant)
.Select(t => new WebhooksLog
{
Uid = t.Uid,
CreationTime = t.CreationTime,
RequestPayload = t.RequestPayload,
RequestHeaders = t.RequestHeaders,
ResponsePayload = t.ResponsePayload,
ResponseHeaders = t.ResponseHeaders,
Status = t.Status
}).ToList();
}
public List<WebhooksConfig> GetWebhookConfigs(int tenant)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs.Where(t => t.TenantId == tenant).ToList();
}
public WebhookEntry ReadFromJournal(int id)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksLogs
.Where(it => it.Id == id)
.Join(webhooksDbContext.WebhooksConfigs, t => t.ConfigId, t => t.ConfigId, (payload, config) => new { payload, config })
.Select(t => new WebhookEntry { Id = t.payload.Id, Payload = t.payload.RequestPayload, SecretKey = t.config.SecretKey, Uri = t.config.Uri })
.OrderBy(t => t.Id).FirstOrDefault();
}
public void RemoveWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var removeObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
var removeObj = await webhooksDbContext.WebhooksConfigs
.Where(it => it.TenantId == tenant && it.Id == id)
.FirstOrDefaultAsync();
webhooksDbContext.WebhooksConfigs.Remove(removeObj);
webhooksDbContext.SaveChanges();
}
public void UpdateWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
await webhooksDbContext.SaveChangesAsync();
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var updateObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
webhooksDbContext.WebhooksConfigs.Update(updateObj);
webhooksDbContext.SaveChanges();
}
public void UpdateWebhookJournal(int id, ProcessStatus status, string responsePayload, string responseHeaders, string requestHeaders)
return removeObj;
}
public IAsyncEnumerable<WebhooksLog> ReadJournal(int startIndex, int limit, DateTime? delivery, string hookname, string route)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var webhook = webhooksDbContext.WebhooksLogs.Where(t => t.Id == id).FirstOrDefault();
var webhooksDbContext = _dbContextFactory.CreateDbContext();
var q = webhooksDbContext.WebhooksLogs
.AsNoTracking()
.Where(r => r.TenantId == Tenant);
if (delivery.HasValue)
{
var date = delivery.Value;
q = q.Where(r => r.Delivery == date);
}
if (!string.IsNullOrEmpty(hookname))
{
q = q.Where(r => r.Config.Name == hookname);
}
if (!string.IsNullOrEmpty(route))
{
q = q.Where(r => r.Route == route);
}
if (startIndex != 0)
{
q = q.Skip(startIndex);
}
if (limit != 0)
{
q = q.Take(limit);
}
return q.OrderByDescending(t => t.Id).AsAsyncEnumerable();
}
public async Task<WebhooksLog> ReadJournal(int id)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return await webhooksDbContext.WebhooksLogs
.AsNoTracking()
.Where(it => it.Id == id)
.FirstOrDefaultAsync();
}
public async Task<WebhooksLog> WriteToJournal(WebhooksLog webhook)
{
webhook.TenantId = _tenantManager.GetCurrentTenant().Id;
webhook.Uid = _authContext.CurrentAccount.ID;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var entity = await webhooksDbContext.WebhooksLogs.AddAsync(webhook);
await webhooksDbContext.SaveChangesAsync();
return entity.Entity;
}
public async Task<WebhooksLog> UpdateWebhookJournal(int id, int status, DateTime delivery, string requestHeaders, string responsePayload, string responseHeaders)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var webhook = await webhooksDbContext.WebhooksLogs.Where(t => t.Id == id).FirstOrDefaultAsync();
webhook.Status = status;
webhook.RequestHeaders = requestHeaders;
webhook.ResponsePayload = responsePayload;
webhook.ResponseHeaders = responseHeaders;
webhook.RequestHeaders = requestHeaders;
webhooksDbContext.WebhooksLogs.Update(webhook);
webhooksDbContext.SaveChanges();
}
webhook.ResponseHeaders = responseHeaders;
webhook.Delivery = delivery;
public int WriteToJournal(WebhooksLog webhook)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var entity = webhooksDbContext.WebhooksLogs.Add(webhook);
webhooksDbContext.SaveChanges();
return entity.Entity.Id;
webhooksDbContext.WebhooksLogs.Update(webhook);
await webhooksDbContext.SaveChangesAsync();
return webhook;
}
}

View File

@ -28,8 +28,8 @@ namespace ASC.Webhooks.Core.EF.Context;
public class WebhooksDbContext : DbContext
{
public virtual DbSet<WebhooksConfig> WebhooksConfigs { get; set; }
public virtual DbSet<WebhooksLog> WebhooksLogs { get; set; }
public DbSet<WebhooksConfig> WebhooksConfigs { get; set; }
public DbSet<WebhooksLog> WebhooksLogs { get; set; }
public WebhooksDbContext(DbContextOptions<WebhooksDbContext> options) : base(options) { }

View File

@ -25,12 +25,20 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Webhooks.Core.EF.Model;
public partial class WebhooksConfig
public class WebhooksConfig : BaseEntity
{
public int ConfigId { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public string SecretKey { get; set; }
public int TenantId { get; set; }
public string Uri { get; set; }
public string Uri { get; set; }
public bool Enabled { get; set; }
public override object[] GetKeys()
{
return new object[] { Id };
}
}
public static class WebhooksConfigExtension
@ -46,15 +54,18 @@ public static class WebhooksConfigExtension
{
modelBuilder.Entity<WebhooksConfig>(entity =>
{
entity.HasKey(e => new { e.ConfigId })
.HasName("PRIMARY");
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.ToTable("webhooks_config")
.HasCharSet("utf8");
entity.Property(e => e.ConfigId)
entity.Property(e => e.Id)
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -68,7 +79,17 @@ public static class WebhooksConfigExtension
entity.Property(e => e.SecretKey)
.HasMaxLength(50)
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
.HasDefaultValueSql("''");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasColumnName("name")
.IsRequired();
entity.Property(e => e.Enabled)
.HasColumnName("enabled")
.HasDefaultValueSql("'1'")
.HasColumnType("tinyint(1)");
});
}
@ -76,14 +97,17 @@ public static class WebhooksConfigExtension
{
modelBuilder.Entity<WebhooksConfig>(entity =>
{
entity.HasKey(e => new { e.ConfigId })
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.ToTable("webhooks_config");
entity.ToTable("webhooks_config");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.ConfigId)
entity.Property(e => e.Id)
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -97,7 +121,16 @@ public static class WebhooksConfigExtension
entity.Property(e => e.SecretKey)
.HasMaxLength(50)
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
.HasDefaultValueSql("''");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasColumnName("name")
.IsRequired();
entity.Property(e => e.Enabled)
.HasColumnName("enabled")
.HasDefaultValueSql("true");
});
}
}

View File

@ -26,25 +26,31 @@
namespace ASC.Webhooks.Core.EF.Model;
public partial class WebhooksLog
public class WebhooksLog
{
public int ConfigId { get; set; }
public DateTime CreationTime { get; set; }
public string Event { get; set; }
public int Id { get; set; }
public int Id { get; set; }
public string Method { get; set; }
public string Route { get; set; }
public string RequestHeaders { get; set; }
public string RequestPayload { get; set; }
public string ResponseHeaders { get; set; }
public string ResponsePayload { get; set; }
public ProcessStatus Status { get; set; }
public int Status { get; set; }
public int TenantId { get; set; }
public string Uid { get; set; }
public Guid Uid { get; set; }
public DateTime? Delivery { get; set; }
public WebhooksConfig Config { get; set; }
}
public static class WebhooksPayloadExtension
{
public static ModelBuilderWrapper AddWebhooksLog(this ModelBuilderWrapper modelBuilder)
{
{
modelBuilder.Entity<WebhooksLog>().Navigation(e => e.Config).AutoInclude();
modelBuilder
.Add(MySqlAddWebhooksLog, Provider.MySql)
.Add(PgSqlAddWebhooksLog, Provider.PostgreSql);
@ -60,7 +66,10 @@ public static class WebhooksPayloadExtension
.HasName("PRIMARY");
entity.ToTable("webhooks_logs")
.HasCharSet("utf8");
.HasCharSet("utf8");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.Id)
.HasColumnType("int")
@ -72,9 +81,10 @@ public static class WebhooksPayloadExtension
.HasColumnName("config_id");
entity.Property(e => e.Uid)
.HasColumnType("varchar")
.HasColumnName("uid")
.HasMaxLength(50);
.HasColumnName("uid")
.HasColumnType("varchar(36)")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -83,7 +93,9 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.RequestPayload)
.IsRequired()
.HasColumnName("request_payload")
.HasColumnType("json");
.HasColumnType("text")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.RequestHeaders)
.HasColumnName("request_headers")
@ -91,25 +103,35 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.ResponsePayload)
.HasColumnName("response_payload")
.HasColumnType("json");
.HasColumnType("text")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.ResponseHeaders)
.HasColumnName("response_headers")
.HasColumnType("json");
entity.Property(e => e.Event)
entity.Property(e => e.Method)
.HasColumnType("varchar")
.HasColumnName("event")
.HasColumnName("method")
.HasMaxLength(100);
entity.Property(e => e.Route)
.HasColumnType("varchar")
.HasColumnName("route")
.HasMaxLength(100);
entity.Property(e => e.CreationTime)
.HasColumnType("datetime")
.HasColumnName("creation_time");
entity.Property(e => e.Delivery)
.HasColumnType("datetime")
.HasColumnName("delivery");
entity.Property(e => e.Status)
.HasColumnType("varchar")
.HasColumnName("status")
.HasMaxLength(50);
.HasColumnType("int")
.HasColumnName("status");
});
}
@ -120,7 +142,10 @@ public static class WebhooksPayloadExtension
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.ToTable("webhooks_logs");
entity.ToTable("webhooks_logs");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.Id)
.HasColumnType("int")
@ -142,34 +167,40 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.RequestPayload)
.IsRequired()
.HasColumnName("request_payload")
.HasColumnType("json");
.HasColumnName("request_payload");
entity.Property(e => e.RequestHeaders)
.HasColumnName("request_headers")
.HasColumnType("json");
entity.Property(e => e.ResponsePayload)
.HasColumnName("response_payload")
.HasColumnType("json");
.HasColumnName("response_payload");
entity.Property(e => e.ResponseHeaders)
.HasColumnName("response_headers")
.HasColumnType("json");
entity.Property(e => e.Event)
entity.Property(e => e.Method)
.HasColumnType("varchar")
.HasColumnName("event")
.HasColumnName("method")
.HasMaxLength(100);
entity.Property(e => e.Route)
.HasColumnType("varchar")
.HasColumnName("route")
.HasMaxLength(100);
entity.Property(e => e.CreationTime)
.HasColumnType("datetime")
.HasColumnName("creation_time");
.HasColumnName("creation_time");
entity.Property(e => e.Delivery)
.HasColumnType("datetime")
.HasColumnName("delivery");
entity.Property(e => e.Status)
.HasColumnType("varchar")
.HasColumnName("status")
.HasMaxLength(50);
.HasColumnType("int")
.HasColumnName("status");
});
}
}

View File

@ -29,5 +29,6 @@ namespace ASC.Webhooks.Core;
[Scope]
public interface IWebhookPublisher
{
public void Publish(string eventName, string requestPayload);
public Task PublishAsync(string method, string route, string requestPayload);
public Task<WebhooksLog> PublishAsync(string method, string route, string requestPayload, int configId);
}

View File

@ -30,51 +30,56 @@ namespace ASC.Webhooks.Core;
public class WebhookPublisher : IWebhookPublisher
{
private readonly DbWorker _dbWorker;
private readonly TenantManager _tenantManager;
private readonly ICacheNotify<WebhookRequest> _webhookNotify;
public WebhookPublisher(
DbWorker dbWorker,
TenantManager tenantManager,
ICacheNotify<WebhookRequest> webhookNotify)
{
_dbWorker = dbWorker;
_tenantManager = tenantManager;
_webhookNotify = webhookNotify;
}
public void Publish(string eventName, string requestPayload)
public async Task PublishAsync(string method, string route, string requestPayload)
{
var tenantId = _tenantManager.GetCurrentTenant().Id;
var webhookConfigs = _dbWorker.GetWebhookConfigs(tenantId);
foreach (var config in webhookConfigs)
if (string.IsNullOrEmpty(requestPayload))
{
var webhooksLog = new WebhooksLog
{
Uid = Guid.NewGuid().ToString(),
TenantId = tenantId,
Event = eventName,
CreationTime = DateTime.UtcNow,
RequestPayload = requestPayload,
Status = ProcessStatus.InProcess,
ConfigId = config.ConfigId
};
var DbId = _dbWorker.WriteToJournal(webhooksLog);
return;
}
var webhookConfigs = _dbWorker.GetWebhookConfigs();
var request = new WebhookRequest()
{
Id = DbId
};
_webhookNotify.Publish(request, CacheNotifyAction.Update);
await foreach (var config in webhookConfigs.Where(r => r.Enabled))
{
_ = await PublishAsync(method, route, requestPayload, config.Id);
}
}
}
public enum ProcessStatus
{
InProcess,
Success,
Failed
public async Task<WebhooksLog> PublishAsync(string method, string route, string requestPayload, int configId)
{
if (string.IsNullOrEmpty(requestPayload))
{
return null;
}
var webhooksLog = new WebhooksLog
{
Method = method,
Route = route,
CreationTime = DateTime.UtcNow,
RequestPayload = requestPayload,
ConfigId = configId
};
var webhook = await _dbWorker.WriteToJournal(webhooksLog);
var request = new WebhookRequest
{
Id = webhook.Id
};
_webhookNotify.Publish(request, CacheNotifyAction.Update);
return webhook;
}
}

View File

@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ASC.Api.Core\ASC.Api.Core.csproj" />
</ItemGroup>

View File

@ -37,9 +37,13 @@ global using ASC.Common.Log;
global using ASC.Common.Utils;
global using ASC.Web.Webhooks;
global using ASC.Webhooks.Core;
global using ASC.Webhooks.Service;
global using ASC.Webhooks.Service.Log;
global using ASC.Webhooks.Service.Services;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.Extensions.Hosting.WindowsServices;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Logging;
global using Polly;
global using Polly.Extensions.Http;

View File

@ -41,11 +41,20 @@ builder.Host.ConfigureDefault(args, (hostContext, config, env, path) =>
diHelper.TryAdd<DbWorker>();
services.AddHostedService<BuildQueueService>();
diHelper.TryAdd<BuildQueueService>();
services.AddHostedService<WorkerService>();
diHelper.TryAdd<WorkerService>();
diHelper.TryAdd<WorkerService>();
services.AddHttpClient("webhook")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler((s, request) =>
{
var settings = s.GetRequiredService<Settings>();
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(settings.RepeatCount.HasValue ? settings.RepeatCount.Value : 5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
});
});
builder.WebHost.ConfigureDefaultKestrel();

View File

@ -29,26 +29,36 @@ namespace ASC.Webhooks.Service.Services;
[Singletone]
public class WorkerService : BackgroundService
{
private readonly ILogger<WorkerService> _logger;
private readonly ConcurrentQueue<WebhookRequest> _queue;
private readonly ILogger<WorkerService> _logger;
private readonly int? _threadCount = 10;
private readonly WebhookSender _webhookSender;
private readonly TimeSpan _waitingPeriod;
private readonly TimeSpan _waitingPeriod;
private readonly ConcurrentQueue<WebhookRequest> _queue;
private readonly ICacheNotify<WebhookRequest> _webhookNotify;
public WorkerService(WebhookSender webhookSender,
public WorkerService(
ICacheNotify<WebhookRequest> webhookNotify,
WebhookSender webhookSender,
ILogger<WorkerService> logger,
BuildQueueService buildQueueService,
Settings settings)
{
_logger = logger;
{
_webhookNotify = webhookNotify;
_queue = new ConcurrentQueue<WebhookRequest>();
_logger = logger;
_webhookSender = webhookSender;
_queue = buildQueueService.Queue;
_threadCount = settings.ThreadCount;
_waitingPeriod = TimeSpan.FromSeconds(5);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
{
_webhookNotify.Subscribe(_queue.Enqueue, CacheNotifyAction.Update);
stoppingToken.Register(() =>
{
_webhookNotify.Unsubscribe(CacheNotifyAction.Update);
});
while (!stoppingToken.IsCancellationRequested)
{
var queueSize = _queue.Count;
@ -62,7 +72,7 @@ public class WorkerService : BackgroundService
continue;
}
var tasks = new List<Task>();
var tasks = new List<Task>(queueSize);
var counter = 0;
for (var i = 0; i < queueSize; i++)
@ -82,13 +92,13 @@ public class WorkerService : BackgroundService
if (counter >= _threadCount)
{
Task.WaitAll(tasks.ToArray());
await Task.WhenAll(tasks);
tasks.Clear();
counter = 0;
}
}
Task.WaitAll(tasks.ToArray());
}
await Task.WhenAll(tasks);
_logger.DebugProcedureFinish();
}
}

View File

@ -37,7 +37,7 @@ public class Settings
{
var cfg = configuration.GetSetting<Settings>("webhooks");
RepeatCount = cfg.RepeatCount ?? 5;
ThreadCount = cfg.ThreadCount ?? 1;
ThreadCount = cfg.ThreadCount ?? 10;
}
public int? RepeatCount { get; }
public int? ThreadCount { get; }

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text.Json.Serialization;
namespace ASC.Webhooks.Service;
[Singletone]
@ -31,15 +33,19 @@ public class WebhookSender
{
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _log;
public int? RepeatCount { get; init; }
private readonly IServiceScopeFactory _scopeFactory;
private readonly IServiceScopeFactory _scopeFactory;
private readonly JsonSerializerOptions _jsonSerializerOptions;
public WebhookSender(ILoggerProvider options, IServiceScopeFactory scopeFactory, Settings settings, IHttpClientFactory clientFactory)
public WebhookSender(ILoggerProvider options, IServiceScopeFactory scopeFactory, IHttpClientFactory clientFactory)
{
_log = options.CreateLogger("ASC.Webhooks.Core");
_scopeFactory = scopeFactory;
RepeatCount = settings.RepeatCount;
_clientFactory = clientFactory;
_clientFactory = clientFactory;
_jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IgnoreReadOnlyProperties = true
};
}
public async Task Send(WebhookRequest webhookRequest, CancellationToken cancellationToken)
@ -47,82 +53,65 @@ public class WebhookSender
using var scope = _scopeFactory.CreateScope();
var dbWorker = scope.ServiceProvider.GetRequiredService<DbWorker>();
var entry = dbWorker.ReadFromJournal(webhookRequest.Id);
var id = entry.Id;
var requestURI = entry.Uri;
var secretKey = entry.SecretKey;
var data = entry.Payload;
var response = new HttpResponseMessage();
var request = new HttpRequestMessage();
for (var i = 0; i < RepeatCount; i++)
{
try
{
request = new HttpRequestMessage(HttpMethod.Post, requestURI);
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Secret", "SHA256=" + GetSecretHash(secretKey, data));
request.Content = new StringContent(
data,
Encoding.UTF8,
"application/json");
var httpClient = _clientFactory.CreateClient();
response = await httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Success);
_log.DebugResponse(response);
break;
}
else if (i == RepeatCount - 1)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Failed);
_log.DebugResponse(response);
}
}
catch (Exception ex)
{
if (i == RepeatCount - 1)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Failed);
}
_log.ErrorWithException(ex);
continue;
}
var entry = await dbWorker.ReadJournal(webhookRequest.Id);
var status = 0;
string responsePayload = null;
string responseHeaders = null;
string requestHeaders = null;
var delivery = DateTime.MinValue;
try
{
var httpClient = _clientFactory.CreateClient("webhook");
var request = new HttpRequestMessage(HttpMethod.Post, entry.Config.Uri)
{
Content = new StringContent(entry.RequestPayload, Encoding.UTF8, "application/json")
};
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Secret", "SHA256=" + GetSecretHash(entry.Config.SecretKey, entry.RequestPayload));
requestHeaders = JsonSerializer.Serialize(request.Headers.ToDictionary(r => r.Key, v => v.Value), _jsonSerializerOptions);
var response = await httpClient.SendAsync(request, cancellationToken);
status = (int)response.StatusCode;
responseHeaders = JsonSerializer.Serialize(response.Headers.ToDictionary(r => r.Key, v => v.Value), _jsonSerializerOptions);
responsePayload = await response.Content.ReadAsStringAsync();
delivery = DateTime.UtcNow;
_log.DebugResponse(response);
}
catch (HttpRequestException e)
{
if (e.StatusCode.HasValue)
{
status = (int)e.StatusCode.Value;
}
responsePayload = e.Message;
delivery = DateTime.UtcNow;
_log.ErrorWithException(e);
}
catch (Exception e)
{
_log.ErrorWithException(e);
}
if (delivery != DateTime.MinValue)
{
await dbWorker.UpdateWebhookJournal(entry.Id, status, delivery, requestHeaders, responsePayload, responseHeaders);
}
}
private string GetSecretHash(string secretKey, string body)
{
string computedSignature;
{
var secretBytes = Encoding.UTF8.GetBytes(secretKey);
using (var hasher = new HMACSHA256(secretBytes))
{
var data = Encoding.UTF8.GetBytes(body);
computedSignature = BitConverter.ToString(hasher.ComputeHash(data));
return BitConverter.ToString(hasher.ComputeHash(data));
}
return computedSignature;
}
private void UpdateDb(DbWorker dbWorker, int id, HttpResponseMessage response, HttpRequestMessage request, ProcessStatus status)
{
var responseHeaders = JsonSerializer.Serialize(response.Headers.ToDictionary(r => r.Key, v => v.Value));
var requestHeaders = JsonSerializer.Serialize(request.Headers.ToDictionary(r => r.Key, v => v.Value));
string responsePayload;
using (var streamReader = new StreamReader(response.Content.ReadAsStream()))
{
var responseContent = streamReader.ReadToEnd();
responsePayload = JsonSerializer.Serialize(responseContent);
}
dbWorker.UpdateWebhookJournal(id, status, responsePayload, responseHeaders, requestHeaders);
}
}

View File

@ -34,6 +34,9 @@
"hosting": {
"intervalCheckRegisterInstanceInSeconds": "1",
"timeUntilUnregisterInSeconds": "15"
},
"oidc": {
"authority" : ""
}
},
"license": {
@ -50,7 +53,7 @@
"enabled": "true"
},
"version": {
"number": "11.5.0",
"number": "1.0.0",
"release": {
"date": "",
"sign": ""

View File

@ -0,0 +1,163 @@
// <auto-generated />
using System;
using ASC.Webhooks.Core.EF.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ASC.Migrations.MySql.Migrations.WebhooksDb
{
[DbContext(typeof(WebhooksDbContext))]
[Migration("20220818144209_WebhooksDbContext_Upgrade1")]
partial class WebhooksDbContext_Upgrade1
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("enabled")
.HasDefaultValueSql("'1'");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
b.Property<uint>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uri")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<int>("ConfigId")
.HasColumnType("int")
.HasColumnName("config_id");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
.HasColumnName("request_headers");
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("text")
.HasColumnName("request_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("text")
.HasColumnName("response_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<uint>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("varchar(36)")
.HasColumnName("uid")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,151 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ASC.Migrations.MySql.Migrations.WebhooksDb
{
public partial class WebhooksDbContext_Upgrade1 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "config_id",
table: "webhooks_config",
newName: "id");
migrationBuilder.UpdateData(
table: "webhooks_logs",
keyColumn: "uid",
keyValue: null,
column: "uid",
value: "");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar(36)",
nullable: false,
collation: "utf8_general_ci",
oldClrType: typeof(string),
oldType: "varchar(50)",
oldMaxLength: 50,
oldNullable: true)
.Annotation("MySql:CharSet", "utf8")
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AlterColumn<int>(
name: "status",
table: "webhooks_logs",
type: "int",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(50)",
oldMaxLength: 50)
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AddColumn<DateTime>(
name: "delivery",
table: "webhooks_logs",
type: "datetime",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "enabled",
table: "webhooks_config",
type: "tinyint(1)",
nullable: false,
defaultValueSql: "'1'");
migrationBuilder.AddColumn<string>(
name: "name",
table: "webhooks_config",
type: "varchar(50)",
maxLength: 50,
nullable: false,
defaultValue: "")
.Annotation("MySql:CharSet", "utf8");
migrationBuilder.CreateIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs",
column: "config_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_logs",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_config",
column: "tenant_id");
migrationBuilder.AddForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs",
column: "config_id",
principalTable: "webhooks_config",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "delivery",
table: "webhooks_logs");
migrationBuilder.DropColumn(
name: "enabled",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "name",
table: "webhooks_config");
migrationBuilder.RenameColumn(
name: "id",
table: "webhooks_config",
newName: "config_id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar(50)",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(36)",
oldCollation: "utf8_general_ci")
.Annotation("MySql:CharSet", "utf8")
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AlterColumn<string>(
name: "status",
table: "webhooks_logs",
type: "varchar(50)",
maxLength: 50,
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:CharSet", "utf8");
}
}
}

View File

@ -16,15 +16,27 @@ namespace ASC.Migrations.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("ConfigId")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("enabled")
.HasDefaultValueSql("'1'");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
@ -44,9 +56,12 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("ConfigId")
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
@ -67,10 +82,14 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<string>("Event")
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("event");
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
@ -78,21 +97,28 @@ namespace ASC.Migrations.MySql.Migrations
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("json")
.HasColumnName("request_payload");
.HasColumnType("text")
.HasColumnName("request_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("json")
.HasColumnName("response_payload");
.HasColumnType("text")
.HasColumnName("response_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<uint>("TenantId")
@ -100,17 +126,35 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("uid");
.IsRequired()
.HasColumnType("varchar(36)")
.HasColumnName("uid")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,158 @@
// <auto-generated />
using System;
using ASC.Webhooks.Core.EF.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace ASC.Migrations.PostgreSql.Migrations.WebhooksDb
{
[DbContext(typeof(WebhooksDbContext))]
[Migration("20220818144209_WebhooksDbContext_Upgrade1")]
partial class WebhooksDbContext_Upgrade1
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasColumnName("enabled")
.HasDefaultValueSql("true");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
b.Property<int>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uri")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ConfigId")
.HasColumnType("int")
.HasColumnName("config_id");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
.HasColumnName("request_headers");
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("text")
.HasColumnName("request_payload");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("text")
.HasColumnName("response_payload");
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<int>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar")
.HasColumnName("uid");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,138 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ASC.Migrations.PostgreSql.Migrations.WebhooksDb
{
public partial class WebhooksDbContext_Upgrade1 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "config_id",
table: "webhooks_config",
newName: "id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50,
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "status",
table: "webhooks_logs",
type: "int",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50);
migrationBuilder.AddColumn<DateTime>(
name: "delivery",
table: "webhooks_logs",
type: "datetime",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "enabled",
table: "webhooks_config",
type: "boolean",
nullable: false,
defaultValueSql: "true");
migrationBuilder.AddColumn<string>(
name: "name",
table: "webhooks_config",
type: "character varying(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.CreateIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs",
column: "config_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_logs",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_config",
column: "tenant_id");
migrationBuilder.AddForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs",
column: "config_id",
principalTable: "webhooks_config",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "delivery",
table: "webhooks_logs");
migrationBuilder.DropColumn(
name: "enabled",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "name",
table: "webhooks_config");
migrationBuilder.RenameColumn(
name: "id",
table: "webhooks_config",
newName: "config_id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50);
migrationBuilder.AlterColumn<string>(
name: "status",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: false,
oldClrType: typeof(int),
oldType: "int");
}
}
}

View File

@ -18,17 +18,29 @@ namespace ASC.Migrations.PostgreSql.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("ConfigId")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("config_id")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasColumnName("enabled")
.HasDefaultValueSql("true");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
@ -47,9 +59,12 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("ConfigId")
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
});
@ -69,10 +84,14 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<string>("Event")
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("event");
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
@ -80,7 +99,7 @@ namespace ASC.Migrations.PostgreSql.Migrations
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("json")
.HasColumnType("text")
.HasColumnName("request_payload");
b.Property<string>("ResponseHeaders")
@ -88,13 +107,16 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("json")
.HasColumnType("text")
.HasColumnName("response_payload");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<int>("TenantId")
@ -102,6 +124,7 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar")
.HasColumnName("uid");
@ -109,8 +132,24 @@ namespace ASC.Migrations.PostgreSql.Migrations
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Girişi bərpa edin",
"RecoverContactEmailPlaceholder": "Əlaqə üçün e-poçt ünvanı",
"RecoverDescribeYourProblemPlaceholder": "Probleminizi təsvir edin",
"RecoverTextBody": "Cari hesabınızla giriş edə bilmirsinizsə və ya yeni istifadəçi kimi qeydiyyatdan keçmək istəyirsinizsə, o zaman portal administratoru ilə əlaqə saxlayın.",
"RecoverTitle": "Girişin bərpası",
"TurnOnDesktopVersion": "Masaüstü versiyanı işə salın"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Поднови достъп",
"RecoverContactEmailPlaceholder": "Имейл за контакт",
"RecoverDescribeYourProblemPlaceholder": "Опишете проблема си",
"RecoverTextBody": "Ако не можете да се впишете със съществуващия си профил или искате да се регистрирате като нов потребител, свържете се с администратора на портала. ",
"RecoverTitle": "Възстановяване на достъп",
"TurnOnDesktopVersion": "Превключете на настолна версия"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Obnovit přístup",
"RecoverContactEmailPlaceholder": "Kontaktní emailová adresa",
"RecoverDescribeYourProblemPlaceholder": "Popište svůj problém",
"RecoverTextBody": "Pokud se nemůžete přihlásit pomocí stávajícího účtu nebo chcete být zaregistrováni jako nový uživatel, kontaktujte správce portálu. ",
"RecoverTitle": "Obnovení přístupu",
"TurnOnDesktopVersion": "Zapnout verzi pro počítač"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Zugriff wiederherstellen",
"RecoverContactEmailPlaceholder": "E-Mail-Adresse",
"RecoverDescribeYourProblemPlaceholder": "Bitte Problem beschreiben",
"RecoverTextBody": "Wenn Sie sich mit Ihren Anmeldeinformationen nicht einloggen können oder ein neues Profil erstellen möchten, wenden Sie sich an den Administrator des Portals.",
"RecoverTitle": "Zugriffswiederherstellung",
"TurnOnDesktopVersion": "Desktopversion anzeigen"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Ανάκτηση πρόσβασης",
"RecoverContactEmailPlaceholder": "Email επικοινωνίας",
"RecoverDescribeYourProblemPlaceholder": "Περιγράψτε το πρόβλημά σας",
"RecoverTextBody": "Εάν δεν μπορείτε να συνδεθείτε με τον υπάρχοντα λογαριασμό σας ή θέλετε να εγγραφείτε ως νέος χρήστης, επικοινωνήστε με τον διαχειριστή της πύλης.",
"RecoverTitle": "Ανάκτηση πρόσβασης",
"TurnOnDesktopVersion": "Ενεργοποιήστε την έκδοση για υπολογιστές"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Recover access",
"RecoverContactEmailPlaceholder": "Contact email",
"RecoverDescribeYourProblemPlaceholder": "Describe your problem",
"RecoverTextBody": "If you can't log in with your existing account or want to be registered as a new user, contact the portal administrator.",
"RecoverTitle": "Access recovery",
"TurnOnDesktopVersion": "Turn on desktop version"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Recuperar acceso",
"RecoverContactEmailPlaceholder": "E-mail de contacto",
"RecoverDescribeYourProblemPlaceholder": "Describa su problema",
"RecoverTextBody": "Si no puede conectarse con su cuenta actual o quiere registrarse como nuevo usuario, póngase en contacto con el administrador del portal. ",
"RecoverTitle": "Recuperación de acceso",
"TurnOnDesktopVersion": "Activar la versión de escritorio"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Palauta käyttöoikeus",
"RecoverContactEmailPlaceholder": "Sähköposti yhteydenottoa varten",
"RecoverDescribeYourProblemPlaceholder": "Kuvaile ongelmasi",
"RecoverTextBody": "Jos et voi kirjautua sisään nykyisellä tililläsi tai haluat rekisteröityä uutena käyttäjänä, ota yhteyttä portaalin järjestelmänvalvojaan. ",
"RecoverTitle": "Käytä palautusta",
"TurnOnDesktopVersion": "Ota työpöytäversio käyttöön"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Récupérer l'accès",
"RecoverContactEmailPlaceholder": "E-mail de contact",
"RecoverDescribeYourProblemPlaceholder": "Décrivez votre problème",
"RecoverTextBody": "Si vous ne pouvez pas vous connecter avec votre compte existant ou que vous souhaitez en créer un nouveau, merci de contacter l'administrateur du portail.",
"RecoverTitle": "Récupération d'accès",
"TurnOnDesktopVersion": "Activer l'application de bureau"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Վերականգնել մատչումը",
"RecoverContactEmailPlaceholder": "Կոնտակտային էլ. փոստ",
"RecoverDescribeYourProblemPlaceholder": "Նկարագրեք Ձեր խնդիրը",
"RecoverTextBody": "Եթե ​​Դուք չեք կարող մուտք գործել Ձեր գոյություն ունեցող հաշիվ կամ ցանկանում եք գրանցվել որպես նոր օգտվող, կապվեք կայքէջի ադմինիստրատորի հետ:",
"RecoverTitle": "Մատչման վերականգնում",
"TurnOnDesktopVersion": "Միացնել աշխատասեղանի տարբերակը"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Rimuovi accesso",
"RecoverContactEmailPlaceholder": "Email di contatto",
"RecoverDescribeYourProblemPlaceholder": "Descrivi il tuo problema",
"RecoverTextBody": "Se non riesci ad accedere con il tuo account esistente o desideri essere registrato come un nuovo utente, contatta l'amministratore del portale.",
"RecoverTitle": "Ripristino dell'accesso",
"TurnOnDesktopVersion": "Attivare la versione desktop"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "アクセス回復",
"RecoverContactEmailPlaceholder": "連絡先メール",
"RecoverDescribeYourProblemPlaceholder": "ご質問を説明ください",
"RecoverTextBody": "既存のアカウントでログインできない場合や、新規ユーザーとして登録したい場合は、ポータル管理者にお問い合わせください。 ",
"RecoverTitle": "アクセス回復機能",
"TurnOnDesktopVersion": "デスクトップ版を起動する"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "액세스 복구",
"RecoverContactEmailPlaceholder": "연락처 이메일",
"RecoverDescribeYourProblemPlaceholder": "겪고 계신 문제를 설명해주세요",
"RecoverTextBody": "기존 계정으로 로그인하실 수 없거나 새로운 사용자로 등록하시길 원하시는 경우, 포털 관리자에게 문의하세요. ",
"RecoverTitle": "액세스 복원",
"TurnOnDesktopVersion": "데스크탑 버전 켜기"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "ກູ້ຄືນການເຂົ້າເຖິງ",
"RecoverContactEmailPlaceholder": "ອີເມວຕິດຕໍ່",
"RecoverDescribeYourProblemPlaceholder": "ອະທິບາຍບັນຫາຂອງທ່ານ",
"RecoverTextBody": "ຖ້າທ່ານບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້ຫຼືຕ້ອງການລົງທະບຽນໃຫມ່, ໃຫ້ທ່ານຕິດຕໍ່ຜູ້ດູແລລະບົບ portal ",
"RecoverTitle": "ການເຂົ້າເຖິງການກູ້ຄືນ",
"TurnOnDesktopVersion": "ເປີດທາງ desktop"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Atgūt piekļuvi",
"RecoverContactEmailPlaceholder": "E-pasts saziņai",
"RecoverDescribeYourProblemPlaceholder": "Aprakstiet savu problēmu",
"RecoverTextBody": "Ja nevarat pieteikties ar savu esošo kontu vai vēlaties reģistrēties kā jauns lietotājs, sazinieties ar portāla administratoru. ",
"RecoverTitle": "Piekļuves atkopšana",
"TurnOnDesktopVersion": "Ieslēgt darbvirsmas versiju"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Toegang herstellen",
"RecoverContactEmailPlaceholder": "Contact e-mail",
"RecoverDescribeYourProblemPlaceholder": "Beschrijf uw probleem",
"RecoverTextBody": "Als u zich niet kunt aanmelden met uw bestaande account of als nieuwe gebruiker wilt worden geregistreerd, neem dan contact op met de portaalbeheerder.",
"RecoverTitle": "Toegang herstel",
"TurnOnDesktopVersion": "Schakel desktop versie in"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Odzyskaj dostęp",
"RecoverContactEmailPlaceholder": "E-mail kontaktowy",
"RecoverDescribeYourProblemPlaceholder": "Opisz swój problem",
"RecoverTextBody": "Jeśli nie możesz się zalogować za pomocą istniejącego konta, lub chcesz zarejestrować się jako nowy użytkownik, skontaktuj się z administratorem portalu. ",
"RecoverTitle": "Odzyskiwanie dostępu",
"TurnOnDesktopVersion": "Włącz wersję na komputer stacjonarny"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Recuperar o acesso",
"RecoverContactEmailPlaceholder": "E-mail de contato",
"RecoverDescribeYourProblemPlaceholder": "Descreva seu problema",
"RecoverTextBody": "Se você não conseguir entrar com sua conta existente ou quiser ser registrado como um novo usuário, entre em contato com o administrador do portal. ",
"RecoverTitle": "Recuperação de acesso",
"TurnOnDesktopVersion": "Ligar a versão desktop"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Recuperar o acesso",
"RecoverContactEmailPlaceholder": "Email de contacto",
"RecoverDescribeYourProblemPlaceholder": "Descreva o seu problema",
"RecoverTextBody": "Se não consegue iniciar sessão com a sua conta ou se quiser registar-se como um novo utilizador, contacte o administrador do portal.",
"RecoverTitle": "Recuperação de acesso",
"TurnOnDesktopVersion": "Ativar a versão para computador"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Recuperarea accesului la cont",
"RecoverContactEmailPlaceholder": "E-mail de contact",
"RecoverDescribeYourProblemPlaceholder": "Descrieți problema",
"RecoverTextBody": "Dacă nu puteți să vă conectați la contul dvs curent sau doriți să vă înregistrați din nou, contactați administratorul portalului. ",
"RecoverTitle": "Restabilirea accesului",
"TurnOnDesktopVersion": "Lansați versiunea desktop"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Доступ к порталу",
"RecoverContactEmailPlaceholder": "Адрес email, по которому можно связаться с Вами",
"RecoverDescribeYourProblemPlaceholder": "Пожалуйста, опишите вашу проблему",
"RecoverTextBody": "Если Вы уже зарегистрированы и у Вас есть проблемы с доступом к этому порталу, или Вы хотите зарегистрироваться как новый пользователь портала, пожалуйста, обратитесь к администратору портала, используя форму, расположенную ниже.",
"RecoverTitle": "Доступ к порталу",
"TurnOnDesktopVersion": "Включить версию для ПК"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Obnoviť prístup",
"RecoverContactEmailPlaceholder": "Kontaktný email",
"RecoverDescribeYourProblemPlaceholder": "Opíšte svoj problém",
"RecoverTextBody": "Ak sa nemôžete prihlásiť so svojím existujúcim kontom, alebo sa chcete zaregistrovať ako nový užívateľ, kontaktujte admina portálu.",
"RecoverTitle": "Obnova prístupu",
"TurnOnDesktopVersion": "Zapnúť počítačovú verziu"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Obnova dostopa",
"RecoverContactEmailPlaceholder": "Kontaktni email",
"RecoverDescribeYourProblemPlaceholder": "Opišite vaš problem",
"RecoverTextBody": "Če se ne morete prijaviti z obstoječim računom ali se želite registrirati kot nov uporabnik, se obrnite na skrbnika portala. ",
"RecoverTitle": "Dostop za obnovo",
"TurnOnDesktopVersion": "Vključi namizno verzijo"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Erişimi kurtar",
"RecoverContactEmailPlaceholder": "İletişim e-postası",
"RecoverDescribeYourProblemPlaceholder": "Sorununuzu tarif edin",
"RecoverTextBody": "Eğer hesabınıza giriş yapamıyorsanız veya yeni kullanıcı olarak kayıt olmak istiyorsanız, portal yöneticisi ile iletişime geçin. ",
"RecoverTitle": "Erişim kurtarma",
"TurnOnDesktopVersion": "Masaüstü sürümünü aç"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Відновити доступ",
"RecoverContactEmailPlaceholder": "Контактна електронна пошта",
"RecoverDescribeYourProblemPlaceholder": "Опишіть свою проблему",
"RecoverTextBody": "Якщо ви не можете увійти зі своїм існуючим обліковим записом або хочете зареєструватися як новий користувач, зверніться до адміністратора порталу.",
"RecoverTitle": "Відновлення доступу",
"TurnOnDesktopVersion": "Увімкнути настільну версію"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "Khôi phục quyền truy cập",
"RecoverContactEmailPlaceholder": "Email liên hệ",
"RecoverDescribeYourProblemPlaceholder": "Mô tả vấn đề của bạn",
"RecoverTextBody": "Nếu bạn không thể đăng nhập bằng tài khoản hiện có của mình hoặc muốn đăng ký làm người dùng mới, hãy liên hệ với quản trị viên cổng thông tin.",
"RecoverTitle": "Khôi phục truy cập",
"TurnOnDesktopVersion": "Bật phiên bản dành cho máy tính"
}

View File

@ -1,8 +1,3 @@
{
"RecoverAccess": "恢复访问",
"RecoverContactEmailPlaceholder": "联系邮箱",
"RecoverDescribeYourProblemPlaceholder": "描述您的问题",
"RecoverTextBody": "如果您无法使用现有账户登录或希望注册为新用户,请联系门户管理员。",
"RecoverTitle": "访问恢复",
"TurnOnDesktopVersion": "打开桌面版"
}

View File

@ -0,0 +1,32 @@
import React from "react";
import styled from "styled-components";
import { ReactSVG } from "react-svg";
import { hugeMobile } from "@docspace/components/utils/device";
import { isMobileOnly } from "react-device-detect";
const StyledWrapper = styled.div`
.logo-wrapper {
width: 100%;
height: 46px;
@media ${hugeMobile} {
display: none;
}
}
`;
const DocspaceLogo = (props) => {
const { className } = props;
if (isMobileOnly) return <></>;
return (
<StyledWrapper>
<ReactSVG
src="/static/images/docspace.big.react.svg"
className={`logo-wrapper ${className}`}
/>
</StyledWrapper>
);
};
export default DocspaceLogo;

View File

@ -417,7 +417,6 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
const pathname = window.location.pathname.toLowerCase();
const isEditor = pathname.indexOf("doceditor") !== -1;
const isLogin = pathname.indexOf("login") !== -1;
const loginRoutes = [];
@ -463,7 +462,7 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
<Router history={history}>
<Toast />
<ReactSmartBanner t={t} ready={ready} />
{isEditor || isLogin || !isMobileOnly ? <></> : <NavMenu />}
{isEditor || !isMobileOnly ? <></> : <NavMenu />}
<IndicatorLoader />
<ScrollToTop />
<DialogsWrapper t={t} />

View File

@ -1,12 +1,6 @@
import React from "react";
import styled, { css } from "styled-components";
import {
isIOS,
isFirefox,
isSafari,
isMobile,
isMobileOnly,
} from "react-device-detect";
import { isIOS, isFirefox, isMobileOnly } from "react-device-detect";
const StyledMain = styled.main`
height: ${isIOS && !isFirefox ? "calc(var(--vh, 1vh) * 100)" : "100vh"};
@ -41,6 +35,7 @@ const Main = React.memo((props) => {
// }, []);
//console.log("Main render");
return <StyledMain className="main" {...props} />;
});

View File

@ -2,7 +2,6 @@ import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import Box from "@docspace/components/box";
import RecoverAccess from "./recover-access-container";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@docspace/common/utils";
@ -24,7 +23,10 @@ const Header = styled.header`
width: 475px;
}
@media (max-width: 375px) {
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
//padding: 0 16px;
}
}
@ -40,10 +42,9 @@ const Header = styled.header`
}
.header-logo-icon {
width: 146px;
height: 24px;
position: relative;
padding: 3px 20px 0 6px;
width: 100%;
height: 100%;
padding: 12px 0;
cursor: pointer;
}
`;
@ -69,18 +70,11 @@ const HeaderUnAuth = ({
{!isAuthenticated && isLoaded ? (
<div>
<a className="header-logo-wrapper" href="/">
<img
className="header-logo-min_icon"
src={combineUrl(
AppServerConfig.proxyURL,
"/static/images/nav.logo.react.svg"
)}
/>
<img
className="header-logo-icon"
src={combineUrl(
AppServerConfig.proxyURL,
"/static/images/nav.logo.opened.react.svg"
"/static/images/logo.docspace.react.svg"
)}
/>
</a>
@ -88,8 +82,6 @@ const HeaderUnAuth = ({
) : (
<></>
)}
<div>{enableAdmMess && !wizardToken && <RecoverAccess t={t} />}</div>
</Box>
</Header>
);

View File

@ -1,136 +0,0 @@
import React, { useState } from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import Box from "@docspace/components/box";
import Text from "@docspace/components/text";
import toastr from "@docspace/components/toast/toastr";
import UnionIcon from "../svg/union.react.svg";
import RecoverAccessModalDialog from "./recover-access-modal-dialog";
import { sendRecoverRequest } from "@docspace/common/api/settings/index";
import commonIconsStyles from "@docspace/components/utils/common-icons-style";
import { Base } from "@docspace/components/themes";
const StyledUnionIcon = styled(UnionIcon)`
${commonIconsStyles}
`;
const RecoverContainer = styled(Box)`
cursor: pointer;
background-color: ${(props) => props.theme.header.recoveryColor};
.recover-icon {
@media (max-width: 450px) {
padding: 12px;
}
}
.recover-text {
@media (max-width: 450px) {
display: none;
}
}
`;
RecoverContainer.defaultProps = { theme: Base };
const RecoverAccess = ({ t }) => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [emailErr, setEmailErr] = useState(false);
const [description, setDescription] = useState("");
const [descErr, setDescErr] = useState(false);
const onRecoverClick = () => {
setVisible(true);
};
const onRecoverModalClose = () => {
setVisible(false);
setEmail("");
setEmailErr(false);
setDescription("");
setDescErr(false);
};
const onChangeEmail = (e) => {
setEmail(e.currentTarget.value);
setEmailErr(false);
};
const onChangeDescription = (e) => {
setDescription(e.currentTarget.value);
setDescErr(false);
};
const onSendRecoverRequest = () => {
if (!email.trim()) {
setEmailErr(true);
}
if (!description.trim()) {
setDescErr(true);
} else {
setLoading(true);
sendRecoverRequest(email, description)
.then((res) => {
setLoading(false);
toastr.success(res);
})
.catch((error) => {
setLoading(false);
toastr.error(error);
})
.finally(onRecoverModalClose);
}
};
return (
<>
<Box
widthProp="100%"
heightProp="100%"
displayProp="flex"
justifyContent="flex-end"
alignItems="center"
>
<RecoverContainer
heightProp="100%"
displayProp="flex"
onClick={onRecoverClick}
>
<Box paddingProp="12px 8px 12px 12px" className="recover-icon">
<StyledUnionIcon />
</Box>
<Box
paddingProp="14px 12px 14px 0px"
className="recover-text"
widthProp="100%"
>
<Text color="#fff" isBold={true}>
{t("RecoverAccess")}
</Text>
</Box>
</RecoverContainer>
</Box>
{visible && (
<RecoverAccessModalDialog
visible={visible}
loading={loading}
email={email}
emailErr={emailErr}
description={description}
descErr={descErr}
t={t}
onChangeEmail={onChangeEmail}
onChangeDescription={onChangeDescription}
onRecoverModalClose={onRecoverModalClose}
onSendRecoverRequest={onSendRecoverRequest}
/>
)}
</>
);
};
RecoverAccess.propTypes = {
t: PropTypes.func.isRequired,
};
export default RecoverAccess;

View File

@ -0,0 +1,26 @@
import React from "react";
import styled, { css } from "styled-components";
import { isIOS, isFirefox, isMobileOnly } from "react-device-detect";
const StyledWrapper = styled.div`
height: ${isIOS && !isFirefox ? "calc(var(--vh, 1vh) * 100)" : "100vh"};
width: 100vw;
z-index: 0;
display: flex;
flex-direction: row;
box-sizing: border-box;
${isMobileOnly &&
css`
height: auto;
min-height: 100%;
width: 100%;
`}
`;
const ConfirmWrapper = (props) => {
const { children, className } = props;
return <StyledWrapper className={className}>{children}</StyledWrapper>;
};
export default ConfirmWrapper;

View File

@ -1,6 +1,7 @@
import React, { lazy } from "react";
import { Switch } from "react-router-dom";
import ConfirmRoute from "SRC_DIR/helpers/confirmRoute";
import ConfirmRoute from "../../helpers/confirmRoute";
import ConfirmWrapper from "./ConfirmWrapper";
const ActivateUserForm = lazy(() => import("./sub-components/activateUser"));
const CreateUserForm = lazy(() => import("./sub-components/createUser"));
@ -19,56 +20,58 @@ const Confirm = ({ match }) => {
//console.log("Confirm render");
const path = match.path;
return (
<Switch>
<ConfirmRoute
forUnauthorized
path={`${path}/LinkInvite`}
component={CreateUserForm}
/>
<ConfirmRoute
forUnauthorized
path={`${path}/Activation`}
component={ActivateUserForm}
/>
<ConfirmRoute
exact
path={`${path}/EmailActivation`}
component={ActivateEmailForm}
/>
<ConfirmRoute
exact
path={`${path}/EmailChange`}
component={ChangeEmailForm}
/>
<ConfirmRoute
forUnauthorized
path={`${path}/PasswordChange`}
component={ChangePasswordForm}
/>
<ConfirmRoute
exact
path={`${path}/ProfileRemove`}
component={ProfileRemoveForm}
/>
<ConfirmRoute
exact
path={`${path}/PhoneActivation`}
component={ChangePhoneForm}
/>
<ConfirmRoute
exact
path={`${path}/PortalOwnerChange`}
component={ChangeOwnerForm}
/>
<ConfirmRoute exact path={`${path}/TfaAuth`} component={TfaAuthForm} />
<ConfirmRoute
exact
path={`${path}/TfaActivation`}
component={TfaActivationForm}
/>
<ConfirmWrapper className="with-background-pattern">
<Switch>
<ConfirmRoute
forUnauthorized
path={`${path}/LinkInvite`}
component={CreateUserForm}
/>
<ConfirmRoute
forUnauthorized
path={`${path}/Activation`}
component={ActivateUserForm}
/>
<ConfirmRoute
exact
path={`${path}/EmailActivation`}
component={ActivateEmailForm}
/>
<ConfirmRoute
exact
path={`${path}/EmailChange`}
component={ChangeEmailForm}
/>
<ConfirmRoute
forUnauthorized
path={`${path}/PasswordChange`}
component={ChangePasswordForm}
/>
<ConfirmRoute
exact
path={`${path}/ProfileRemove`}
component={ProfileRemoveForm}
/>
<ConfirmRoute
exact
path={`${path}/PhoneActivation`}
component={ChangePhoneForm}
/>
<ConfirmRoute
exact
path={`${path}/PortalOwnerChange`}
component={ChangeOwnerForm}
/>
<ConfirmRoute exact path={`${path}/TfaAuth`} component={TfaAuthForm} />
<ConfirmRoute
exact
path={`${path}/TfaActivation`}
component={TfaActivationForm}
/>
{/* <Route component={Error404} /> */}
</Switch>
{/* <Route component={Error404} /> */}
</Switch>
</ConfirmWrapper>
);
};

View File

@ -13,28 +13,41 @@ export const StyledPage = styled.div`
}
@media ${mobile} {
margin-top: 72px;
}
`;
export const StyledHeader = styled.div`
text-align: left;
.title {
margin-bottom: 24px;
margin-top: 32px;
}
.subtitle {
margin-bottom: 32px;
}
.password-form {
width: 100%;
margin-bottom: 8px;
}
`;
export const StyledHeader = styled.div`
.title {
margin-bottom: 32px;
text-align: center;
}
.subtitle {
margin-bottom: 32px;
}
.docspace-logo {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 64px;
}
`;
export const StyledBody = styled.div`
width: 320px;
@media ${tablet} {
justify-content: center;
}
display: flex;
flex-direction: column;
align-items: center;
@media ${mobile} {
width: 100%;
@ -48,37 +61,27 @@ export const StyledBody = styled.div`
width: 100%;
}
.confirm-button {
width: 100%;
margin-top: 8px;
}
.password-change-form {
margin-top: 32px;
margin-bottom: 16px;
}
.confirm-subtitle {
margin-bottom: 8px;
}
.info-delete {
.phone-input {
margin-bottom: 24px;
}
.phone-input {
margin-top: 32px;
margin-bottom: 16px;
.delete-profile-confirm {
margin-bottom: 8px;
}
.phone-title {
margin-bottom: 8px;
}
`;
export const ButtonsWrapper = styled.div`
display: flex;
flex: 1fr 1fr;
flex-direction: row;
gap: 16px;
.button {
width: 100%;
}
width: 100%;
`;

View File

@ -12,6 +12,8 @@ import {
ButtonsWrapper,
} from "./StyledConfirm";
import withLoader from "../withLoader";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const ChangeOwnerForm = (props) => {
const { t, greetingTitle } = props;
@ -20,34 +22,36 @@ const ChangeOwnerForm = (props) => {
<StyledPage>
<StyledBody>
<StyledHeader>
<DocspaceLogo className="docspace-logo" />
<Text fontSize="23px" fontWeight="700" className="title">
{greetingTitle}
</Text>
</StyledHeader>
<FormWrapper>
<Text className="subtitle">
{t("ConfirmOwnerPortalTitle", { newOwner: "NEW OWNER" })}
</Text>
</StyledHeader>
<ButtonsWrapper>
<Button
className="button"
primary
size="normal"
label={t("Common:SaveButton")}
tabIndex={2}
isDisabled={false}
//onClick={this.onAcceptClick} // call toast with t("ConfirmOwnerPortalSuccessMessage")
/>
<Button
className="button"
size="normal"
label={t("Common:CancelButton")}
tabIndex={2}
isDisabled={false}
//onClick={this.onCancelClick}
/>
</ButtonsWrapper>
<ButtonsWrapper>
<Button
primary
scale
size="medium"
label={t("Common:SaveButton")}
tabIndex={2}
isDisabled={false}
//onClick={this.onAcceptClick} // call toast with t("ConfirmOwnerPortalSuccessMessage")
/>
<Button
scale
size="medium"
label={t("Common:CancelButton")}
tabIndex={2}
isDisabled={false}
//onClick={this.onCancelClick}
/>
</ButtonsWrapper>
</FormWrapper>
</StyledBody>
</StyledPage>
);

View File

@ -9,10 +9,12 @@ import FieldContainer from "@docspace/components/field-container";
import { inject, observer } from "mobx-react";
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
import withLoader from "../withLoader";
import { getPasswordErrorMessage } from "SRC_DIR/helpers/utils";
import { getPasswordErrorMessage } from "../../../helpers/utils";
import { createPasswordHash } from "@docspace/common/utils";
import tryRedirectTo from "@docspace/common/utils/tryRedirectTo";
import toastr from "@docspace/components/toast/toastr";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const ChangePasswordForm = (props) => {
const {
@ -81,63 +83,66 @@ const ChangePasswordForm = (props) => {
<StyledPage>
<StyledBody>
<StyledHeader>
<Text fontSize="23px" fontWeight="700">
<DocspaceLogo className="docspace-logo" />
<Text fontSize="23px" fontWeight="700" className="title">
{greetingTitle}
</Text>
</StyledHeader>
<div className="password-change-form">
<Text className="confirm-subtitle">{t("PassworResetTitle")}</Text>
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={isPasswordErrorShow && !passwordValid}
errorMessage={`${t(
"Common:PasswordLimitMessage"
)}: ${getPasswordErrorMessage(t, settings)}`}
>
<PasswordInput
className="confirm-input"
simpleView={false}
passwordSettings={settings}
id="password"
inputName="password"
placeholder={t("Common:Password")}
type="password"
inputValue={password}
<FormWrapper>
<div className="password-form">
<Text fontSize="16px" fontWeight="600" className="subtitle">
{t("PassworResetTitle")}
</Text>
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={isPasswordErrorShow && !passwordValid}
size="large"
scale={true}
tabIndex={1}
autoComplete="current-password"
onChange={onChangePassword}
onValidateInput={onValidatePassword}
onBlur={onBlurPassword}
onKeyDown={onKeyPress}
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
tooltipPasswordLength={`${t("Common:PasswordMinimumLength")}: ${
settings ? settings.minLength : 8
}`}
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
tooltipPasswordCapital={`${t("Common:PasswordLimitUpperCase")}`}
tooltipPasswordSpecial={`${t(
"Common:PasswordLimitSpecialSymbols"
)}`}
generatePasswordTitle={t("Wizard:GeneratePassword")}
/>
</FieldContainer>
</div>
errorMessage={`${t(
"Common:PasswordLimitMessage"
)}: ${getPasswordErrorMessage(t, settings)}`}
>
<PasswordInput
simpleView={false}
passwordSettings={settings}
id="password"
inputName="password"
placeholder={t("Common:Password")}
type="password"
inputValue={password}
hasError={isPasswordErrorShow && !passwordValid}
size="large"
scale
tabIndex={1}
autoComplete="current-password"
onChange={onChangePassword}
onValidateInput={onValidatePassword}
onBlur={onBlurPassword}
onKeyDown={onKeyPress}
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
tooltipPasswordLength={`${t("Common:PasswordMinimumLength")}: ${
settings ? settings.minLength : 8
}`}
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
tooltipPasswordCapital={`${t("Common:PasswordLimitUpperCase")}`}
tooltipPasswordSpecial={`${t(
"Common:PasswordLimitSpecialSymbols"
)}`}
generatePasswordTitle={t("Wizard:GeneratePassword")}
/>
</FieldContainer>
</div>
<Button
className="confirm-button"
primary
size="normal"
label={t("Common:Create")}
tabIndex={5}
onClick={onSubmit}
isDisabled={isLoading}
/>
<Button
primary
size="medium"
scale
label={t("Common:Create")}
tabIndex={5}
onClick={onSubmit}
isDisabled={isLoading}
/>
</FormWrapper>
</StyledBody>
</StyledPage>
);

View File

@ -8,6 +8,8 @@ import Section from "@docspace/common/components/Section";
import { inject, observer } from "mobx-react";
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
import withLoader from "../withLoader";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const ChangePhoneForm = (props) => {
const { t, greetingTitle } = props;
@ -17,39 +19,45 @@ const ChangePhoneForm = (props) => {
<StyledPage>
<StyledBody>
<StyledHeader>
<DocspaceLogo className="docspace-logo" />
<Text fontSize="23px" fontWeight="700" className="title">
{greetingTitle}
</Text>
<Text fontSize="16px" fontWeight="600" className="confirm-subtitle">
{t("EnterPhone")}
</Text>
<Text>
{t("CurrentNumber")}: {currentNumber}
</Text>
<Text>{t("PhoneSubtitle")}</Text>
</StyledHeader>
<TextInput
className="phone-input"
id="phone"
name="phone"
type="phone"
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
hasError={false}
guide={false}
/>
<FormWrapper>
<div className="subtitle">
<Text fontSize="16px" fontWeight="600" className="phone-title">
{t("EnterPhone")}
</Text>
<Text>
{t("CurrentNumber")}: {currentNumber}
</Text>
<Text>{t("PhoneSubtitle")}</Text>
</div>
<Button
className="confirm-button"
primary
size="normal"
label={t("GetCode")}
tabIndex={2}
isDisabled={false}
/>
<TextInput
className="phone-input"
id="phone"
name="phone"
type="phone"
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
hasError={false}
guide={false}
/>
<Button
primary
scale
size="medium"
label={t("GetCode")}
tabIndex={2}
isDisabled={false}
/>
</FormWrapper>
</StyledBody>
</StyledPage>
);

View File

@ -14,7 +14,6 @@ import PasswordInput from "@docspace/components/password-input";
import FieldContainer from "@docspace/components/field-container";
import toastr from "@docspace/components/toast/toastr";
import SocialButton from "@docspace/components/social-button";
//import FacebookButton from "@docspace/components/facebook-button";
import {
getAuthProviders,
getCapabilities,
@ -30,16 +29,15 @@ import withLoader from "../withLoader";
import MoreLoginModal from "login/moreLogin";
import AppLoader from "@docspace/common/components/AppLoader";
import EmailInput from "@docspace/components/email-input";
import { getPasswordErrorMessage } from "SRC_DIR/helpers/utils";
import { hugeMobile, tablet } from "@docspace/components/utils/device";
import { getPasswordErrorMessage } from "../../../helpers/utils";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
export const ButtonsWrapper = styled.div`
display: flex;
flex-direction: column;
width: 320px;
@media (max-width: 768px) {
width: 100%;
}
width: 100%;
.buttonWrapper {
margin-bottom: 8px;
@ -48,15 +46,29 @@ export const ButtonsWrapper = styled.div`
`;
const ConfirmContainer = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 70px;
align-items: center;
margin: 80px auto 0 auto;
max-width: 960px;
display: flex;
flex: 1fr 1fr;
gap: 80px;
flex-direction: row;
justify-content: center;
margin-top: 80px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
@media ${tablet} {
margin: 100px auto 0 auto;
display: flex;
flex: 1fr;
flex-direction: column;
align-items: center;
gap: 80px;
}
@media ${hugeMobile} {
margin-top: 32px;
width: 100%;
flex: 1fr;
flex-direction: column;
gap: 80px;
padding-right: 8px;
}
`;
@ -71,9 +83,17 @@ const GreetingContainer = styled.div`
display: ${(props) => !props.isGreetingMode && "none"};
}
@media ${hugeMobile} {
width: 100%;
}
.greeting-title {
width: 100%;
padding-bottom: 32px;
@media ${tablet} {
text-align: center;
}
}
.greeting-block {
@ -107,6 +127,21 @@ const GreetingContainer = styled.div`
padding: 16px;
width: 100%;
}
.docspace-logo {
padding-bottom: 32px;
.injected-svg {
height: 44px;
}
@media ${tablet} {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 64px;
}
}
`;
const RegisterContainer = styled.div`
@ -114,6 +149,7 @@ const RegisterContainer = styled.div`
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
.or-label {
margin: 0 8px;
@ -125,18 +161,10 @@ const RegisterContainer = styled.div`
.line {
display: flex;
width: 320px;
width: 100%;
align-items: center;
color: #eceef1;
padding-top: 35px;
@media (max-width: 768px) {
width: 480px;
}
@media (max-width: 414px) {
width: 311px;
}
}
.line:before,
@ -152,7 +180,7 @@ const RegisterContainer = styled.div`
.auth-form-container {
margin-top: 32px;
width: 320px;
width: 100%;
.form-field {
height: 48px;
@ -527,6 +555,7 @@ const Confirm = (props) => {
return (
<ConfirmContainer>
<GreetingContainer isGreetingMode={isGreetingMode}>
<DocspaceLogo className="docspace-logo" />
<Text
fontSize="23px"
fontWeight={700}
@ -553,162 +582,183 @@ const Confirm = (props) => {
</div>
</GreetingContainer>
<RegisterContainer isGreetingMode={isGreetingMode}>
{ssoExists() && <ButtonsWrapper>{ssoButton()}</ButtonsWrapper>}
<FormWrapper>
<RegisterContainer isGreetingMode={isGreetingMode}>
{ssoExists() && <ButtonsWrapper>{ssoButton()}</ButtonsWrapper>}
{oauthDataExists() && (
<>
<ButtonsWrapper>{providerButtons()}</ButtonsWrapper>
{providers && providers.length > 2 && (
<Link
isHovered
type="action"
fontSize="13px"
fontWeight="600"
color="#3B72A7"
className="more-label"
onClick={moreAuthOpen}
>
{t("Common:ShowMore")}
</Link>
)}
</>
)}
{oauthDataExists() && (
<>
<ButtonsWrapper>{providerButtons()}</ButtonsWrapper>
{providers && providers.length > 2 && (
<Link
isHovered
type="action"
fontSize="13px"
fontWeight="600"
color="#3B72A7"
className="more-label"
onClick={moreAuthOpen}
>
{t("Common:ShowMore")}
</Link>
)}
</>
)}
{(oauthDataExists() || ssoExists()) && (
<div className="line">
<Text color="#A3A9AE" className="or-label">
{t("Or")}
</Text>
</div>
)}
{(oauthDataExists() || ssoExists()) && (
<div className="line">
<Text color="#A3A9AE" className="or-label">
{t("Or")}
</Text>
</div>
)}
<form className="auth-form-container">
<div className="auth-form-fields">
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={isEmailErrorShow && !emailValid}
errorMessage={
emailErrorText
? t(`Common:${emailErrorText}`)
: t("Common:RequiredField")
}
>
<EmailInput
id="login"
name="login"
type="email"
<form className="auth-form-container">
<div className="auth-form-fields">
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={isEmailErrorShow && !emailValid}
value={email}
placeholder={t("Common:Email")}
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={onChangeEmail}
onBlur={onBlurEmail}
onValidateInput={onValidateEmail}
forwardedRef={inputRef}
/>
</FieldContainer>
errorMessage={
emailErrorText
? t(`Common:${emailErrorText}`)
: t("Common:RequiredField")
}
>
<EmailInput
id="login"
name="login"
type="email"
hasError={isEmailErrorShow && !emailValid}
value={email}
placeholder={t("Common:Email")}
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={onChangeEmail}
onBlur={onBlurEmail}
onValidateInput={onValidateEmail}
forwardedRef={inputRef}
/>
</FieldContainer>
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={!fnameValid}
errorMessage={errorText ? errorText : t("Common:RequiredField")}
>
<TextInput
id="first-name"
name="first-name"
type="text"
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={!fnameValid}
value={fname}
placeholder={t("FirstName")}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
onChange={onChangeFname}
onKeyDown={onKeyPress}
/>
</FieldContainer>
errorMessage={errorText ? errorText : t("Common:RequiredField")}
>
<TextInput
id="first-name"
name="first-name"
type="text"
hasError={!fnameValid}
value={fname}
placeholder={t("FirstName")}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
onChange={onChangeFname}
onKeyDown={onKeyPress}
/>
</FieldContainer>
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={!snameValid}
errorMessage={errorText ? errorText : t("Common:RequiredField")}
>
<TextInput
id="last-name"
name="last-name"
type="text"
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={!snameValid}
value={sname}
placeholder={t("Common:LastName")}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
onChange={onChangeSname}
onKeyDown={onKeyPress}
/>
</FieldContainer>
errorMessage={errorText ? errorText : t("Common:RequiredField")}
>
<TextInput
id="last-name"
name="last-name"
type="text"
hasError={!snameValid}
value={sname}
placeholder={t("Common:LastName")}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
onChange={onChangeSname}
onKeyDown={onKeyPress}
/>
</FieldContainer>
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={isPasswordErrorShow && !passwordValid}
errorMessage={`${t(
"Common:PasswordLimitMessage"
)}: ${getPasswordErrorMessage(t, settings)}`}
>
<PasswordInput
simpleView={false}
hideNewPasswordButton
showCopyLink={false}
passwordSettings={settings}
id="password"
inputName="password"
placeholder={t("Common:Password")}
type="password"
<FieldContainer
className="form-field"
isVertical={true}
labelVisible={false}
hasError={isPasswordErrorShow && !passwordValid}
inputValue={password}
size="large"
errorMessage={`${t(
"Common:PasswordLimitMessage"
)}: ${getPasswordErrorMessage(t, settings)}`}
>
<PasswordInput
simpleView={false}
hideNewPasswordButton
showCopyLink={false}
passwordSettings={settings}
id="password"
inputName="password"
placeholder={t("Common:Password")}
type="password"
hasError={isPasswordErrorShow && !passwordValid}
inputValue={password}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
onBlur={onBlurPassword}
onKeyDown={onKeyPress}
onValidateInput={onValidatePassword}
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
tooltipPasswordLength={`${t(
"Common:PasswordMinimumLength"
)}: ${settings ? settings.minLength : 8}`}
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
tooltipPasswordCapital={`${t(
"Common:PasswordLimitUpperCase"
)}`}
tooltipPasswordSpecial={`${t(
"Common:PasswordLimitSpecialSymbols"
)}`}
generatePasswordTitle={t("Wizard:GeneratePassword")}
/>
</FieldContainer>
<Button
id="submit"
className="login-button"
primary
size="medium"
scale={true}
label={
isLoading
? t("Common:LoadingProcessing")
: t("LoginRegistryButton")
}
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
onBlur={onBlurPassword}
onKeyDown={onKeyPress}
onValidateInput={onValidatePassword}
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
tooltipPasswordLength={`${t("Common:PasswordMinimumLength")}: ${
settings ? settings.minLength : 8
}`}
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
tooltipPasswordCapital={`${t("Common:PasswordLimitUpperCase")}`}
tooltipPasswordSpecial={`${t(
"Common:PasswordLimitSpecialSymbols"
)}`}
generatePasswordTitle={t("Wizard:GeneratePassword")}
isLoading={isLoading}
onClick={onSubmit}
/>
</FieldContainer>
</div>
<Button
id="submit"
className="login-button"
className="login-button is-greeting-mode-button"
primary
size="normal"
size="medium"
scale={true}
label={
isLoading
@ -718,38 +768,21 @@ const Confirm = (props) => {
tabIndex={1}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit}
onClick={onGreetingSubmit}
/>
</div>
</form>
<Button
id="submit"
className="login-button is-greeting-mode-button"
primary
size="normal"
scale={true}
label={
isLoading
? t("Common:LoadingProcessing")
: t("LoginRegistryButton")
}
tabIndex={1}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onGreetingSubmit}
<MoreLoginModal
t={t}
visible={moreAuthVisible}
onClose={moreAuthClose}
providers={providers}
onSocialLoginClick={onSocialButtonClick}
ssoLabel={ssoLabel}
ssoUrl={ssoUrl}
/>
</form>
<MoreLoginModal
t={t}
visible={moreAuthVisible}
onClose={moreAuthClose}
providers={providers}
onSocialLoginClick={onSocialButtonClick}
ssoLabel={ssoLabel}
ssoUrl={ssoUrl}
/>
</RegisterContainer>
</RegisterContainer>
</FormWrapper>
</ConfirmContainer>
);
};

View File

@ -9,6 +9,8 @@ import { deleteSelf } from "@docspace/common/api/people";
import toastr from "@docspace/components/toast/toastr";
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
import withLoader from "../withLoader";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const ProfileRemoveForm = (props) => {
const { t, greetingTitle, linkData, logout } = props;
@ -35,6 +37,7 @@ const ProfileRemoveForm = (props) => {
<StyledPage>
<StyledBody>
<StyledHeader>
<DocspaceLogo className="docspace-logo" />
<Text fontSize="23px" fontWeight="700" className="title">
{t("DeleteProfileSuccessMessage")}
</Text>
@ -51,26 +54,34 @@ const ProfileRemoveForm = (props) => {
<StyledPage>
<StyledBody>
<StyledHeader>
<DocspaceLogo className="docspace-logo" />
<Text fontSize="23px" fontWeight="700" className="title">
{greetingTitle}
</Text>
<Text fontSize="16px" fontWeight="600" className="confirm-subtitle">
{t("DeleteProfileConfirmation")}
</Text>
<Text className="info-delete">
{t("DeleteProfileConfirmationInfo")}
</Text>
</StyledHeader>
<Button
className="confirm-button"
primary
size="normal"
label={t("DeleteProfileBtn")}
tabIndex={1}
isDisabled={isLoading}
onClick={onDeleteProfile}
/>
<FormWrapper>
<div className="subtitle">
<Text
fontSize="16px"
fontWeight="600"
className="delete-profile-confirm"
>
{t("DeleteProfileConfirmation")}
</Text>
<Text>{t("DeleteProfileConfirmationInfo")}</Text>
</div>
<Button
primary
scale
size="medium"
label={t("DeleteProfileBtn")}
tabIndex={1}
isDisabled={isLoading}
onClick={onDeleteProfile}
/>
</FormWrapper>
</StyledBody>
</StyledPage>
);

View File

@ -14,36 +14,53 @@ import toastr from "client/toastr";
import ErrorContainer from "@docspace/common/components/ErrorContainer";
import { hugeMobile, tablet } from "@docspace/components/utils/device";
import Link from "@docspace/components/link";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const StyledForm = styled(Box)`
margin: 63px auto auto 216px;
margin-top: 63px;
display: flex;
flex: 1fr 1fr;
gap: 80px;
flex-direction: row;
justify-content: center;
@media ${tablet} {
margin: 120px auto;
width: 480px;
margin: 100px auto 0 auto;
display: flex;
flex: 1fr;
flex-direction: column;
align-items: center;
gap: 32px;
}
@media ${hugeMobile} {
margin-top: 72px;
margin-top: 32px;
width: 100%;
flex: 1fr;
flex-direction: column;
gap: 0px;
padding-right: 8px;
}
.app-code-wrapper {
width: 100%;
@media ${tablet} {
flex-direction: column;
}
}
.docspace-logo {
padding-bottom: 64px;
@media ${tablet} {
display: flex;
align-items: center;
justify-content: center;
}
}
.set-app-description {
width: 100%;
max-width: 500px;
@ -61,8 +78,7 @@ const StyledForm = styled(Box)`
display: flex;
align-items: center;
justify-content: center;
padding: 24px 80px;
background: #f8f9f9;
padding: 0px 80px;
border-radius: 6px;
margin-bottom: 32px;
@ -70,6 +86,10 @@ const StyledForm = styled(Box)`
display: none;
}
}
.app-code-continue-btn {
margin-top: 8px;
}
`;
const TfaActivationForm = withLoader((props) => {
const {
@ -114,6 +134,7 @@ const TfaActivationForm = withLoader((props) => {
return (
<StyledForm className="set-app-container">
<Box className="set-app-description" marginProp="0 0 32px 0">
<DocspaceLogo className="docspace-logo" />
<Text isBold fontSize="14px" className="set-app-title">
{t("SetAppTitle")}
</Text>
@ -149,56 +170,58 @@ const TfaActivationForm = withLoader((props) => {
</Trans>
</Text>
</Box>
<Box
displayProp="flex"
flexDirection="column"
className="app-code-wrapper"
>
<div className="qrcode-wrapper">
<img src={qrCode} height="180px" width="180px" alt="QR-code"></img>
</div>
<Box className="app-code-input">
<FieldContainer
labelVisible={false}
hasError={error ? true : false}
errorMessage={error}
>
<TextInput
id="code"
name="code"
type="text"
size="large"
scale
isAutoFocussed
tabIndex={1}
placeholder={t("EnterCodePlaceholder")}
isDisabled={isLoading}
maxLength={6}
onChange={(e) => {
setCode(e.target.value);
setError("");
}}
value={code}
<FormWrapper>
<Box
displayProp="flex"
flexDirection="column"
className="app-code-wrapper"
>
<div className="qrcode-wrapper">
<img src={qrCode} height="180px" width="180px" alt="QR-code"></img>
</div>
<Box className="app-code-input">
<FieldContainer
labelVisible={false}
hasError={error ? true : false}
onKeyDown={onKeyPress}
errorMessage={error}
>
<TextInput
id="code"
name="code"
type="text"
size="large"
scale
isAutoFocussed
tabIndex={1}
placeholder={t("EnterCodePlaceholder")}
isDisabled={isLoading}
maxLength={6}
onChange={(e) => {
setCode(e.target.value);
setError("");
}}
value={code}
hasError={error ? true : false}
onKeyDown={onKeyPress}
/>
</FieldContainer>
</Box>
<Box className="app-code-continue-btn">
<Button
scale
primary
size="medium"
tabIndex={3}
label={
isLoading ? t("Common:LoadingProcessing") : t("SetAppButton")
}
isDisabled={!code.length || isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</FieldContainer>
</Box>
</Box>
<Box className="app-code-continue-btn">
<Button
scale
primary
size="medium"
tabIndex={3}
label={
isLoading ? t("Common:LoadingProcessing") : t("SetAppButton")
}
isDisabled={!code.length || isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</Box>
</Box>
</FormWrapper>
</StyledForm>
);
});

View File

@ -16,6 +16,8 @@ import {
smallTablet,
tablet,
} from "@docspace/components/utils/device";
import FormWrapper from "@docspace/components/form-wrapper";
import DocspaceLogo from "../../../DocspaceLogo";
const StyledForm = styled(Box)`
margin: 63px auto;
@ -34,19 +36,28 @@ const StyledForm = styled(Box)`
}
@media ${hugeMobile} {
margin: 72px 8px auto 8px;
padding: 16px;
margin: 32px 8px auto 8px;
padding-left: 8px;
width: 100%;
}
.docspace-logo {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 64px;
}
.app-code-wrapper {
@media ${tablet} {
flex-direction: column;
}
width: 100%;
}
.app-code-text {
margin-bottom: 14px;
margin-bottom: 8px;
}
.app-code-continue-btn {
margin-top: 8px;
}
`;
@ -82,65 +93,66 @@ const TfaAuthForm = withLoader((props) => {
if (target.code === "Enter" || target.code === "NumpadEnter") onSubmit();
};
const width = window.innerWidth;
return (
<StyledForm className="app-code-container">
<Box className="app-code-description" marginProp="0 0 32px 0">
<Text isBold fontSize="14px" className="app-code-text">
{t("EnterAppCodeTitle")}
</Text>
<Text>{t("EnterAppCodeDescription")}</Text>
</Box>
<Box
displayProp="flex"
flexDirection="column"
className="app-code-wrapper"
>
<Box className="app-code-input">
<FieldContainer
labelVisible={false}
hasError={error ? true : false}
errorMessage={error}
>
<TextInput
id="code"
name="code"
type="text"
size="huge"
scale
isAutoFocussed
tabIndex={1}
placeholder={t("EnterCodePlaceholder")}
isDisabled={isLoading}
maxLength={6}
onChange={(e) => {
setCode(e.target.value);
setError("");
}}
value={code}
<DocspaceLogo className="docspace-logo" />
<FormWrapper>
<Box className="app-code-description" marginProp="0 0 32px 0">
<Text isBold fontSize="14px" className="app-code-text">
{t("EnterAppCodeTitle")}
</Text>
<Text>{t("EnterAppCodeDescription")}</Text>
</Box>
<Box
displayProp="flex"
flexDirection="column"
className="app-code-wrapper"
>
<Box className="app-code-input">
<FieldContainer
labelVisible={false}
hasError={error ? true : false}
onKeyDown={onKeyPress}
errorMessage={error}
>
<TextInput
id="code"
name="code"
type="text"
size="huge"
scale
isAutoFocussed
tabIndex={1}
placeholder={t("EnterCodePlaceholder")}
isDisabled={isLoading}
maxLength={6}
onChange={(e) => {
setCode(e.target.value);
setError("");
}}
value={code}
hasError={error ? true : false}
onKeyDown={onKeyPress}
/>
</FieldContainer>
</Box>
<Box className="app-code-continue-btn">
<Button
scale
primary
size="medium"
tabIndex={3}
label={
isLoading
? t("Common:LoadingProcessing")
: t("Common:ContinueButton")
}
isDisabled={!code.length || isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</FieldContainer>
</Box>
</Box>
<Box className="app-code-continue-btn">
<Button
scale
primary
size="medium"
tabIndex={3}
label={
isLoading
? t("Common:LoadingProcessing")
: t("Common:ContinueButton")
}
isDisabled={!code.length || isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</Box>
</Box>
</FormWrapper>
</StyledForm>
);
});

View File

@ -62,3 +62,18 @@ body.desktop {
padding: 30px;
bottom: 15px;
}
.with-background-pattern {
background-image: url("/static/images/background.pattern.react.svg");
background-repeat: no-repeat;
background-attachment: fixed;
background-size: 100% 100%;
@media (max-width: 1024px) {
background-size: cover;
}
@media (max-width: 428px) {
background-image: none;
}
}

View File

@ -0,0 +1,36 @@
import React from "react";
import styled from "styled-components";
import { tablet, hugeMobile } from "@docspace/components/utils/device";
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 32px;
background: #ffffff;
box-shadow: 0px 5px 20px rgba(4, 15, 27, 0.07);
border-radius: 12px;
max-width: 320px;
min-width: 320px;
@media ${tablet} {
max-width: 416px;
min-width: 416px;
}
@media ${hugeMobile} {
padding: 0;
border-radius: 0;
box-shadow: none;
max-width: 343px;
min-width: 343px;
background: #ffffff;
}
`;
const FormWrapper = (props) => {
const { children } = props;
return <StyledWrapper>{children}</StyledWrapper>;
};
export default FormWrapper;

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Kodu tapa bilmirsiniz? «Spam» qutunuzu yoxlayın.",
"Or": "VƏ YA",
"PasswordRecoveryTitle": "Parolun bərpa edilməsi",
"RecoverAccess": "Girişi bərpa edin",
"RecoverContactEmailPlaceholder": "Əlaqə üçün e-poçt ünvanı",
"RecoverDescribeYourProblemPlaceholder": "Probleminizi təsvir edin",
"RecoverTextBody": "Cari hesabınızla giriş edə bilmirsinizsə və ya yeni istifadəçi kimi qeydiyyatdan keçmək istəyirsinizsə, o zaman portal administratoru ilə əlaqə saxlayın.",
"RecoverTitle": "Girişin bərpası",
"Register": "Qeydiyyatdan keç",
"RegisterSendButton": "Sorğunu göndər",
"RegisterTextBodyAfterDomainsList": "Qeydiyyatdan keçmək üçün, elektron poçt ünvanını daxil edin və Sorğunu göndər düyməsinə basın.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Не можете да намерите кода? Проверете папката «Спам».",
"Or": "ИЛИ",
"PasswordRecoveryTitle": "Възстановяване на парола",
"RecoverAccess": "Поднови достъп",
"RecoverContactEmailPlaceholder": "Имейл за контакт",
"RecoverDescribeYourProblemPlaceholder": "Опишете проблема си",
"RecoverTextBody": "Ако не можете да се впишете със съществуващия си профил или искате да се регистрирате като нов потребител, свържете се с администратора на портала. ",
"RecoverTitle": "Възстановяване на достъп",
"Register": "Регистрирай се",
"RegisterSendButton": "Изпрати заявка",
"RegisterTextBodyAfterDomainsList": "За да се регистрирате, въведете имейла си и натиснете Изпрати заявка. Връзка за активация ще Ви бъде изпратена.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Nemůžete najít kód? Zkontrolujte složku «Spam».",
"Or": "NEBO",
"PasswordRecoveryTitle": "Obnovení hesla",
"RecoverAccess": "Obnovit přístup",
"RecoverContactEmailPlaceholder": "Kontaktní emailová adresa",
"RecoverDescribeYourProblemPlaceholder": "Popište svůj problém",
"RecoverTextBody": "Pokud se nemůžete přihlásit pomocí stávajícího účtu nebo chcete být zaregistrováni jako nový uživatel, kontaktujte správce portálu. ",
"RecoverTitle": "Obnovení přístupu",
"Register": "Registrovat se",
"RegisterSendButton": "Odeslat požadavek",
"RegisterTextBodyAfterDomainsList": "Pro registraci zadejte svůj e-mail a klikněte na tlačítko Odeslat žádost. Bude vám zaslán aktivační odkaz. ",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Sie können den Code nicht finden? Prüfen Sie Ihren Spam-Ordner.",
"Or": "oder",
"PasswordRecoveryTitle": "Kennwort wiederherstellen",
"RecoverAccess": "Zugriff wiederherstellen",
"RecoverContactEmailPlaceholder": "E-Mail-Adresse",
"RecoverDescribeYourProblemPlaceholder": "Bitte Problem beschreiben",
"RecoverTextBody": "Wenn Sie sich mit Ihren Anmeldeinformationen nicht einloggen können oder ein neues Profil erstellen möchten, wenden Sie sich an den Administrator des Portals.",
"RecoverTitle": "Zugriffswiederherstellung",
"Register": "Registrieren",
"RegisterSendButton": "Anfrage senden",
"RegisterTextBodyAfterDomainsList": "Für Registrierung geben Sie Ihre E-Mail-Adresse ein und klicken Sie auf Anfrage senden. Der Link zur Aktivierung Ihres Kontos wird an dieser Adresse gesendet.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Δεν μπορείτε να βρείτε τον κωδικό; Ελέγξτε τον φάκελο «Ανεπιθύμητα»",
"Or": "ή",
"PasswordRecoveryTitle": "Ανάκτηση κωδικού πρόσβασης",
"RecoverAccess": "Ανάκτηση πρόσβασης",
"RecoverContactEmailPlaceholder": "Email επικοινωνίας",
"RecoverDescribeYourProblemPlaceholder": "Περιγράψτε το πρόβλημά σας",
"RecoverTextBody": "Εάν δεν μπορείτε να συνδεθείτε με τον υπάρχοντα λογαριασμό σας ή θέλετε να εγγραφείτε ως νέος χρήστης, επικοινωνήστε με τον διαχειριστή της πύλης.",
"RecoverTitle": "Ανάκτηση πρόσβασης",
"Register": "Εγγραφή",
"RegisterSendButton": "Αποστολή αιτήματος",
"RegisterTextBodyAfterDomainsList": "Για να εγγραφείτε, πληκτρολογήστε το email σας και κάντε κλικ στο κουμπί Αποστολή αιτήματος. Εκεί θα σας αποσταλεί ένας σύνδεσμος ενεργοποίησης.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Can't find the code? Check your «Spam» folder.",
"Or": "OR",
"PasswordRecoveryTitle": "Password recovery",
"RecoverAccess": "Recover access",
"RecoverContactEmailPlaceholder": "Contact email",
"RecoverDescribeYourProblemPlaceholder": "Describe your problem",
"RecoverTextBody": "If you can't log in with your existing account or want to be registered as a new user, contact the portal administrator.",
"RecoverTitle": "Access recovery",
"Register": "Register",
"RegisterSendButton": "Send request",
"RegisterTextBodyAfterDomainsList": "To register, enter your email and click Send request. A message with a link to activate your account will be sent to the specified address.",
@ -22,5 +27,7 @@
"RegistrationEmailWatermark": "Email",
"Remember": "Remember me",
"RememberHelper": "The default session lifetime is 20 minutes. Check this option to set it to 1 year. To set your own value, go to Settings.",
"ResendCode": "Resend code"
"ResendCode": "Resend code",
"SignInWithCode": "Sign in with secret code",
"SignInWithPassword": "Sign in with a password"
}

View File

@ -13,6 +13,11 @@
"NotFoundCode": "¿No encuentra el código? Compruebe su carpeta de correo no deseado.",
"Or": "O",
"PasswordRecoveryTitle": "Recuperación de contraseña",
"RecoverAccess": "Recuperar acceso",
"RecoverContactEmailPlaceholder": "E-mail de contacto",
"RecoverDescribeYourProblemPlaceholder": "Describa su problema",
"RecoverTextBody": "Si no puede conectarse con su cuenta actual o quiere registrarse como nuevo usuario, póngase en contacto con el administrador del portal. ",
"RecoverTitle": "Recuperación de acceso",
"Register": "Registrarse",
"RegisterSendButton": "Enviar solicitud",
"RegisterTextBodyAfterDomainsList": "Para registrarse, introduzca su email y haga clic en Enviar solicitud. Se le enviará un enlace de activación.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Etkö löydä koodia? Tarkista «Roskaposti» -kansiosi.",
"Or": "TAI",
"PasswordRecoveryTitle": "Salasanan palautus",
"RecoverAccess": "Palauta käyttöoikeus",
"RecoverContactEmailPlaceholder": "Sähköposti yhteydenottoa varten",
"RecoverDescribeYourProblemPlaceholder": "Kuvaile ongelmasi",
"RecoverTextBody": "Jos et voi kirjautua sisään nykyisellä tililläsi tai haluat rekisteröityä uutena käyttäjänä, ota yhteyttä portaalin järjestelmänvalvojaan. ",
"RecoverTitle": "Käytä palautusta",
"Register": "Rekisteröidy",
"RegisterSendButton": "Lähetä pyyntö",
"RegisterTextBodyAfterDomainsList": "Rekisteröidy kirjoittamalla sähköpostiosoitteesi ja napsauttamalla Lähetä pyyntö. Sinulle lähetetään aktivointilinkki. ",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Vous n'avez pas trouvé le code ? Veuillez vérifier votre dossier 'Spam'.",
"Or": "Ou",
"PasswordRecoveryTitle": "Récupération du mot de passe",
"RecoverAccess": "Récupérer l'accès",
"RecoverContactEmailPlaceholder": "E-mail de contact",
"RecoverDescribeYourProblemPlaceholder": "Décrivez votre problème",
"RecoverTextBody": "Si vous ne pouvez pas vous connecter avec votre compte existant ou que vous souhaitez en créer un nouveau, merci de contacter l'administrateur du portail.",
"RecoverTitle": "Récupération d'accès",
"Register": "Inscription",
"RegisterSendButton": "Envoyer une demande",
"RegisterTextBodyAfterDomainsList": "Pour vous inscrire, merci de saisir votre adresse e-mail et de cliquer sur Envoyer une demande. Un message avec un lien pour activer votre compte sera envoyé à cette adresse.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Չե՞ք կարողանում գտնել կոդը:Ստուգեք Ձեր «Սպամ» պանակը.",
"Or": "Կամ",
"PasswordRecoveryTitle": "Գաղտնաբառի վերականգնում",
"RecoverAccess": "Վերականգնել մատչումը",
"RecoverContactEmailPlaceholder": "Կոնտակտային էլ. փոստ",
"RecoverDescribeYourProblemPlaceholder": "Նկարագրեք Ձեր խնդիրը",
"RecoverTextBody": "Եթե ​​Դուք չեք կարող մուտք գործել Ձեր գոյություն ունեցող հաշիվ կամ ցանկանում եք գրանցվել որպես նոր օգտվող, կապվեք կայքէջի ադմինիստրատորի հետ:",
"RecoverTitle": "Մատչման վերականգնում",
"Register": "Գրանցվել",
"RegisterSendButton": "Հայցում ուղարկել",
"RegisterTextBodyAfterDomainsList": "Գրանցվելու համար մուտքագրեք Ձեր էլ.փոստը և սեղմեք Ուղարկել հայցումը: Ձեզ կուղարկվի ակտիվացման հղում:",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Non riesci a trovare il codice? Controlla la tua cartella «Spam».",
"Or": "o",
"PasswordRecoveryTitle": "Recupero della password",
"RecoverAccess": "Rimuovi accesso",
"RecoverContactEmailPlaceholder": "Email di contatto",
"RecoverDescribeYourProblemPlaceholder": "Descrivi il tuo problema",
"RecoverTextBody": "Se non riesci ad accedere con il tuo account esistente o desideri essere registrato come un nuovo utente, contatta l'amministratore del portale.",
"RecoverTitle": "Ripristino dell'accesso",
"Register": "Registra",
"RegisterSendButton": "Invia richiesta",
"RegisterTextBodyAfterDomainsList": "Per registrarti, inserisci la tua email e clicca su Inviare la richiesta. Un messaggio con un link per attivare il tuo account verrà inviato all'indirizzo specificato.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "コードが見つかりませんか?「迷惑メール」のフォルダをご確認ください。",
"Or": "または",
"PasswordRecoveryTitle": "パスワード復旧",
"RecoverAccess": "アクセス回復",
"RecoverContactEmailPlaceholder": "連絡先メール",
"RecoverDescribeYourProblemPlaceholder": "ご質問を説明ください",
"RecoverTextBody": "既存のアカウントでログインできない場合や、新規ユーザーとして登録したい場合は、ポータル管理者にお問い合わせください。 ",
"RecoverTitle": "アクセス回復機能",
"Register": "登録",
"RegisterSendButton": "リクエスト送信",
"RegisterTextBodyAfterDomainsList": "登録するには、メールアドレスを入力して「リクエスト送信」をクリックします。指定されたアドレスに、アカウント有効化のためのリンクが添付されたメッセージが送信されます。",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "코드가 안 보이세요? «스팸» 폴더를 확인해 보세요.",
"Or": "또는",
"PasswordRecoveryTitle": "비밀번호 복원",
"RecoverAccess": "액세스 복구",
"RecoverContactEmailPlaceholder": "연락처 이메일",
"RecoverDescribeYourProblemPlaceholder": "겪고 계신 문제를 설명해주세요",
"RecoverTextBody": "기존 계정으로 로그인하실 수 없거나 새로운 사용자로 등록하시길 원하시는 경우, 포털 관리자에게 문의하세요. ",
"RecoverTitle": "액세스 복원",
"Register": "가입",
"RegisterSendButton": "요청 전송",
"RegisterTextBodyAfterDomainsList": "가입하시려면, 이메일을 입력한 뒤 가입 요청을 클릭하세요. 활성화 링크가 전송됩니다.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "ບໍ່ສາມາດຊອກຫາລະຫັດໄດ້ບໍ? ກວດເບິ່ງໂຟນເດີ «Spam» ຂອງທ່ານ.",
"Or": "ຫຼື",
"PasswordRecoveryTitle": "ກູ້ຄືນລະຫັດຜ່ານ",
"RecoverAccess": "ກູ້ຄືນການເຂົ້າເຖິງ",
"RecoverContactEmailPlaceholder": "ອີເມວຕິດຕໍ່",
"RecoverDescribeYourProblemPlaceholder": "ອະທິບາຍບັນຫາຂອງທ່ານ",
"RecoverTextBody": "ຖ້າທ່ານບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້ຫຼືຕ້ອງການລົງທະບຽນໃຫມ່, ໃຫ້ທ່ານຕິດຕໍ່ຜູ້ດູແລລະບົບ portal ",
"RecoverTitle": "ການເຂົ້າເຖິງການກູ້ຄືນ",
"Register": "ລົງທະບຽນ",
"RegisterSendButton": "ສົ່ງຄຳຂໍ",
"RegisterTextBodyAfterDomainsList": "ການລົງທະບຽນ, ໃຫ້ທ່ານປ້ອນອີເມວຂອງທ່ານແລ້ວກົດສົ່ງຄຳຮ້ອງຂໍ. ລີງຂໍເປີດໃຊ້ງານຈະສົ່ງຫາທ່ານຜ່ານທາງອີເມວ",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Vai nevarat atrast kodu? Pārbaudiet mapi Surogātpasts.",
"Or": "VAI",
"PasswordRecoveryTitle": "Paroles atgūšana",
"RecoverAccess": "Atgūt piekļuvi",
"RecoverContactEmailPlaceholder": "E-pasts saziņai",
"RecoverDescribeYourProblemPlaceholder": "Aprakstiet savu problēmu",
"RecoverTextBody": "Ja nevarat pieteikties ar savu esošo kontu vai vēlaties reģistrēties kā jauns lietotājs, sazinieties ar portāla administratoru. ",
"RecoverTitle": "Piekļuves atkopšana",
"Register": "Reģistrēties",
"RegisterSendButton": "Sūtīt pieprasījumu",
"RegisterTextBodyAfterDomainsList": "Lai reģistrētos, ievadiet savu e-pastu un noklikšķiniet uz Sūtīt pieprasījumu. Jums tiks nosūtīta aktivizācijas saite.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Kunt u de code niet vinden? Bekijk uw \"Spam\" map.",
"Or": "OR",
"PasswordRecoveryTitle": "Wachtwoord herstellen",
"RecoverAccess": "Toegang herstellen",
"RecoverContactEmailPlaceholder": "Contact e-mail",
"RecoverDescribeYourProblemPlaceholder": "Beschrijf uw probleem",
"RecoverTextBody": "Als u zich niet kunt aanmelden met uw bestaande account of als nieuwe gebruiker wilt worden geregistreerd, neem dan contact op met de portaalbeheerder.",
"RecoverTitle": "Toegang herstel",
"Register": "Registreren",
"RegisterSendButton": "Stuur verzoek",
"RegisterTextBodyAfterDomainsList": "Om te registreren, voert u uw e-mail in en klikt u op Stuur verzoek. U ontvangt een activatielink.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Nie możesz znaleźć kodu? Sprawdź swój folder ze spamem.",
"Or": "LUB",
"PasswordRecoveryTitle": "Odzyskaj hasło",
"RecoverAccess": "Odzyskaj dostęp",
"RecoverContactEmailPlaceholder": "E-mail kontaktowy",
"RecoverDescribeYourProblemPlaceholder": "Opisz swój problem",
"RecoverTextBody": "Jeśli nie możesz się zalogować za pomocą istniejącego konta, lub chcesz zarejestrować się jako nowy użytkownik, skontaktuj się z administratorem portalu. ",
"RecoverTitle": "Odzyskiwanie dostępu",
"Register": "Zarejestruj",
"RegisterSendButton": "Wyślij wniosek",
"RegisterTextBodyAfterDomainsList": "Aby zarejestrować się, podaj swój adres e-mail i kliknij Wyślij wniosek. Link aktywacyjny zostanie wysłany na wskazany przez Ciebie adres. ",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Você não consegue encontrar o código? Verifique sua pasta «Spam».",
"Or": "OU",
"PasswordRecoveryTitle": "Recuperação de senha",
"RecoverAccess": "Recuperar o acesso",
"RecoverContactEmailPlaceholder": "E-mail de contato",
"RecoverDescribeYourProblemPlaceholder": "Descreva seu problema",
"RecoverTextBody": "Se você não conseguir entrar com sua conta existente ou quiser ser registrado como um novo usuário, entre em contato com o administrador do portal. ",
"RecoverTitle": "Recuperação de acesso",
"Register": "Registro",
"RegisterSendButton": "Enviar solicitação",
"RegisterTextBodyAfterDomainsList": "Para registrar-se, digite seu e-mail e clique em Enviar solicitação. Um link de ativação será enviado a você. ",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Não encontra o código? Verifique a pasta de «Spam».",
"Or": "OU",
"PasswordRecoveryTitle": "Recuperação de palavra-passe",
"RecoverAccess": "Recuperar o acesso",
"RecoverContactEmailPlaceholder": "Email de contacto",
"RecoverDescribeYourProblemPlaceholder": "Descreva o seu problema",
"RecoverTextBody": "Se não consegue iniciar sessão com a sua conta ou se quiser registar-se como um novo utilizador, contacte o administrador do portal.",
"RecoverTitle": "Recuperação de acesso",
"Register": "Registar",
"RegisterSendButton": "Enviar pedido",
"RegisterTextBodyAfterDomainsList": "Para registar-se, introduza o seu e-mail e clique em Enviar pedido. Ser-lhe-á enviado uma ligação de ativação.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Nu puteți găsi codul? Ar trebui să vă verificați folderul Spam.",
"Or": "SAU",
"PasswordRecoveryTitle": "Recuperarea parolei",
"RecoverAccess": "Recuperarea accesului la cont",
"RecoverContactEmailPlaceholder": "E-mail de contact",
"RecoverDescribeYourProblemPlaceholder": "Descrieți problema",
"RecoverTextBody": "Dacă nu puteți să vă conectați la contul dvs curent sau doriți să vă înregistrați din nou, contactați administratorul portalului. ",
"RecoverTitle": "Restabilirea accesului",
"Register": "Înregistrare",
"RegisterSendButton": "Trimite solicitarea",
"RegisterTextBodyAfterDomainsList": "Pentru a vă înregistra, introduceți adresa de e-mail și dați clic pe Trimite solicitarea. Veți primi o scrisoare de activare.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Не можете найти код? Проверьте папку «Спам».",
"Or": "ИЛИ",
"PasswordRecoveryTitle": "Восстановление пароля",
"RecoverAccess": "Доступ к порталу",
"RecoverContactEmailPlaceholder": "Адрес email, по которому можно связаться с Вами",
"RecoverDescribeYourProblemPlaceholder": "Пожалуйста, опишите вашу проблему",
"RecoverTextBody": "Если Вы уже зарегистрированы и у Вас есть проблемы с доступом к этому порталу, или Вы хотите зарегистрироваться как новый пользователь портала, пожалуйста, обратитесь к администратору портала, используя форму, расположенную ниже.",
"RecoverTitle": "Доступ к порталу",
"Register": "Регистрация",
"RegisterSendButton": "Отправить запрос",
"RegisterTextBodyAfterDomainsList": "Чтобы зарегистрироваться, введите свой email и нажмите кнопку Отправить запрос. Сообщение со ссылкой для активации вашей учётной записи будет отправлено на указанный адрес.",

View File

@ -13,6 +13,11 @@
"NotFoundCode": "Nemôžete nájsť kód? Skontrolujte priečinok «Spam».",
"Or": "Alebo",
"PasswordRecoveryTitle": "Obnova hesla",
"RecoverAccess": "Obnoviť prístup",
"RecoverContactEmailPlaceholder": "Kontaktný email",
"RecoverDescribeYourProblemPlaceholder": "Opíšte svoj problém",
"RecoverTextBody": "Ak sa nemôžete prihlásiť so svojím existujúcim kontom, alebo sa chcete zaregistrovať ako nový užívateľ, kontaktujte admina portálu.",
"RecoverTitle": "Obnova prístupu",
"Register": "Registrovať sa",
"RegisterSendButton": "Poslať žiadosť",
"RegisterTextBodyAfterDomainsList": "Ak sa chcete zaregistrovať, zadajte svoj e-mail a kliknite na tlačidlo Poslať žiadosť. Bude vám zaslané aktivačné prepojenie. ",

Some files were not shown because too many files have changed in this diff Show More