Merge pull request #63 from ONLYOFFICE/feature/new-login-page

Feature/new login page
This commit is contained in:
Ilya Oleshko 2020-07-31 14:49:18 +03:00 committed by GitHub
commit cfc40a6487
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2453 additions and 1536 deletions

View File

@ -41,6 +41,7 @@
<PackageReference Include="JWT" Version="6.1.4" />
<PackageReference Include="log4net" Version="2.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />

View File

@ -107,13 +107,15 @@ namespace ASC.Core.Common.Configuration
CoreBaseSettings coreBaseSettings,
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache) : this()
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory) : this()
{
TenantManager = tenantManager;
CoreBaseSettings = coreBaseSettings;
CoreSettings = coreSettings;
Configuration = configuration;
Cache = cache;
ConsumerFactory = consumerFactory;
OnlyDefault = configuration["core:default-consumers"] == "true";
Name = "";
Order = int.MaxValue;
@ -125,8 +127,9 @@ namespace ASC.Core.Common.Configuration
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> additional)
: this(tenantManager, coreBaseSettings, coreSettings, configuration, cache)
: this(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory)
{
Name = name;
Order = order;
@ -140,8 +143,9 @@ namespace ASC.Core.Common.Configuration
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional)
: this(tenantManager, coreBaseSettings, coreSettings, configuration, cache)
: this(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory)
{
Name = name;
Order = order;
@ -300,8 +304,9 @@ namespace ASC.Core.Common.Configuration
CoreBaseSettings coreBaseSettings,
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache)
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory)
{
}
@ -312,8 +317,9 @@ namespace ASC.Core.Common.Configuration
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, additional)
{
Init(additional);
}
@ -324,8 +330,9 @@ namespace ASC.Core.Common.Configuration
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
Init(additional);
}
@ -361,12 +368,12 @@ namespace ASC.Core.Common.Configuration
var additional = fromConfig.AdditionalKeys.ToDictionary(prop => prop, prop => fromConfig[prop]);
additional.Add(HandlerTypeKey, HandlerType.AssemblyQualifiedName);
return new DataStoreConsumer(fromConfig.TenantManager, fromConfig.CoreBaseSettings, fromConfig.CoreSettings, fromConfig.Configuration, fromConfig.Cache, fromConfig.Name, fromConfig.Order, props, additional);
return new DataStoreConsumer(fromConfig.TenantManager, fromConfig.CoreBaseSettings, fromConfig.CoreSettings, fromConfig.Configuration, fromConfig.Cache, fromConfig.ConsumerFactory, fromConfig.Name, fromConfig.Order, props, additional);
}
public object Clone()
{
return new DataStoreConsumer(TenantManager, CoreBaseSettings, CoreSettings, Configuration, Cache, Name, Order, Props.ToDictionary(r => r.Key, r => r.Value), Additional.ToDictionary(r => r.Key, r => r.Value));
return new DataStoreConsumer(TenantManager, CoreBaseSettings, CoreSettings, Configuration, Cache, ConsumerFactory, Name, Order, Props.ToDictionary(r => r.Key, r => r.Value), Additional.ToDictionary(r => r.Key, r => r.Value));
}
}

View File

@ -41,6 +41,8 @@ namespace ASC.Core
public string[] Methods { get; set; }
public string MethodsFromDb { set { Methods = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } }
public static implicit operator SubscriptionMethod(SubscriptionMethodCache cache)
{

View File

@ -47,8 +47,8 @@ namespace ASC.Core.Data
{
ActionId = r.Action,
ObjectId = r.Object,
RecipientId = r.Recipient.ToString(),
SourceId = r.Source.ToString(),
RecipientId = r.Recipient,
SourceId = r.Source,
Subscribed = !r.Unsubscribed,
Tenant = r.Tenant
};
@ -56,10 +56,10 @@ namespace ASC.Core.Data
FromDbSubscriptionMethodToSubscriptionMethod = r => new SubscriptionMethod
{
ActionId = r.Action,
RecipientId = r.Recipient.ToString(),
SourceId = r.Source.ToString(),
RecipientId = r.Recipient,
SourceId = r.Source,
Tenant = r.Tenant,
Methods = r.Sender.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
MethodsFromDb = r.Sender
};
}
@ -164,8 +164,9 @@ namespace ASC.Core.Data
q = q.Where(r => r.Recipient == recipientId);
}
var a = q.OrderBy(r => r.Tenant)
.GroupBy(r => new { r.Recipient, r.Action }, (x, y) => y.First());
var a = q
.OrderBy(r => r.Tenant)
.Distinct();
var methods = a.Select(FromDbSubscriptionMethodToSubscriptionMethod).ToList();

View File

@ -26,25 +26,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using ASC.Common;
using ASC.Common.Utils;
using ASC.Core.Common.Configuration;
using ASC.FederatedLogin.Helpers;
using ASC.FederatedLogin.LoginProviders;
using ASC.FederatedLogin.Profile;
using ASC.Security.Cryptography;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
namespace ASC.FederatedLogin
{
@ -52,33 +52,30 @@ namespace ASC.FederatedLogin
{
private Dictionary<string, string> _params;
private readonly RequestDelegate _next;
private IWebHostEnvironment WebHostEnvironment { get; }
private IMemoryCache MemoryCache { get; }
public Signature Signature { get; }
public InstanceCrypto InstanceCrypto { get; }
public ConsumerFactory ConsumerFactory { get; }
public ProviderManager ProviderManager { get; }
public Login(
RequestDelegate next,
IWebHostEnvironment webHostEnvironment,
IMemoryCache memoryCache,
Signature signature,
InstanceCrypto instanceCrypto,
ConsumerFactory consumerFactory)
ProviderManager providerManager)
{
_next = next;
WebHostEnvironment = webHostEnvironment;
MemoryCache = memoryCache;
Signature = signature;
InstanceCrypto = instanceCrypto;
ConsumerFactory = consumerFactory;
ProviderManager = providerManager;
}
public async Task Invoke(HttpContext context)
{
context.PushRewritenUri();
var scope = context.PushRewritenUri();
if (string.IsNullOrEmpty(context.Request.Query["p"]))
{
@ -91,21 +88,22 @@ namespace ASC.FederatedLogin
//Pack and redirect
var uriBuilder = new UriBuilder(context.Request.GetUrlRewriter());
var token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_params)));
var token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(_params)));
uriBuilder.Query = "p=" + token;
context.Response.Redirect(uriBuilder.Uri.ToString(), true);
await context.Response.CompleteAsync();
return;
}
else
{
_params = ((Dictionary<string, object>)JsonConvert.DeserializeObject(
Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(context.Request.Query["p"])))).ToDictionary(x => x.Key, y => (string)y.Value);
_params = JsonSerializer.Deserialize<Dictionary<string, string>>(Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(context.Request.Query["p"])));
}
if (!string.IsNullOrEmpty(Auth))
{
try
{
var profile = ProviderManager.Process(Auth, Signature, InstanceCrypto, ConsumerFactory, context, _params);
var profile = ProviderManager.Process(Auth, context, _params);
if (profile != null)
{
await SendClientData(context, profile);
@ -185,7 +183,7 @@ namespace ASC.FederatedLogin
switch (Mode)
{
case LoginMode.Redirect:
RedirectToReturnUrl(context, profile);
await RedirectToReturnUrl(context, profile);
break;
case LoginMode.Popup:
await SendJsCallback(context, profile);
@ -200,7 +198,7 @@ namespace ASC.FederatedLogin
await context.Response.WriteAsync(JsCallbackHelper.GetCallbackPage().Replace("%PROFILE%", profile.ToJson()).Replace("%CALLBACK%", Callback));
}
private void RedirectToReturnUrl(HttpContext context, LoginProfile profile)
private async Task RedirectToReturnUrl(HttpContext context, LoginProfile profile)
{
var useMinimalProfile = Minimal;
if (useMinimalProfile)
@ -219,6 +217,49 @@ namespace ASC.FederatedLogin
{
context.Response.Redirect(new Uri(ReturnUrl, UriKind.Absolute).AddProfile(profile).ToString(), true);
}
await context.Response.CompleteAsync();
return;
}
}
public class LoginHandler
{
public RequestDelegate Next { get; }
public IServiceProvider ServiceProvider { get; }
public LoginHandler(RequestDelegate next, IServiceProvider serviceProvider)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
using var scope = ServiceProvider.CreateScope();
var login = scope.ServiceProvider.GetService<Login>();
await login.Invoke(context);
}
}
public static class LoginHandlerExtensions
{
public static DIHelper AddLoginHandlerService(this DIHelper services)
{
if (services.TryAddScoped<Login>())
{
return services
.AddSignatureService()
.AddInstanceCryptoService()
.AddProviderManagerService();
}
return services;
}
public static IApplicationBuilder UseLoginHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoginHandler>();
}
}
}

View File

@ -82,10 +82,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
Signature = signature;
InstanceCrypto = instanceCrypto;
@ -95,7 +96,12 @@ namespace ASC.FederatedLogin.LoginProviders
{
try
{
var token = Auth(context, Scopes);
var token = Auth(context, Scopes, out var redirect);
if (redirect)
{
return null;
}
return GetLoginProfile(token?.AccessToken);
}
@ -109,7 +115,7 @@ namespace ASC.FederatedLogin.LoginProviders
}
}
protected virtual OAuth20Token Auth(HttpContext context, string scopes, Dictionary<string, string> additionalArgs = null)
protected virtual OAuth20Token Auth(HttpContext context, string scopes, out bool redirect, Dictionary<string, string> additionalArgs = null)
{
var error = context.Request.Query["error"];
if (!string.IsNullOrEmpty(error))
@ -125,9 +131,11 @@ namespace ASC.FederatedLogin.LoginProviders
if (string.IsNullOrEmpty(code))
{
OAuth20TokenHelper.RequestCode<T>(context, ConsumerFactory, scopes, additionalArgs);
redirect = true;
return null;
}
redirect = false;
return OAuth20TokenHelper.GetAccessToken<T>(ConsumerFactory, code);
}

View File

@ -66,8 +66,9 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}

View File

@ -62,8 +62,9 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}
}

View File

@ -66,8 +66,9 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}

View File

@ -62,8 +62,9 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}
}

View File

@ -59,10 +59,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional) { }
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional) { }
public override LoginProfile GetLoginProfile(string accessToken)
{

View File

@ -74,10 +74,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional) { }
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional) { }
public override LoginProfile GetLoginProfile(string accessToken)
{
@ -89,7 +90,7 @@ namespace ASC.FederatedLogin.LoginProviders
public OAuth20Token Auth(HttpContext context)
{
return Auth(context, GoogleScopeContacts, (context.Request.Query["access_type"].ToString() ?? "") == "offline"
return Auth(context, GoogleScopeContacts, out var _, (context.Request.Query["access_type"].ToString() ?? "") == "offline"
? new Dictionary<string, string>
{
{ "access_type", "offline" },

View File

@ -102,10 +102,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional)
{
}
@ -114,7 +115,12 @@ namespace ASC.FederatedLogin.LoginProviders
{
try
{
var token = Auth(context, Scopes);
var token = Auth(context, Scopes, out var redirect);
if (redirect)
{
return null;
}
if (token == null)
{
@ -133,7 +139,7 @@ namespace ASC.FederatedLogin.LoginProviders
}
}
protected override OAuth20Token Auth(HttpContext context, string scopes, Dictionary<string, string> additionalArgs = null)
protected override OAuth20Token Auth(HttpContext context, string scopes, out bool redirect, Dictionary<string, string> additionalArgs = null)
{
var error = context.Request.Query["error"];
if (!string.IsNullOrEmpty(error))
@ -149,8 +155,11 @@ namespace ASC.FederatedLogin.LoginProviders
if (string.IsNullOrEmpty(code))
{
RequestCode(context, scopes);
redirect = true;
return null;
}
redirect = false;
var state = context.Request.Query["state"];
return GetAccessToken(state, code);
}

View File

@ -82,10 +82,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional) { }
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional) { }
public override LoginProfile GetLoginProfile(string accessToken)
{

View File

@ -85,10 +85,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional)
{
}
@ -96,7 +97,12 @@ namespace ASC.FederatedLogin.LoginProviders
{
try
{
var token = Auth(context, Scopes);
var token = Auth(context, Scopes, out var redirect);
if (redirect)
{
return null;
}
if (token == null)
{

View File

@ -65,8 +65,9 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}
}

View File

@ -26,7 +26,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ASC.Common;
using ASC.Common.Utils;
using ASC.Core.Common.Configuration;
using ASC.FederatedLogin.Profile;
@ -38,21 +40,44 @@ namespace ASC.FederatedLogin.LoginProviders
{
public class ProviderManager
{
public static ILoginProvider GetLoginProvider(string providerType, Signature signature, InstanceCrypto instanceCrypto, ConsumerFactory consumerFactory)
public static List<string> AuthProviders = new List<string>
{
ProviderConstants.Google,
ProviderConstants.Facebook,
ProviderConstants.Twitter,
ProviderConstants.LinkedIn,
ProviderConstants.MailRu,
ProviderConstants.VK,
ProviderConstants.Yandex,
ProviderConstants.GosUslugi
};
public Signature Signature { get; }
public InstanceCrypto InstanceCrypto { get; }
public ConsumerFactory ConsumerFactory { get; }
public ProviderManager(Signature signature, InstanceCrypto instanceCrypto, ConsumerFactory consumerFactory)
{
Signature = signature;
InstanceCrypto = instanceCrypto;
ConsumerFactory = consumerFactory;
}
public ILoginProvider GetLoginProvider(string providerType)
{
return providerType == ProviderConstants.OpenId
? new OpenIdLoginProvider(signature, instanceCrypto, consumerFactory)
: consumerFactory.GetByKey(providerType) as ILoginProvider;
? new OpenIdLoginProvider(Signature, InstanceCrypto, ConsumerFactory)
: ConsumerFactory.GetByKey(providerType) as ILoginProvider;
}
public static LoginProfile Process(string providerType, Signature signature, InstanceCrypto instanceCrypto, ConsumerFactory consumerFactory, HttpContext context, IDictionary<string, string> @params)
public LoginProfile Process(string providerType, HttpContext context, IDictionary<string, string> @params)
{
return GetLoginProvider(providerType, signature, instanceCrypto, consumerFactory).ProcessAuthoriztion(context, @params);
return GetLoginProvider(providerType).ProcessAuthoriztion(context, @params);
}
public static LoginProfile GetLoginProfile(string providerType, string accessToken, Signature signature, InstanceCrypto instanceCrypto, ConsumerFactory consumerFactory)
public LoginProfile GetLoginProfile(string providerType, string accessToken)
{
var consumer = GetLoginProvider(providerType, signature, instanceCrypto, consumerFactory);
var consumer = GetLoginProvider(providerType);
if (consumer == null) throw new ArgumentException("Unknown provider type", "providerType");
try
@ -61,8 +86,34 @@ namespace ASC.FederatedLogin.LoginProviders
}
catch (Exception ex)
{
return LoginProfile.FromError(signature, instanceCrypto, ex);
return LoginProfile.FromError(Signature, InstanceCrypto, ex);
}
}
public bool IsNotEmpty
{
get
{
return AuthProviders
.Select(GetLoginProvider)
.Any(loginProvider => loginProvider != null && loginProvider.IsEnabled);
}
}
}
public static class ProviderManagerServiceExtension
{
public static DIHelper AddProviderManagerService(this DIHelper services)
{
if (services.TryAddScoped<ProviderManager>())
{
return services
.AddSignatureService()
.AddInstanceCryptoService()
.AddConsumerFactoryService();
}
return services;
}
}
}

View File

@ -89,10 +89,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional)
{
}
@ -101,13 +102,18 @@ namespace ASC.FederatedLogin.LoginProviders
{
try
{
var token = Auth(context, Scopes, context.Request.Query["access_type"] == "offline"
var token = Auth(context, Scopes, out var redirect, context.Request.Query["access_type"] == "offline"
? new Dictionary<string, string>
{
{ "revoke", "1" }
}
: null);
if (redirect)
{
return null;
}
if (token == null)
{
throw new Exception("Login failed");

View File

@ -54,10 +54,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional)
{
}

View File

@ -57,14 +57,15 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional) { }
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional) { }
public OAuth20Token Auth(HttpContext context)
{
return Auth(context, Scopes);
return Auth(context, Scopes, out var _);
}
public override LoginProfile GetLoginProfile(string accessToken)

View File

@ -82,10 +82,11 @@ namespace ASC.FederatedLogin.LoginProviders
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
Signature signature,
InstanceCrypto instanceCrypto,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, signature, instanceCrypto, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, signature, instanceCrypto, name, order, props, additional)
{
}
@ -93,12 +94,16 @@ namespace ASC.FederatedLogin.LoginProviders
{
try
{
var token = Auth(context, Scopes, context.Request.Query["access_type"] == "offline"
var token = Auth(context, Scopes, out var redirect, context.Request.Query["access_type"] == "offline"
? new Dictionary<string, string>
{
{ "force_confirm", "true" }
}
: null);
if (redirect)
{
return null;
}
return GetLoginProfile(token?.AccessToken);
}

View File

@ -41,11 +41,11 @@
"type": "ASC.FederatedLogin.LoginProviders.BoxLoginProvider, ASC.FederatedLogin"
},
{
"key": "Box",
"key": "box",
"type": "ASC.Core.Common.Configuration.Consumer, ASC.Core.Common"
},
{
"key": "Box",
"key": "box",
"type": "ASC.FederatedLogin.LoginProviders.BoxLoginProvider, ASC.FederatedLogin"
}
],
@ -191,11 +191,11 @@
"type": "ASC.FederatedLogin.LoginProviders.FacebookLoginProvider, ASC.FederatedLogin"
},
{
"key": "Facebook",
"key": "facebook",
"type": "ASC.Core.Common.Configuration.Consumer, ASC.Core.Common"
},
{
"key": "Facebook",
"key": "facebook",
"type": "ASC.FederatedLogin.LoginProviders.FacebookLoginProvider, ASC.FederatedLogin"
}
],
@ -252,11 +252,11 @@
"type": "ASC.FederatedLogin.LoginProviders.GoogleLoginProvider, ASC.FederatedLogin"
},
{
"key": "Google",
"key": "google",
"type": "ASC.Core.Common.Configuration.Consumer, ASC.Core.Common"
},
{
"key": "Google",
"key": "google",
"type": "ASC.FederatedLogin.LoginProviders.GoogleLoginProvider, ASC.FederatedLogin"
}
],
@ -283,11 +283,11 @@
"type": "ASC.FederatedLogin.LoginProviders.LinkedInLoginProvider, ASC.FederatedLogin"
},
{
"key": "LinkedIn",
"key": "linkedin",
"type": "ASC.Core.Common.Configuration.Consumer, ASC.Core.Common"
},
{
"key": "LinkedIn",
"key": "linkedin",
"type": "ASC.FederatedLogin.LoginProviders.LinkedInLoginProvider, ASC.FederatedLogin"
}
],
@ -406,11 +406,11 @@
"type": "ASC.FederatedLogin.LoginProviders.TwitterLoginProvider, ASC.FederatedLogin"
},
{
"key": "Twitter",
"key": "twitter",
"type": "ASC.Core.Common.Configuration.Consumer, ASC.Core.Common"
},
{
"key": "Twitter",
"key": "twitter",
"type": "ASC.FederatedLogin.LoginProviders.TwitterLoginProvider, ASC.FederatedLogin"
}
],

View File

@ -95,6 +95,11 @@ server {
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /login.ashx {
proxy_pass http://localhost:5003;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /products {
location ~* /people {
#rewrite products/people/(.*) /$1 break;

View File

@ -74,11 +74,12 @@ namespace ASC.Web.Files.Helpers
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory factory,
string name,
int order,
Dictionary<string, string> props,
Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, factory, name, order, props, additional)
{
Log = option.CurrentValue;
}

View File

@ -144,8 +144,9 @@ namespace ASC.Web.Files.ThirdPartyApp
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, additional)
{
PathProvider = pathProvider;
TenantUtil = tenantUtil;

View File

@ -154,8 +154,9 @@ namespace ASC.Web.Files.ThirdPartyApp
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, additional)
{
Logger = option.CurrentValue;
PathProvider = pathProvider;

View File

@ -30,7 +30,9 @@ using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.ServiceModel.Security;
using System.Text.RegularExpressions;
using System.Web;
using ASC.Api.Collections;
@ -39,6 +41,7 @@ using ASC.Api.Utils;
using ASC.Common;
using ASC.Common.Logging;
using ASC.Common.Utils;
using ASC.Common.Web;
using ASC.Core;
using ASC.Core.Billing;
using ASC.Core.Common.Configuration;
@ -48,12 +51,16 @@ using ASC.Core.Users;
using ASC.Data.Storage;
using ASC.Data.Storage.Configuration;
using ASC.Data.Storage.Migration;
using ASC.FederatedLogin;
using ASC.FederatedLogin.LoginProviders;
using ASC.FederatedLogin.Profile;
using ASC.IPSecurity;
using ASC.MessagingSystem;
using ASC.Security.Cryptography;
using ASC.Web.Api.Models;
using ASC.Web.Api.Routing;
using ASC.Web.Core;
using ASC.Web.Core.Mobile;
using ASC.Web.Core.PublicResources;
using ASC.Web.Core.Sms;
using ASC.Web.Core.Users;
@ -74,6 +81,7 @@ using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
@ -99,6 +107,11 @@ namespace ASC.Api.Settings
public SmsProviderManager SmsProviderManager { get; }
public TimeZoneConverter TimeZoneConverter { get; }
public CustomNamingPeople CustomNamingPeople { get; }
public IPSecurity.IPSecurity IpSecurity { get; }
public IMemoryCache MemoryCache { get; }
public ProviderManager ProviderManager { get; }
public MobileDetector MobileDetector { get; }
public IOptionsSnapshot<AccountLinker> AccountLinker { get; }
public UserManager UserManager { get; }
public TenantManager TenantManager { get; }
public TenantExtra TenantExtra { get; }
@ -178,7 +191,12 @@ namespace ASC.Api.Settings
ConsumerFactory consumerFactory,
SmsProviderManager smsProviderManager,
TimeZoneConverter timeZoneConverter,
CustomNamingPeople customNamingPeople)
CustomNamingPeople customNamingPeople,
IPSecurity.IPSecurity ipSecurity,
IMemoryCache memoryCache,
ProviderManager providerManager,
MobileDetector mobileDetector,
IOptionsSnapshot<AccountLinker> accountLinker)
{
Log = option.Get("ASC.Api");
WebHostEnvironment = webHostEnvironment;
@ -188,6 +206,11 @@ namespace ASC.Api.Settings
SmsProviderManager = smsProviderManager;
TimeZoneConverter = timeZoneConverter;
CustomNamingPeople = customNamingPeople;
IpSecurity = ipSecurity;
MemoryCache = memoryCache;
ProviderManager = providerManager;
MobileDetector = mobileDetector;
AccountLinker = accountLinker;
MessageService = messageService;
StudioNotifyService = studioNotifyService;
ApiContext = apiContext;
@ -247,10 +270,158 @@ namespace ASC.Api.Settings
settings.OwnerId = Tenant.OwnerId;
settings.NameSchemaId = CustomNamingPeople.Current.Id;
}
else
{
settings.EnabledJoin =
(Tenant.TrustedDomainsType == TenantTrustedDomainsType.Custom &&
Tenant.TrustedDomains.Count > 0) ||
Tenant.TrustedDomainsType == TenantTrustedDomainsType.All;
var studioAdminMessageSettings = SettingsManager.Load<StudioAdminMessageSettings>();
settings.EnableAdmMess = studioAdminMessageSettings.Enable || TenantExtra.IsNotPaid();
settings.ThirdpartyEnable = SetupInfo.ThirdPartyAuthEnabled && ProviderManager.IsNotEmpty;
}
return settings;
}
[Create("messagesettings")]
public ContentResult EnableAdminMessageSettings(AdminMessageSettingsModel model)
{
PermissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
SettingsManager.Save(new StudioAdminMessageSettings { Enable = model.TurnOn });
MessageService.Send(MessageAction.AdministratorMessageSettingsUpdated);
return new ContentResult() { Content = Resource.SuccessfullySaveSettingsMessage };
}
[AllowAnonymous]
[Create("sendadmmail")]
public ContentResult SendAdmMail(AdminMessageSettingsModel model)
{
var studioAdminMessageSettings = SettingsManager.Load<StudioAdminMessageSettings>();
var enableAdmMess = studioAdminMessageSettings.Enable || TenantExtra.IsNotPaid();
if (!enableAdmMess)
throw new MethodAccessException("Method not available");
if (!model.Email.TestEmailRegex())
throw new Exception(Resource.ErrorNotCorrectEmail);
if (string.IsNullOrEmpty(model.Message))
throw new Exception(Resource.ErrorEmptyMessage);
CheckCache("sendadmmail");
StudioNotifyService.SendMsgToAdminFromNotAuthUser(model.Email, model.Message);
MessageService.Send(MessageAction.ContactAdminMailSent);
return new ContentResult() { Content = Resource.AdminMessageSent };
}
[Create("maildomainsettings")]
public ContentResult SaveMailDomainSettings(MailDomainSettingsModel model)
{
PermissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
if (model.Type == TenantTrustedDomainsType.Custom)
{
Tenant.TrustedDomains.Clear();
foreach (var d in model.Domains.Select(domain => (domain ?? "").Trim().ToLower()))
{
if (!(!string.IsNullOrEmpty(d) && new Regex("^[a-z0-9]([a-z0-9-.]){1,98}[a-z0-9]$").IsMatch(d)))
return new ContentResult() { Content = Resource.ErrorNotCorrectTrustedDomain };
Tenant.TrustedDomains.Add(d);
}
if (Tenant.TrustedDomains.Count == 0)
model.Type = TenantTrustedDomainsType.None;
}
Tenant.TrustedDomainsType = model.Type;
SettingsManager.Save(new StudioTrustedDomainSettings { InviteUsersAsVisitors = model.InviteUsersAsVisitors });
TenantManager.SaveTenant(Tenant);
MessageService.Send(MessageAction.TrustedMailDomainSettingsUpdated);
return new ContentResult { Content = Resource.SuccessfullySaveSettingsMessage };
}
[AllowAnonymous]
[Create("sendjoininvite")]
public ContentResult SendJoinInviteMail(AdminMessageSettingsModel model)
{
try
{
var email = model.Email;
if (!(
(Tenant.TrustedDomainsType == TenantTrustedDomainsType.Custom &&
Tenant.TrustedDomains.Count > 0) ||
Tenant.TrustedDomainsType == TenantTrustedDomainsType.All))
throw new MethodAccessException("Method not available");
if (!email.TestEmailRegex())
throw new Exception(Resource.ErrorNotCorrectEmail);
CheckCache("sendjoininvite");
var user = UserManager.GetUserByEmail(email);
if (!user.ID.Equals(Constants.LostUser.ID))
throw new Exception(CustomNamingPeople.Substitute<Resource>("ErrorEmailAlreadyExists"));
var settings = SettingsManager.Load<IPRestrictionsSettings>();
if (settings.Enable && !IpSecurity.Verify())
throw new Exception(Resource.ErrorAccessRestricted);
var trustedDomainSettings = SettingsManager.Load<StudioTrustedDomainSettings>();
var emplType = trustedDomainSettings.InviteUsersAsVisitors ? EmployeeType.Visitor : EmployeeType.User;
var enableInviteUsers = TenantStatisticsProvider.GetUsersCount() < TenantExtra.GetTenantQuota().ActiveUsers;
if (!enableInviteUsers)
emplType = EmployeeType.Visitor;
switch (Tenant.TrustedDomainsType)
{
case TenantTrustedDomainsType.Custom:
{
var address = new MailAddress(email);
if (Tenant.TrustedDomains.Any(d => address.Address.EndsWith("@" + d, StringComparison.InvariantCultureIgnoreCase)))
{
StudioNotifyService.SendJoinMsg(email, emplType);
MessageService.Send(MessageInitiator.System, MessageAction.SentInviteInstructions, email);
return new ContentResult() { Content = Resource.FinishInviteJoinEmailMessage };
}
throw new Exception(Resource.ErrorEmailDomainNotAllowed);
}
case TenantTrustedDomainsType.All:
{
StudioNotifyService.SendJoinMsg(email, emplType);
MessageService.Send(MessageInitiator.System, MessageAction.SentInviteInstructions, email);
return new ContentResult() { Content = Resource.FinishInviteJoinEmailMessage };
}
default:
throw new Exception(Resource.ErrorNotCorrectEmail);
}
}
catch (FormatException)
{
return new ContentResult() { Content = Resource.ErrorNotCorrectEmail };
}
catch (Exception e)
{
return new ContentResult() { Content = e.Message.HtmlEncode() };
}
}
[Read("customschemas")]
public List<SchemaModel> PeopleSchemas()
{
@ -309,6 +480,50 @@ namespace ASC.Api.Settings
return new QuotaWrapper(Tenant, CoreBaseSettings, CoreConfiguration, TenantExtra, TenantStatisticsProvider, AuthContext, SettingsManager, WebItemManager);
}
[AllowAnonymous]
[Read("authproviders")]
public ICollection<AccountInfo> GetAuthProviders(bool inviteView, bool settingsView, string clientCallback, string fromOnly)
{
ICollection<AccountInfo> infos = new List<AccountInfo>();
IEnumerable<LoginProfile> linkedAccounts = new List<LoginProfile>();
if (AuthContext.IsAuthenticated)
{
linkedAccounts = AccountLinker.Get("webstudio").GetLinkedProfiles(AuthContext.CurrentAccount.ID.ToString());
}
fromOnly = string.IsNullOrWhiteSpace(fromOnly) ? string.Empty : fromOnly.ToLower();
foreach (var provider in ProviderManager.AuthProviders.Where(provider => string.IsNullOrEmpty(fromOnly) || fromOnly == provider || (provider == "google" && fromOnly == "openid")))
{
if (inviteView && provider.ToLower() == "twitter") continue;
var loginProvider = ProviderManager.GetLoginProvider(provider);
if (loginProvider != null && loginProvider.IsEnabled)
{
var url = VirtualPathUtility.ToAbsolute("~/login.ashx") + $"?auth={provider}";
var mode = (settingsView || inviteView || (!MobileDetector.IsMobile() && !Request.DesktopApp())
? ("&mode=popup&callback=" + clientCallback)
: ("&mode=Redirect&returnurl="
+ HttpUtility.UrlEncode(new Uri(Request.GetUrlRewriter(),
"Auth.aspx"
+ (Request.DesktopApp() ? "?desktop=true" : "")
).ToString())));
infos.Add(new AccountInfo
{
Linked = linkedAccounts.Any(x => x.Provider == provider),
Provider = provider,
Url = url + mode
});
}
}
return infos;
}
[AllowAnonymous]
[Read("cultures")]
public IEnumerable<object> GetSupportedCultures()
@ -1488,6 +1703,21 @@ namespace ASC.Api.Settings
return new { Url = hubUrl };
}
private readonly int maxCount = 10;
private readonly int expirationMinutes = 2;
private void CheckCache(string basekey)
{
var key = ApiContext.HttpContextAccessor.HttpContext.Request.GetUserHostAddress() + basekey;
if (MemoryCache.TryGetValue<int>(key, out var count))
{
if (count > maxCount)
throw new Exception(Resource.ErrorRequestLimitExceeded);
}
MemoryCache.Set(key, ++count, TimeSpan.FromMinutes(expirationMinutes));
}
}
public static class SettingsControllerExtension
@ -1537,7 +1767,10 @@ namespace ASC.Api.Settings
.AddEmployeeWraper()
.AddConsumerFactoryService()
.AddSmsProviderManagerService()
.AddCustomNamingPeopleService();
.AddCustomNamingPeopleService()
.AddProviderManagerService()
.AddAccountLinker()
.AddMobileDetectorService();
}
}
}

View File

@ -0,0 +1,10 @@
namespace ASC.Web.Api.Models
{
public class AccountInfo
{
public string Provider { get; set; }
public string Url { get; set; }
public bool Linked { get; set; }
public string Class { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using ASC.Core.Tenants;
namespace ASC.Web.Api.Models
{
public class MailDomainSettingsModel
{
public TenantTrustedDomainsType Type { get; set; }
public List<string> Domains { get; set; }
public bool InviteUsersAsVisitors { get; set; }
}
public class AdminMessageSettingsModel
{
public string Email { get; set; }
public string Message { get; set; }
public bool TurnOn { get; set; }
}
}

View File

@ -51,6 +51,12 @@ namespace ASC.Api.Settings
public string NameSchemaId { get; set; }
public bool? EnabledJoin { get; set; }
public bool? EnableAdmMess { get; set; }
public bool? ThirdpartyEnable { get; set; }
public static SettingsWrapper GetSample()
{
return new SettingsWrapper

View File

@ -25,6 +25,8 @@ namespace ASC.Web.Api
public override void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
var diHelper = new DIHelper(services);
diHelper

View File

@ -91,3 +91,12 @@ export function getSettings() {
data
});
}
export function sendRegisterRequest(email) {
const data = { email };
return request({
method: "post",
url: `/settings/sendjoininvite`,
data
});
}

View File

@ -11,6 +11,7 @@ import HeaderUnauth from "./sub-components/header-unauth";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import { connect } from "react-redux";
class Layout extends React.Component {
constructor(props) {
@ -286,4 +287,11 @@ LayoutWrapper.propTypes = {
language: PropTypes.string.isRequired
};
export default LayoutWrapper;
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export default connect(mapStateToProps, null)(LayoutWrapper);

View File

@ -19,13 +19,13 @@ const Header = styled.header`
padding: 0 32px;
.header-items-wrapper {
width: 896px;
width: 960px;
@media (max-width: 768px) {
width: 411px;
width: 475px;
}
@media (max-width: 375px) {
width: 247px;
width: 311px;
}
}

View File

@ -16,26 +16,21 @@ const Header = styled.header`
position: absolute;
width: 100vw;
height: 56px;
.header-logo-wrapper {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.header-module-title {
display: block;
font-size: 21px;
line-height: 0;
@media ${desktop} {
display: none;
}
}
.header-logo-min_icon {
display: none;
cursor: pointer;
width: 24px;
height: 24px;
@media (max-width: 620px) {
@ -43,14 +38,12 @@ const Header = styled.header`
display: ${props => props.module && "block"};
}
}
.header-logo-icon {
width: 146px;
height: 24px;
position: relative;
padding: 4px 20px 0 6px;
cursor: pointer;
@media (max-width: 620px) {
display: ${props => (props.module ? "none" : "block")};
padding: 0px 20px 0 6px;

View File

@ -59,16 +59,14 @@ const RecoverAccess = ({ t }) => {
else {
setLoading(true);
sendRecoverRequest(email, description)
.then(
res => {
.then(() => {
setLoading(false);
toastr.success(res);
},
message => {
toastr.success("Successfully sent")
})
.catch((error) => {
setLoading(false);
toastr.error(message);
}
)
toastr.error(error)
})
.finally(onRecoverModalClose)
}
};

View File

@ -2,48 +2,70 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
Box,
Button,
TextInput,
Text,
Heading,
Link,
toastr,
Checkbox,
HelpButton
HelpButton,
PasswordInput,
FieldContainer
} from "asc-web-components";
import PageLayout from "../../components/PageLayout";
import { connect } from "react-redux";
import styled from "styled-components";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import SubModalDialog from "./sub-components/modal-dialog";
import ForgotPasswordModalDialog from "./sub-components/forgot-password-modal-dialog";
import { login, setIsLoaded } from "../../store/auth/actions";
import { sendInstructionsToChangePassword } from "../../api/people";
import Register from "./sub-components/register-container";
const FormContainer = styled.form`
margin: 50px auto 0 auto;
max-width: 432px;
const LoginContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 120px auto 0 auto;
max-width: 960px;
.login-header {
min-height: 79px;
margin-bottom: 24px;
.login-logo {
max-width: 216px;
max-height: 35px;
@media (max-width: 768px) {
padding: 0 16px;
max-width: 475px;
}
@media (max-width: 375px) {
margin: 72px auto 0 auto;
max-width: 311px;
}
.login-title {
margin: 8px 0;
.greeting-title {
width: 100%;
@media (max-width: 768px) {
text-align: left;
}
@media (max-width: 375px) {
font-size: 23px;
}
}
.login-input {
margin-bottom: 24px;
.auth-form-container {
margin: 32px 22.5% 0 22.5%;
width: 32.4%;
@media (max-width: 768px) {
margin: 32px 0 0 0;
width: 100%
}
@media (max-width: 375px) {
margin: 32px 0 0 0;
width: 100%
}
.login-forgot-wrapper {
height: 36px;
padding: 14px 0;
.login-checkbox-wrapper {
position: absolute;
@ -55,6 +77,7 @@ const FormContainer = styled.form`
font-size: 12px;
}
}
.login-tooltip {
display: inline-flex;
@ -67,6 +90,7 @@ const FormContainer = styled.form`
}
}
}
.login-link {
float: right;
line-height: 16px;
@ -80,6 +104,17 @@ const FormContainer = styled.form`
.login-button-dialog {
margin-right: 8px;
}
.login-bottom-border {
width: 100%;
height: 1px;
background: #ECEEF1;
}
.login-bottom-text {
margin: 0 8px;
}
}
`;
class Form extends Component {
@ -96,7 +131,9 @@ class Form extends Component {
isChecked: false,
openDialog: false,
email: "",
errorText: ""
emailError: false,
errorText: "",
socialButtons: []
};
}
@ -113,7 +150,7 @@ class Form extends Component {
};
onChangeEmail = event => {
this.setState({ email: event.target.value });
this.setState({ email: event.target.value, emailError: false });
};
onChangeCheckbox = () => this.setState({ isChecked: !this.state.isChecked });
@ -135,6 +172,10 @@ class Form extends Component {
};
onSendPasswordInstructions = () => {
if (!this.state.email.trim()) {
this.setState({ emailError: true });
}
else {
this.setState({ isLoading: true });
sendInstructionsToChangePassword(this.state.email)
.then(
@ -142,6 +183,7 @@ class Form extends Component {
message => toastr.error(message)
)
.finally(this.onDialogClose());
}
};
onDialogClose = () => {
@ -149,7 +191,8 @@ class Form extends Component {
openDialog: false,
isDisabled: false,
isLoading: false,
email: ""
email: "",
emailError: false
});
};
@ -203,6 +246,13 @@ class Form extends Component {
window.removeEventListener("keyup", this.onKeyPress);
}
settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
}
render() {
const { greetingTitle, match, t } = this.props;
@ -215,32 +265,40 @@ class Form extends Component {
isChecked,
openDialog,
email,
errorText
emailError,
errorText,
socialButtons
} = this.state;
const { params } = match;
//console.log("Login render");
return (
<FormContainer>
<div className="login-header">
<img
className="login-logo"
src="images/dark_general.png"
alt="Logo"
/>
<Heading className="login-title" color="#116d9d">
{greetingTitle}
</Heading>
</div>
<>
<LoginContainer>
<Text fontSize="32px"
fontWeight={600}
textAlign="center"
className="greeting-title">
{greetingTitle}
</Text>
<form className="auth-form-container">
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={!identifierValid}
errorMessage={t("RequiredFieldMessage")}
>
<TextInput
id="login"
name="login"
hasError={!identifierValid}
value={identifier}
placeholder={t("RegistrationEmailWatermark")}
size="huge"
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
@ -248,33 +306,39 @@ class Form extends Component {
autoComplete="username"
onChange={this.onChangeLogin}
onKeyDown={this.onKeyPress}
className="login-input"
/>
<TextInput
</FieldContainer>
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={!passwordValid}
errorMessage={t("RequiredFieldMessage")}
>
<PasswordInput
simpleView={true}
passwordSettings={this.settings}
id="password"
name="password"
inputName="password"
placeholder={t("Password")}
type="password"
hasError={!passwordValid}
value={password}
placeholder={t("Password")}
size="huge"
inputValue={password}
size="large"
scale={true}
tabIndex={2}
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={this.onChangePassword}
onKeyDown={this.onKeyPress}
className="login-input"
/>
</FieldContainer>
<div className="login-forgot-wrapper">
<div className="login-checkbox-wrapper">
<Checkbox
className="login-checkbox"
isChecked={isChecked}
onChange={this.onChangeCheckbox}
label={t("Remember")}
label={<Text fontSize='13px'>{t("Remember")}</Text>}
/>
<HelpButton
className="login-tooltip"
@ -286,35 +350,38 @@ class Form extends Component {
</div>
<Link
fontSize='12px'
fontSize='13px'
color="#316DAA"
className="login-link"
type="page"
isHovered={true}
isHovered={false}
onClick={this.onClick}
>
{t("ForgotPassword")}
</Link>
</div>
{openDialog ? (
<SubModalDialog
{openDialog &&
<ForgotPasswordModalDialog
openDialog={openDialog}
isLoading={isLoading}
email={email}
emailError={emailError}
onChangeEmail={this.onChangeEmail}
onSendPasswordInstructions={this.onSendPasswordInstructions}
onDialogClose={this.onDialogClose}
t={t}
/>
) : null}
}
<Button
id="button"
className="login-button"
primary
size="big"
size="large"
scale={true}
label={isLoading ? t("LoadingProcessing") : t("LoginButton")}
tabIndex={3}
tabIndex={1}
isDisabled={isLoading}
isLoading={isLoading}
onClick={this.onSubmit}
@ -328,7 +395,19 @@ class Form extends Component {
<Text fontSize='14px' color="#c30">
{errorText}
</Text>
</FormContainer>
{socialButtons.length ? (<Box displayProp="flex" alignItems="center">
<div className="login-bottom-border"></div>
<Text className="login-bottom-text" color="#A3A9AE">{t("Or")}</Text>
<div className="login-bottom-border"></div>
</Box>
) : null}
</form>
</LoginContainer>
</>
);
}
}
@ -341,7 +420,8 @@ Form.propTypes = {
greetingTitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
language: PropTypes.string.isRequired
language: PropTypes.string.isRequired,
socialButtons: PropTypes.array
};
Form.defaultProps = {
@ -351,29 +431,38 @@ Form.defaultProps = {
};
const FormWrapper = withTranslation()(Form);
const RegisterWrapper = withTranslation()(Register);
const LoginForm = props => {
const { language, isLoaded } = props;
const { language, isLoaded, enabledJoin } = props;
i18n.changeLanguage(language);
return (
<>
{isLoaded && <PageLayout sectionBodyContent={<FormWrapper i18n={i18n} {...props} />} />}
{isLoaded && <>
<PageLayout sectionBodyContent={<FormWrapper i18n={i18n} {...props} />} />
{enabledJoin &&
<RegisterWrapper i18n={i18n} {...props} />
}
</>
}
</>
);
};
LoginForm.propTypes = {
language: PropTypes.string.isRequired,
isLoaded: PropTypes.bool
isLoaded: PropTypes.bool,
enabledJoin: PropTypes.bool
};
function mapStateToProps(state) {
return {
isLoaded: state.auth.isLoaded,
language: state.auth.user.cultureName || state.auth.settings.culture,
greetingTitle: state.auth.settings.greetingSettings
greetingTitle: state.auth.settings.greetingSettings,
enabledJoin: state.auth.settings.enabledJoin
};
}

View File

@ -2,17 +2,30 @@
"LoadingProcessing": "Loading...",
"LoginButton": "Sign In",
"Password": "Password",
"RegistrationEmailWatermark": "Your registration email",
"RegistrationEmailWatermark": "E-mail",
"MessageEmailConfirmed": "Your email was activated successfully.",
"MessageAuthorize": "Please authorize yourself.",
"ForgotPassword": "Forgot your password?",
"PasswordRecoveryTitle": "Password recovery",
"PasswordRecoveryPlaceholder": "Your registration e-mail",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Please enter the email you used while registering on the portal. The password recovery instructions will be send to that email address.",
"RegisterTitle": "Registration request",
"RegisterTextBodyBeforeDomainsList": "Registration is available to users with an email account at",
"RegisterTextBodyAfterDomainsList": "To register, enter your email and click on Send request. An email message with a link to activate your account will be sent to the specified email. Please enter the email address where the invitation will be sent:",
"RegisterPlaceholder": "Your registration e-mail",
"RegisterSendButton": "Send request",
"RegisterProcessSending": "Sending...",
"SendButton": "Send",
"RequiredFieldMessage": "Required field",
"CancelButton": "Cancel",
"Remember": "Remember",
"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 the settings.",
"CookieSettingsTitle": "Session Lifetime",
"Authorization": "Authorization",
"Or": "OR",
"Register": "Register",
"OrganizationName": "ONLYOFFICE"
}

View File

@ -6,13 +6,26 @@
"MessageEmailConfirmed": "Ваш email успешно активирован.",
"MessageAuthorize": "Пожалуйста авторизуйтесь.",
"ForgotPassword": "Забыли пароль?",
"PasswordRecoveryTitle": "Восстановление пароля",
"PasswordRecoveryPlaceholder": "Ваш регистрационный email",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Пожалуйста, введите адрес электронной почты, указанный при регистрации на портале. Инструкции для восстановления пароля будут отправлены на этот адрес электронной почты.",
"RegisterTitle": "Запрос на регистрацию",
"RegisterTextBodyBeforeDomainsList": "Регистрация доступна для пользователей, которые имеют почтовый ящик на",
"RegisterTextBodyAfterDomainsList": "Чтобы зарегистрироваться, введите свой email и нажмите кнопку Отправить запрос. Сообщение со ссылкой для активации вашей учётной записи будет отправлено на указанный адрес.",
"RegisterPlaceholder": "Ваш регистрационный email",
"RegisterSendButton": "Отправить запрос",
"RegisterProcessSending": "Отправка...",
"SendButton": "Отправить",
"RequiredFieldMessage": "Обязательное поле",
"CancelButton": "Отмена",
"Remember": "Запомнить",
"RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.",
"CookieSettingsTitle": "Время жизни сессии",
"Authorization": "Авторизация",
"Or": "ИЛИ",
"Register": "Регистрация",
"OrganizationName": "ONLYOFFICE"
}

View File

@ -0,0 +1,91 @@
import React from "react";
import PropTypes from "prop-types";
import { Button, TextInput, Text, ModalDialog, FieldContainer } from "asc-web-components";
import ModalDialogContainer from "./modal-dialog-container";
class ForgotPasswordModalDialog extends React.Component {
render() {
const {
openDialog,
isLoading,
email,
emailError,
onChangeEmail,
onSendPasswordInstructions,
onDialogClose,
t
} = this.props;
return (
<ModalDialogContainer>
<ModalDialog
visible={openDialog}
bodyPadding="16px 0 0 0"
headerContent={
<Text isBold={true} fontSize='21px'>
{t("PasswordRecoveryTitle")}
</Text>
}
bodyContent={[
<Text
key="text-body"
className="text-body"
isBold={false}
fontSize='13px'
>
{t("MessageSendPasswordRecoveryInstructionsOnEmail")}
</Text>,
<FieldContainer
key="e-mail"
isVertical={true}
hasError={emailError}
labelVisible={false}
errorMessage={t("RequiredFieldMessage")}>
<TextInput
hasError={emailError}
placeholder={t("PasswordRecoveryPlaceholder")}
isAutoFocussed={true}
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={2}
isDisabled={isLoading}
value={email}
onChange={onChangeEmail}
/>
</FieldContainer>
]}
footerContent={[
<Button
className="modal-dialog-button"
key="SendBtn"
label={isLoading ? t("LoadingProcessing") : t("SendButton")}
size="big"
scale={false}
primary={true}
onClick={onSendPasswordInstructions}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>
]}
onClose={onDialogClose}
/>
</ModalDialogContainer>
);
}
}
ForgotPasswordModalDialog.propTypes = {
openDialog: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
emailError: PropTypes.bool.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendPasswordInstructions: PropTypes.func.isRequired,
onDialogClose: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default ForgotPasswordModalDialog;

View File

@ -0,0 +1,19 @@
import styled from "styled-components";
const ModalDialogContainer = styled.div`
.modal-dialog-aside-footer {
@media(max-width: 1024px) {
width: 90%;
}
}
.modal-dialog-button {
@media(max-width: 1024px) {
width: 100%;
}
}
.field-body {
margin-top: 16px;
}
`;
export default ModalDialogContainer;

View File

@ -1,86 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { Button, TextInput, Text, ModalDialog } from "asc-web-components";
class SubModalDialog extends React.Component {
render() {
const {
openDialog,
isLoading,
email,
onChangeEmail,
onSendPasswordInstructions,
onDialogClose,
t
} = this.props;
return (
<ModalDialog
visible={openDialog}
headerContent={
<Text isBold={false} fontSize='21px'>
{t("PasswordRecoveryTitle")}
</Text>
}
bodyContent={[
<Text
key="text-body"
className="text-body"
isBold={false}
fontSize='13px'
>
{t("MessageSendPasswordRecoveryInstructionsOnEmail")}
</Text>,
<TextInput
key="e-mail"
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={1}
isDisabled={isLoading}
value={email}
onChange={onChangeEmail}
/>
]}
footerContent={[
<Button
className="login-button-dialog"
key="SendBtn"
label={isLoading ? t("LoadingProcessing") : t("SendButton")}
size="big"
scale={false}
primary={true}
onClick={onSendPasswordInstructions}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>,
<Button
key="CancelBtn"
label={t("CancelButton")}
size="big"
scale={false}
primary={false}
onClick={onDialogClose}
isDisabled={isLoading}
tabIndex={3}
/>
]}
onClose={onDialogClose}
/>
);
}
}
SubModalDialog.propTypes = {
openDialog: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendPasswordInstructions: PropTypes.func.isRequired,
onDialogClose: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default SubModalDialog;

View File

@ -0,0 +1,86 @@
import React, { useState } from "react";
import { Box, Text, toastr } from "asc-web-components";
import RegisterModalDialog from "./register-modal-dialog";
import styled from "styled-components";
import PropTypes from 'prop-types';
import { sendRegisterRequest } from "../../../api/settings/index";
const StyledRegister = styled(Box)`
position: absolute;
z-index: 184;
width: 100%;
height: 66px;
padding: 1.5em;
bottom: 0;
right: 0;
background-color: #F8F9F9;
cursor: pointer;
`;
const Register = ({ t }) => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [emailErr, setEmailErr] = useState(false);
const onRegisterClick = () => {
setVisible(true);
};
const onRegisterModalClose = () => {
setVisible(false);
setEmail("");
setEmailErr(false);
};
const onChangeEmail = (e) => {
setEmail(e.currentTarget.value);
setEmailErr(false);
};
const onSendRegisterRequest = () => {
if (!email.trim()) {
setEmailErr(true);
}
else {
setLoading(true);
sendRegisterRequest(email)
.then(() => {
setLoading(false);
toastr.success("Successfully sent")
})
.catch((error) => {
setLoading(false);
toastr.error(error)
})
.finally(onRegisterModalClose)
}
};
return (
<>
<StyledRegister onClick={onRegisterClick}>
<Text color="#316DAA" textAlign="center">
{t("Register")}
</Text>
</StyledRegister>
{visible &&
<RegisterModalDialog
visible={visible}
loading={loading}
email={email}
emailErr={emailErr}
t={t}
onChangeEmail={onChangeEmail}
onRegisterModalClose={onRegisterModalClose}
onSendRegisterRequest={onSendRegisterRequest}
/>}
</>
)
}
Register.propTypes = {
t: PropTypes.func.isRequired
};
export default Register;

View File

@ -0,0 +1,98 @@
import React from "react";
import PropTypes from "prop-types";
import { Button, TextInput, Text, ModalDialog, FieldContainer } from "asc-web-components";
import ModalDialogContainer from "./modal-dialog-container";
const domains = ['mail.ru', 'gmail.com', 'yandex.ru'];
const domainList = domains
.map((domain, i) =>
<span key={i}>
<b>
{domain}{i === domains.length - 1 ? "." : ", "}
</b>
</span>
);
const RegisterModalDialog = ({
visible,
loading,
email,
emailErr,
t,
onChangeEmail,
onRegisterModalClose,
onSendRegisterRequest
}) => {
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
bodyPadding="16px 0 0 0"
headerContent={
<Text isBold={true} fontSize='21px'>
{t("RegisterTitle")}
</Text>
}
bodyContent={[
<Text
key="text-body"
isBold={false}
fontSize='13px'
>
{t("RegisterTextBodyBeforeDomainsList")} {domainList} {t("RegisterTextBodyAfterDomainsList")}
</Text>,
<FieldContainer
key="e-mail"
isVertical={true}
hasError={emailErr}
labelVisible={false}
errorMessage={t("RequiredFieldMessage")}>
<TextInput
hasError={emailErr}
placeholder={t("RegisterPlaceholder")}
isAutoFocussed={true}
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={3}
isDisabled={loading}
value={email}
onChange={onChangeEmail}
/>
</FieldContainer>
]}
footerContent={[
<Button
className="modal-dialog-button"
key="SendBtn"
label={loading ? t("RegisterProcessSending") : t("RegisterSendButton")}
size="big"
scale={false}
primary={true}
onClick={onSendRegisterRequest}
isLoading={loading}
isDisabled={loading}
tabIndex={3}
/>
]}
onClose={onRegisterModalClose}
/>
</ModalDialogContainer>
);
};
RegisterModalDialog.propTypes = {
visible: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
email: PropTypes.string,
emailErr: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendRegisterRequest: PropTypes.func.isRequired,
onRegisterModalClose: PropTypes.func.isRequired
};
export default RegisterModalDialog;

View File

@ -139,7 +139,7 @@ Checkbox.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
value: PropTypes.string,
label: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
isChecked: PropTypes.bool,
isIndeterminate: PropTypes.bool,

View File

@ -547,7 +547,7 @@ PasswordInput.propTypes = {
autoComplete: PropTypes.string,
inputType: PropTypes.oneOf(["text", "password"]),
inputName: PropTypes.string,
emailInputName: PropTypes.string.isRequired,
emailInputName: PropTypes.string,
inputValue: PropTypes.string,
onChange: PropTypes.func,
inputWidth: PropTypes.string,

View File

@ -2,7 +2,7 @@ import {css} from 'styled-components';
const commonTextStyles = css`
font-family: 'Open Sans', sans-serif, Arial;
text-align: left;
text-align: ${props => props.textAlign};
color: ${props => props.colorProp};
${props => props.truncate && css`
white-space: nowrap;

View File

@ -22,13 +22,14 @@ const StyledText = styled.p`
${commonTextStyles};
`;
const Text = ({ title, tag, as, fontSize, fontWeight, color, ...rest }) => {
const Text = ({ title, tag, as, fontSize, fontWeight, color, textAlign, ...rest }) => {
//console.log("Text render", rest)
return (
<StyledText
fontSizeProp={fontSize}
fontWeightProp={fontWeight}
colorProp={color}
textAlign={textAlign}
as={!as && tag ? tag : as}
title={title}
{...rest}
@ -41,6 +42,7 @@ Text.propTypes = {
tag: PropTypes.string,
title: PropTypes.string,
color: PropTypes.string,
textAlign: PropTypes.string,
fontSize: PropTypes.string,
fontWeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
backgroundColor: PropTypes.string,
@ -54,6 +56,7 @@ Text.propTypes = {
Text.defaultProps = {
title: null,
color: '#333333',
textAlign: 'left',
fontSize: '13px',
truncate: false,
isBold: false,

View File

@ -24,6 +24,7 @@ import { Textarea } from "asc-web-components";
| `id` | `string` | - | - | - | Used as HTML `id` property |
| `isDisabled` | `bool` | - | - | `false` | Indicates that the field cannot be used |
| `isReadOnly` | `bool` | - | - | `false` | Indicates that the field is displaying read-only content |
| `hasError` | `bool` | - | - | - | Indicates the input field has an error |
| `name` | `string` | - | - | - | Used as HTML `name` property |
| `onChange` | `func` | - | - | - | Allow you to handle changing events of component |
| `placeholder` | `string` | - | - | - | Placeholder for Textarea |

View File

@ -6,23 +6,23 @@ import commonInputStyle from '../text-input/common-input-styles';
import TextareaAutosize from 'react-autosize-textarea';
// eslint-disable-next-line react/prop-types, no-unused-vars
const ClearScrollbar = ({ isDisabled, ...props }) => <Scrollbar {...props} />
const ClearScrollbar = ({ isDisabled, heightScale, hasError, ...props }) => <Scrollbar {...props} />
const StyledScrollbar = styled(ClearScrollbar)`
${commonInputStyle};
:focus-within {
border-color: #2DA7DB;
border-color: ${props => props.hasError ? '#c30' : '#2DA7DB'};
}
:focus{
outline: none;
}
width: 100% !important;
height: 91px !important;
height: ${props => props.heightScale ? '67vh' : '91px'} !important;
background-color: ${props => props.isDisabled && '#F8F9F9'};
`;
// eslint-disable-next-line react/prop-types, no-unused-vars
const ClearTextareaAutosize = ({ isDisabled, ...props }) => <TextareaAutosize {...props} />
const ClearTextareaAutosize = ({ isDisabled, heightScale, hasError, ...props }) => <TextareaAutosize {...props} />
const StyledTextarea = styled(ClearTextareaAutosize)`
${commonInputStyle};
width: 100%;
@ -44,6 +44,29 @@ const StyledTextarea = styled(ClearTextareaAutosize)`
outline: none;
}
::-webkit-input-placeholder {
color: ${props => props.isDisabled ? '#D0D5DA' : '#D0D5DA'};
font-family: 'Open Sans',sans-serif;
user-select: none;
}
:-moz-placeholder {
color: ${props => props.isDisabled ? '#D0D5DA' : '#D0D5DA'};
font-family: 'Open Sans',sans-serif;
user-select: none;
}
::-moz-placeholder {
color: ${props => props.isDisabled ? '#D0D5DA' : '#D0D5DA'};
font-family: 'Open Sans',sans-serif;
user-select: none;
}
:-ms-input-placeholder {
color: ${props => props.isDisabled ? '#D0D5DA' : '#D0D5DA'};
font-family: 'Open Sans',sans-serif;
user-select: none;
}
`;
class Textarea extends React.PureComponent {
@ -54,6 +77,8 @@ class Textarea extends React.PureComponent {
id,
isDisabled,
isReadOnly,
hasError,
heightScale,
maxLength,
name,
onChange,
@ -68,6 +93,8 @@ class Textarea extends React.PureComponent {
style={style}
stype='preMediumBlack'
isDisabled={isDisabled}
hasError={hasError}
heightScale={heightScale}
>
<StyledTextarea
id={id}
@ -91,6 +118,8 @@ Textarea.propTypes = {
id: PropTypes.string,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
hasError: PropTypes.bool,
heightScale: PropTypes.bool,
maxLength: PropTypes.number,
name: PropTypes.string,
onChange: PropTypes.func,
@ -104,6 +133,8 @@ Textarea.defaultProps = {
className: '',
isDisabled: false,
isReadOnly: false,
hasError: false,
heightScale: false,
placeholder: '',
tabIndex: -1,
value: '',

View File

@ -28,6 +28,7 @@ storiesOf('Components|Input', module)
placeholder={text('placeholder', 'Add comment')}
isDisabled={boolean('isDisabled', false)}
isReadOnly={boolean('isReadOnly', false)}
hasError={boolean('hasError', false)}
maxLength={number('maxLength', 1000)}
id={text('id', '')}
name={text('name', '')}

View File

@ -66,8 +66,9 @@ namespace ASC.Web.Core.Jabber
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
}
}

View File

@ -26,7 +26,10 @@
using System;
using System.Text.RegularExpressions;
using ASC.Common;
using ASC.Common.Caching;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@ -79,4 +82,13 @@ namespace ASC.Web.Core.Mobile
return result.GetValueOrDefault();
}
}
public static class MobileDetectorExtension
{
public static DIHelper AddMobileDetectorService(this DIHelper services)
{
services.TryAddScoped<MobileDetector>();
return services;
}
}
}

View File

@ -150,9 +150,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, props, additional)
{
Log = options.CurrentValue;
}
@ -211,9 +212,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, options, name, order, props, additional)
{
}
@ -347,9 +349,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, options, name, order, props, additional)
{
}
}
@ -366,9 +369,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, null, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, options, name, order, null, additional)
{
}
}
@ -437,9 +441,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, props, additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, options, name, order, props, additional)
{
VoipDao = voipDao;
}
@ -486,9 +491,10 @@ namespace ASC.Web.Core.Sms
CoreSettings coreSettings,
IConfiguration configuration,
ICacheNotify<ConsumerCacheItem> cache,
ConsumerFactory consumerFactory,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> additional = null)
: base(voipDao, tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, null, additional)
: base(voipDao, tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, options, name, order, null, additional)
{
}
}

View File

@ -0,0 +1,47 @@
/*
*
* (c) Copyright Ascensio System Limited 2010-2018
*
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software. If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute.
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
*
*/
using System;
using ASC.Core.Common.Settings;
namespace ASC.Web.Studio.Core
{
public class StudioAdminMessageSettings : ISettings
{
public bool Enable { get; set; }
public Guid ID
{
get { return new Guid("{28902650-58A9-11E1-B6A9-0F194924019B}"); }
}
public ISettings GetDefault(IServiceProvider serviceProvider)
{
return new StudioAdminMessageSettings { Enable = false };
}
}
}

View File

@ -0,0 +1,47 @@
/*
*
* (c) Copyright Ascensio System Limited 2010-2018
*
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software. If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute.
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
*
*/
using System;
using ASC.Core.Common.Settings;
namespace ASC.Web.Studio.Core
{
public class StudioTrustedDomainSettings : ISettings
{
public bool InviteUsersAsVisitors { get; set; }
public Guid ID
{
get { return new Guid("{00A2DB01-BAE3-48aa-BE32-CE768D7C874E}"); }
}
public ISettings GetDefault(IServiceProvider serviceProvider)
{
return new StudioTrustedDomainSettings { InviteUsersAsVisitors = false };
}
}
}

View File

@ -4,6 +4,7 @@ using ASC.Common.DependencyInjection;
using ASC.Data.Storage;
using ASC.Data.Storage.Configuration;
using ASC.Data.Storage.DiscStorage;
using ASC.FederatedLogin;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -32,7 +33,10 @@ namespace ASC.Web.Studio
diHelper
.AddStorage()
.AddPathUtilsService()
.AddStorageHandlerService();
.AddStorageHandlerService()
.AddLoginHandlerService();
services.AddMemoryCache();
base.ConfigureServices(services);
services.AddAutofac(Configuration, HostEnvironment.ContentRootPath);
@ -59,6 +63,13 @@ namespace ASC.Web.Studio
{
endpoints.InitializeHttpHandlers();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("login.ashx"),
appBranch =>
{
appBranch.UseLoginHandler();
});
}
}
}