Merge branch 'hotfix/v1.1.2' of https://github.com/ONLYOFFICE/DocSpace into hotfix/v1.1.2
This commit is contained in:
commit
6020c1f74f
@ -29,6 +29,12 @@ if ($args[0] -eq "--force") {
|
||||
|
||||
Write-Host "FORCE BUILD BASE IMAGES: $Force" -ForegroundColor Blue
|
||||
|
||||
$ExistsNetwork= docker network ls --format '{{.Name}}' | findstr "onlyoffice"
|
||||
|
||||
if (-not $ExistsNetwork) {
|
||||
docker network create --driver bridge onlyoffice
|
||||
}
|
||||
|
||||
Write-Host "Run MySQL" -ForegroundColor Green
|
||||
docker compose -f "$DockerDir\db.yml" up -d
|
||||
|
||||
|
@ -38,6 +38,12 @@ echo "Run MySQL"
|
||||
|
||||
arch_name="$(uname -m)"
|
||||
|
||||
existsnetwork=$(docker network ls | awk '{print $2;}' | { grep -x onlyoffice || true; });
|
||||
|
||||
if [[ -z ${existsnetwork} ]]; then
|
||||
docker network create --driver bridge onlyoffice
|
||||
fi
|
||||
|
||||
if [ "${arch_name}" = "x86_64" ]; then
|
||||
echo "CPU Type: x86_64 -> run db.yml"
|
||||
docker compose -f $dockerDir/db.yml up -d
|
||||
|
@ -99,7 +99,6 @@
|
||||
DOCEDITOR_HOST=${CONTAINER_PREFIX}doceditor
|
||||
LOGIN_HOST=${CONTAINER_PREFIX}login
|
||||
HELTHCHECKS_HOST=${CONTAINER_PREFIX}healthchecks
|
||||
REDIS_HOST=${CONTAINER_PREFIX}redis
|
||||
|
||||
# proxy upstream environment #
|
||||
SERVICE_API_SYSTEM=${API_SYSTEM_HOST}:${SERVICE_PORT}
|
||||
|
@ -162,7 +162,9 @@ RUN sed -i 's/127.0.0.1:5010/$service_api_system/' /etc/nginx/conf.d/onlyoffice.
|
||||
sed -i 's/127.0.0.1:5011/$service_login/' /etc/nginx/conf.d/onlyoffice.conf && \
|
||||
sed -i 's/127.0.0.1:5033/$service_healthchecks/' /etc/nginx/conf.d/onlyoffice.conf && \
|
||||
sed -i 's/$public_root/\/var\/www\/public\//' /etc/nginx/conf.d/onlyoffice.conf && \
|
||||
sed -i 's/http:\/\/172.*/$document_server;/' /etc/nginx/conf.d/onlyoffice.conf
|
||||
sed -i 's/http:\/\/172.*/$document_server;/' /etc/nginx/conf.d/onlyoffice.conf && \
|
||||
sed -i 's/\(redis_host =\).*/\1 "$server_redis"/' /etc/nginx/conf.d/onlyoffice.conf && \
|
||||
sed -i 's/\(redis_port =\).*/\1 $server_redis_port/' /etc/nginx/conf.d/onlyoffice.conf
|
||||
|
||||
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||
|
||||
|
@ -85,3 +85,15 @@ map "$DOCUMENT_SERVER_URL_EXTERNAL" "$document_server" {
|
||||
default "$DOCUMENT_SERVER_URL_EXTERNAL";
|
||||
"" "http://$DOCUMENT_CONTAINER_NAME";
|
||||
}
|
||||
|
||||
map "$REDIS_HOST" "$server_redis" {
|
||||
volatile;
|
||||
default "$REDIS_HOST";
|
||||
"" "$REDIS_CONTAINER_NAME";
|
||||
}
|
||||
|
||||
map "$REDIS_PORT" "$server_redis_port" {
|
||||
volatile;
|
||||
default "$REDIS_PORT";
|
||||
"" "6379";
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ LOG_DIR = os.environ["LOG_DIR"] if environ.get("LOG_DIR") else "/var/log/" + PRO
|
||||
ROUTER_HOST = os.environ["ROUTER_HOST"] if environ.get("ROUTER_HOST") else "localhost"
|
||||
SOCKET_HOST = os.environ["SOCKET_HOST"] if environ.get("SOCKET_HOST") else "onlyoffice-socket"
|
||||
|
||||
MYSQL_CONTAINER_NAME = os.environ["MYSQL_CONTAINER_NAME"] if environ.get("MYSQL_CONTAINER_NAME") else "onlyoffice-mysql"
|
||||
MYSQL_CONTAINER_NAME = os.environ["MYSQL_CONTAINER_NAME"] if environ.get("MYSQL_CONTAINER_NAME") else "onlyoffice-mysql-server"
|
||||
MYSQL_HOST = os.environ["MYSQL_HOST"] if environ.get("MYSQL_HOST") else None
|
||||
MYSQL_PORT = os.environ["MYSQL_PORT"] if environ.get("MYSQL_PORT") else "3306"
|
||||
MYSQL_DATABASE = os.environ["MYSQL_DATABASE"] if environ.get("MYSQL_DATABASE") else "onlyoffice"
|
||||
|
@ -21,6 +21,7 @@ x-service: &x-service-base
|
||||
expose:
|
||||
- ${SERVICE_PORT}
|
||||
environment:
|
||||
MYSQL_CONTAINER_NAME: ${MYSQL_CONTAINER_NAME}
|
||||
MYSQL_HOST: ${MYSQL_HOST}
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
@ -37,9 +38,24 @@ x-service: &x-service-base
|
||||
DOCUMENT_SERVER_JWT_SECRET: ${DOCUMENT_SERVER_JWT_SECRET}
|
||||
DOCUMENT_SERVER_JWT_HEADER: ${DOCUMENT_SERVER_JWT_HEADER}
|
||||
DOCUMENT_SERVER_URL_PUBLIC: ${DOCUMENT_SERVER_URL_PUBLIC}
|
||||
DOCUMENT_SERVER_URL_INTERNAL: ${DOCUMENT_SERVER_URL_INTERNAL}
|
||||
DOCUMENT_CONTAINER_NAME: ${DOCUMENT_CONTAINER_NAME}
|
||||
DOCUMENT_SERVER_URL_EXTERNAL: ${DOCUMENT_SERVER_URL_EXTERNAL}
|
||||
KAFKA_HOST: ${KAFKA_HOST}
|
||||
ELK_CONTAINER_NAME: ${ELK_CONTAINER_NAME}
|
||||
ELK_SHEME: ${ELK_SHEME}
|
||||
ELK_HOST: ${ELK_HOST}
|
||||
ELK_PORT: ${ELK_PORT}
|
||||
REDIS_CONTAINER_NAME: ${REDIS_CONTAINER_NAME}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
REDIS_PORT: ${REDIS_PORT}
|
||||
REDIS_USER_NAME: ${REDIS_USER_NAME}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
RABBIT_CONTAINER_NAME: ${RABBIT_CONTAINER_NAME}
|
||||
RABBIT_HOST: ${RABBIT_HOST}
|
||||
RABBIT_PORT: ${RABBIT_PORT}
|
||||
RABBIT_VIRTUAL_HOST: ${RABBIT_VIRTUAL_HOST}
|
||||
RABBIT_USER_NAME: ${RABBIT_USER_NAME}
|
||||
RABBIT_PASSWORD: ${RABBIT_PASSWORD}
|
||||
PROXY_HOST: ${PROXY_HOST}
|
||||
volumes:
|
||||
- ${ROOT_DIR}/Data:/app/onlyoffice/data
|
||||
@ -91,7 +107,7 @@ services:
|
||||
onlyoffice-document-server:
|
||||
<<: [*x-profiles-extra-services]
|
||||
image: "${DOCUMENT_SERVER_IMAGE_NAME}"
|
||||
container_name: ${DOCUMENT_SERVER_HOST}
|
||||
container_name: ${DOCUMENT_CONTAINER_NAME}
|
||||
# Strings below enable the JSON Web Token validation.
|
||||
environment:
|
||||
- JWT_ENABLED=true
|
||||
@ -253,7 +269,11 @@ services:
|
||||
- SERVICE_DOCEDITOR=${SERVICE_DOCEDITOR}
|
||||
- SERVICE_LOGIN=${SERVICE_LOGIN}
|
||||
- SERVICE_HELTHCHECKS=${SERVICE_HELTHCHECKS}
|
||||
- DOCUMENT_SERVER=${DOCUMENT_SERVER_HOST}
|
||||
- DOCUMENT_CONTAINER_NAME=${DOCUMENT_CONTAINER_NAME}
|
||||
- DOCUMENT_SERVER_URL_EXTERNAL=${DOCUMENT_SERVER_URL_EXTERNAL}
|
||||
- REDIS_CONTAINER_NAME=${REDIS_CONTAINER_NAME}
|
||||
- REDIS_HOST=${REDIS_HOST}
|
||||
- REDIS_PORT=${REDIS_PORT}
|
||||
- SERVICE_PORT=${SERVICE_PORT}
|
||||
volumes:
|
||||
- proxy_log:/var/log/nginx
|
||||
@ -264,6 +284,7 @@ services:
|
||||
container_name: ${MIGRATION_RUNNER_HOST}
|
||||
restart: "no"
|
||||
environment:
|
||||
MYSQL_CONTAINER_NAME: ${MYSQL_CONTAINER_NAME}
|
||||
MYSQL_HOST: ${MYSQL_HOST}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
|
@ -220,7 +220,9 @@ services:
|
||||
- SERVICE_HELTHCHECKS=${SERVICE_HELTHCHECKS}
|
||||
- DOCUMENT_CONTAINER_NAME=${DOCUMENT_CONTAINER_NAME}
|
||||
- DOCUMENT_SERVER_URL_EXTERNAL=${DOCUMENT_SERVER_URL_EXTERNAL}
|
||||
- REDIS_CONTAINER_NAME=${REDIS_CONTAINER_NAME}
|
||||
- REDIS_HOST=${REDIS_HOST}
|
||||
- REDIS_PORT=${REDIS_PORT}
|
||||
- SERVICE_PORT=${SERVICE_PORT}
|
||||
volumes:
|
||||
- proxy_log:/var/log/nginx
|
||||
|
@ -2,4 +2,3 @@
|
||||
REDIS_HOST=${REDIS_HOST:-"onlyoffice-redis"}
|
||||
|
||||
envsubst '$MAP_HASH_BUCKET_SIZE,$COUNT_WORKER_CONNECTIONS' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
sed -i "s!redis_host = \"127.0.0.1\"!redis_host = \"${REDIS_HOST}\"!g" /etc/nginx/conf.d/onlyoffice.conf
|
||||
|
@ -400,6 +400,89 @@ Function TestSqlConnection
|
||||
Set ConnectionObject = Nothing
|
||||
End Function
|
||||
|
||||
Function OpenRestySetup
|
||||
On Error Resume Next
|
||||
|
||||
Dim objShell, sourcePath, destinationPath, openRestyServicePath, openRestyFolder, objFSO, objFolder
|
||||
|
||||
Set objShell = CreateObject("WScript.Shell")
|
||||
|
||||
destinationPath = Session.Property("APPDIR")
|
||||
openRestyServicePath = Session.Property("APPDIR") & "tools\OpenResty.exe"
|
||||
openRestyFolder = ""
|
||||
Set objFSO = CreateObject("Scripting.FileSystemObject")
|
||||
For Each objFolder In objFSO.GetFolder(destinationPath).SubFolders
|
||||
If Left(objFolder.Name, 9) = "openresty" Then
|
||||
openRestyFolder = objFolder.Name
|
||||
End If
|
||||
Next
|
||||
Set objFSO = Nothing
|
||||
|
||||
sourcePath = Session.Property("APPDIR") & openRestyFolder
|
||||
|
||||
' Run XCopy to copy files and folders
|
||||
objShell.Run "xcopy """ & sourcePath & """ """ & destinationPath & """ /E /I /Y", 0, True
|
||||
|
||||
objShell.CurrentDirectory = destinationPath
|
||||
|
||||
' Run the RMDIR command to delete the folder
|
||||
objShell.Run "cmd /c RMDIR /S /Q """ & openRestyFolder & """", 0, True
|
||||
|
||||
objShell.Run """" & openRestyServicePath & """ install", 0, True
|
||||
|
||||
Set objShell = Nothing
|
||||
|
||||
End Function
|
||||
|
||||
Function MoveNginxConfigs
|
||||
On Error Resume Next
|
||||
|
||||
Dim objFSO, sourceFolder, targetFolder, nginxFolder
|
||||
|
||||
' Define source and target paths
|
||||
Set objFSO = CreateObject("Scripting.FileSystemObject")
|
||||
sourceFolder = Session.Property("APPDIR") & "nginx\conf"
|
||||
targetFolder = "C:\OpenResty\conf"
|
||||
nginxFolder = Session.Property("APPDIR") & "nginx"
|
||||
|
||||
' Check if source folder exists
|
||||
If objFSO.FolderExists(sourceFolder) Then
|
||||
' Check if target folder exists, if not, create it
|
||||
If Not objFSO.FolderExists(targetFolder) Then
|
||||
objFSO.CreateFolder(targetFolder)
|
||||
End If
|
||||
|
||||
' Copy files and folders from source to target
|
||||
CopyFolderContents objFSO.GetFolder(sourceFolder), targetFolder, objFSO
|
||||
|
||||
' Delete source folder
|
||||
objFSO.DeleteFolder nginxFolder, True ' "True" parameter for recursive deletion
|
||||
|
||||
WScript.Echo "Files and folders moved, and source folder deleted."
|
||||
Else
|
||||
WScript.Echo "Source folder does not exist."
|
||||
End If
|
||||
|
||||
Set objFSO = Nothing
|
||||
End Function
|
||||
|
||||
Sub CopyFolderContents(sourceFolder, targetFolder, objFSO)
|
||||
Dim subFolder, objFile
|
||||
|
||||
' Copy files
|
||||
For Each objFile In sourceFolder.Files
|
||||
objFSO.CopyFile objFile.Path, targetFolder & "\" & objFile.Name, True
|
||||
Next
|
||||
|
||||
' Recursively copy subfolders
|
||||
For Each subFolder In sourceFolder.SubFolders
|
||||
Dim newTargetFolder
|
||||
newTargetFolder = targetFolder & "\" & subFolder.Name
|
||||
objFSO.CreateFolder newTargetFolder
|
||||
CopyFolderContents subFolder, newTargetFolder, objFSO
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Function EnterpriseConfigure
|
||||
On Error Resume Next
|
||||
|
||||
|
@ -22,8 +22,8 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
namespace ASC.Web.Studio.Core.Backup;
|
||||
|
||||
public class BackupFileUploadHandler
|
||||
@ -35,56 +35,101 @@ public class BackupFileUploadHandler
|
||||
|
||||
public async Task Invoke(HttpContext context,
|
||||
PermissionContext permissionContext,
|
||||
BackupAjaxHandler backupAjaxHandler)
|
||||
BackupAjaxHandler backupAjaxHandler,
|
||||
ICache cache,
|
||||
TenantManager tenantManager,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
FileUploadResult result;
|
||||
BackupFileUploadResult result = null;
|
||||
try
|
||||
{
|
||||
if (context.Request.Form.Files.Count == 0)
|
||||
{
|
||||
result = Error("No files.");
|
||||
}
|
||||
|
||||
{
|
||||
if (!permissionContext.CheckPermissions(SecutiryConstants.EditPortalSettings))
|
||||
{
|
||||
result = Error("Access denied.");
|
||||
}
|
||||
|
||||
var file = context.Request.Form.Files[0];
|
||||
|
||||
var filePath = backupAjaxHandler.GetTmpFilePath();
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
|
||||
using (var fileStream = File.Create(filePath))
|
||||
{
|
||||
await file.CopyToAsync(fileStream);
|
||||
throw new ArgumentException("Access denied.");
|
||||
}
|
||||
var tenantId = tenantManager.GetCurrentTenant().Id;
|
||||
var path = backupAjaxHandler.GetTmpFilePath();
|
||||
if (context.Request.Query["Init"].ToString() == "true")
|
||||
{
|
||||
long.TryParse(context.Request.Query["totalSize"], out var size);
|
||||
if (size <= 0)
|
||||
{
|
||||
throw new ArgumentException("Total size must be greater than 0.");
|
||||
}
|
||||
|
||||
result = Success();
|
||||
var maxSize = tenantManager.GetCurrentTenantQuota().MaxTotalSize;
|
||||
if (size > maxSize)
|
||||
{
|
||||
throw new ArgumentException(BackupResource.LargeBackup);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
cache.Insert($"{tenantId} backupTotalSize", size.ToString(), TimeSpan.FromMinutes(10));
|
||||
|
||||
int.TryParse(configuration["files:uploader:chunk-size"], out var chunkSize);
|
||||
chunkSize = chunkSize == 0 ? 10 * 1024 * 1024 : chunkSize;
|
||||
|
||||
result = Success(chunkSize);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ArgumentException("Can't start upload.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
long.TryParse(cache.Get<string>($"{tenantId} backupTotalSize"), out var totalSize);
|
||||
if (totalSize <= 0)
|
||||
{
|
||||
throw new ArgumentException("Need init upload.");
|
||||
}
|
||||
|
||||
var file = context.Request.Form.Files[0];
|
||||
using var stream = file.OpenReadStream();
|
||||
|
||||
using var fs = File.Open(path, FileMode.Append);
|
||||
await stream.CopyToAsync(fs);
|
||||
|
||||
if (fs.Length >= totalSize)
|
||||
{
|
||||
cache.Remove($"{tenantId} backupTotalSize");
|
||||
result = Success(endUpload: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Success();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
result = Error(error.Message);
|
||||
}
|
||||
|
||||
await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(result));
|
||||
await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
}));
|
||||
}
|
||||
|
||||
private FileUploadResult Success()
|
||||
private BackupFileUploadResult Success(int chunk = 0, bool endUpload = false)
|
||||
{
|
||||
return new FileUploadResult
|
||||
return new BackupFileUploadResult
|
||||
{
|
||||
Success = true
|
||||
Success = true,
|
||||
ChunkSize = chunk,
|
||||
EndUpload = endUpload
|
||||
};
|
||||
}
|
||||
|
||||
private FileUploadResult Error(string messageFormat, params object[] args)
|
||||
private BackupFileUploadResult Error(string messageFormat, params object[] args)
|
||||
{
|
||||
return new FileUploadResult
|
||||
return new BackupFileUploadResult
|
||||
{
|
||||
Success = false,
|
||||
Message = string.Format(messageFormat, args)
|
||||
@ -92,6 +137,14 @@ public class BackupFileUploadHandler
|
||||
}
|
||||
}
|
||||
|
||||
internal class BackupFileUploadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
public int ChunkSize { get; set; }
|
||||
public bool EndUpload { get; set; }
|
||||
}
|
||||
|
||||
public static class BackupFileUploadHandlerExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseBackupFileUploadHandler(this IApplicationBuilder builder)
|
||||
|
@ -77,5 +77,14 @@ namespace ASC.Data.Backup.Core {
|
||||
return ResourceManager.GetString("ButtonSetPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Backup is larger than the total size of the portal.
|
||||
/// </summary>
|
||||
internal static string LargeBackup {
|
||||
get {
|
||||
return ResourceManager.GetString("LargeBackup", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@ -53,10 +112,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="BackupNotFound" xml:space="preserve">
|
||||
<value>The backup file is invalid. Please, use a file created in ONLYOFFICE v11.5 or later.</value>
|
||||
@ -64,4 +123,7 @@
|
||||
<data name="ButtonSetPassword" xml:space="preserve">
|
||||
<value>Set Password</value>
|
||||
</data>
|
||||
<data name="LargeBackup" xml:space="preserve">
|
||||
<value>Backup is larger than the total size of the portal</value>
|
||||
</data>
|
||||
</root>
|
@ -32,6 +32,8 @@ global using System.Reflection;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.ServiceModel;
|
||||
global using System.Text;
|
||||
|
||||
global using System.Text.Json.Serialization;
|
||||
global using System.Text.RegularExpressions;
|
||||
global using System.Xml;
|
||||
global using System.Xml.Linq;
|
||||
@ -80,7 +82,6 @@ global using ASC.Notify.Recipients;
|
||||
global using ASC.Security.Cryptography;
|
||||
global using ASC.Web.Core.PublicResources;
|
||||
global using ASC.Web.Core.Users;
|
||||
global using ASC.Web.Core.Utility;
|
||||
global using ASC.Web.Core.WhiteLabel;
|
||||
global using ASC.Web.Studio.Core;
|
||||
global using ASC.Web.Studio.Core.Notify;
|
||||
|
@ -109,6 +109,7 @@ public class ChunkZipWriteOperator : IDataWriteOperator
|
||||
|
||||
theMemStream.Position = 0;
|
||||
StoragePath = await _sessionHolder.UploadChunkAsync(_chunkedUploadSession, theMemStream, theMemStream.Length);
|
||||
_sha.TransformBlock(buffer, 0, bytesRead, buffer, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -119,7 +120,6 @@ public class ChunkZipWriteOperator : IDataWriteOperator
|
||||
await theMemStream.CopyToAsync(_fileStream);
|
||||
_fileStream.Flush();
|
||||
}
|
||||
_sha.TransformBlock(buffer, 0, bytesRead, buffer, 0);
|
||||
}
|
||||
}
|
||||
if (last)
|
||||
|
@ -62,25 +62,26 @@ module.exports = function () {
|
||||
}
|
||||
|
||||
const ErrorMessageKey = {
|
||||
Error: 1,
|
||||
SsoError: 17,
|
||||
SsoAuthFailed: 18,
|
||||
SsoAttributesNotFound: 19,
|
||||
};
|
||||
|
||||
function getPortalAuthErrorUrl(req, errorKey) {
|
||||
const url = getPortalAuthUrl(req) + "?am=" + errorKey;
|
||||
const url = getBaseUrl(req) + "/login/error?messageKey=" + errorKey;
|
||||
logger.debug("getPortalAuthErrorUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalErrorUrl(req) {
|
||||
const url = getBaseUrl(req) + "/500.aspx";
|
||||
const url = getBaseUrl(req) + "/login/error?messageKey=" + ErrorMessageKey.Error;
|
||||
logger.debug("getPortal500Url: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortal404Url(req) {
|
||||
const url = getBaseUrl(req) + "/404.aspx";
|
||||
const url = getBaseUrl(req) + "/login/error?messageKey=" + ErrorMessageKey.SsoError;
|
||||
logger.debug("getPortal404Url: " + url);
|
||||
return url;
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ global using ASC.Files.Core.EF;
|
||||
global using ASC.Web.Api.Routing;
|
||||
global using ASC.Web.Studio.Core.Backup;
|
||||
global using ASC.Web.Studio.Core.Notify;
|
||||
global using ASC.Web.Studio.Utility;
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
global using Autofac;
|
||||
|
||||
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
global using Microsoft.AspNetCore.Http.Features;
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
|
@ -16,6 +16,7 @@
|
||||
{
|
||||
"Period":"00:15:00"
|
||||
},
|
||||
"ChunkSize": 20971520
|
||||
"ChunkSize": 20971520,
|
||||
"MaxLocalSize": 1048576000
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import saveAs from "file-saver";
|
||||
|
||||
export const getCrashReport = (userId, version, language, error) => {
|
||||
const currentTime = new Date();
|
||||
const reportTime = currentTime.toTimeString();
|
||||
const reportTime = currentTime.toUTCString();
|
||||
const lsObject = JSON.stringify(window.localStorage) || "";
|
||||
|
||||
const report = {
|
||||
|
@ -96,6 +96,8 @@ const DirectThirdPartyConnection = (props) => {
|
||||
try {
|
||||
if (!isDirectConnection()) setState({ isUpdatingInfo: true });
|
||||
|
||||
onSelectFolder && onSelectFolder("");
|
||||
|
||||
let account;
|
||||
[account, capabilities] = await Promise.all([
|
||||
getSettingsThirdParty(),
|
||||
@ -186,6 +188,7 @@ const DirectThirdPartyConnection = (props) => {
|
||||
|
||||
const onConnect = () => {
|
||||
clearLocalStorage();
|
||||
onSelectFolder && onSelectFolder("");
|
||||
|
||||
const {
|
||||
provider_key,
|
||||
|
@ -8,7 +8,6 @@ import { TenantStatus } from "@docspace/common/constants";
|
||||
import { startRestore } from "@docspace/common/api/portal";
|
||||
import { combineUrl } from "@docspace/common/utils";
|
||||
import toastr from "@docspace/components/toast/toastr";
|
||||
import { request } from "@docspace/common/api/client";
|
||||
|
||||
const ButtonContainer = (props) => {
|
||||
const {
|
||||
@ -28,29 +27,11 @@ const ButtonContainer = (props) => {
|
||||
setTenantStatus,
|
||||
isFormReady,
|
||||
getStorageParams,
|
||||
uploadLocalFile,
|
||||
} = props;
|
||||
|
||||
const [isUploadingLocalFile, setIsUploadingLocalFile] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const localFileUploading = async () => {
|
||||
try {
|
||||
const checkedFile = await request({
|
||||
baseURL: combineUrl(window.DocSpaceConfig?.proxy?.url, config.homepage),
|
||||
method: "post",
|
||||
url: `/backupFileUpload.ashx`,
|
||||
responseType: "text",
|
||||
data: restoreResource,
|
||||
});
|
||||
|
||||
return checkedFile;
|
||||
} catch (e) {
|
||||
toastr.error(e);
|
||||
setIsUploadingLocalFile(false);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onRestoreClick = async () => {
|
||||
if (isCheckedThirdPartyStorage) {
|
||||
const requiredFieldsFilled = isFormReady();
|
||||
@ -74,16 +55,16 @@ const ButtonContainer = (props) => {
|
||||
}
|
||||
|
||||
if (isCheckedLocalFile) {
|
||||
const isUploadedFile = await localFileUploading();
|
||||
const uploadedFile = await uploadLocalFile();
|
||||
|
||||
if (!isUploadedFile) {
|
||||
if (!uploadedFile) {
|
||||
toastr.error(t("BackupCreatedError"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUploadedFile?.Message) {
|
||||
toastr.error(isUploadedFile.Message);
|
||||
setIsUploadingLocalFile(false);
|
||||
if (!uploadedFile.data.EndUpload) {
|
||||
toastr.error(uploadedFile.data.Message ?? t("BackupCreatedError"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@ -107,19 +88,17 @@ const ButtonContainer = (props) => {
|
||||
} catch (e) {
|
||||
toastr.error(e);
|
||||
|
||||
setIsUploadingLocalFile(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isButtonDisabled =
|
||||
isLoading ||
|
||||
isUploadingLocalFile ||
|
||||
!isMaxProgress ||
|
||||
!isConfirmed ||
|
||||
!isEnableRestore ||
|
||||
!restoreResource;
|
||||
const isLoadingButton = isUploadingLocalFile || isLoading;
|
||||
const isLoadingButton = isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -154,11 +133,13 @@ export default inject(({ auth, backup }) => {
|
||||
isFormReady,
|
||||
getStorageParams,
|
||||
restoreResource,
|
||||
uploadLocalFile,
|
||||
} = backup;
|
||||
|
||||
const { isRestoreAndAutoBackupAvailable } = currentQuotaStore;
|
||||
const isMaxProgress = downloadingProgress === 100;
|
||||
return {
|
||||
uploadLocalFile,
|
||||
isMaxProgress,
|
||||
setTenantStatus,
|
||||
isEnableRestore: isRestoreAndAutoBackupAvailable,
|
||||
|
@ -5,10 +5,7 @@ import FileInput from "@docspace/components/file-input";
|
||||
|
||||
const LocalFile = ({ setRestoreResource, isEnableRestore, t }) => {
|
||||
const onClickInput = (file) => {
|
||||
let data = new FormData();
|
||||
data.append("file", file);
|
||||
|
||||
setRestoreResource(data);
|
||||
setRestoreResource(file);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -72,7 +72,7 @@ const DeveloperToolsWrapper = (props) => {
|
||||
const currentTab = data.findIndex((item) => path.includes(item.id));
|
||||
if (currentTab !== -1) setCurrentTab(currentTab);
|
||||
|
||||
await loadBaseInfo();
|
||||
//await loadBaseInfo();
|
||||
setIsLoading(true);
|
||||
};
|
||||
|
||||
|
@ -16,15 +16,8 @@ import SSOLoader from "./sub-components/ssoLoader";
|
||||
import SMTPSettings from "./SMTPSettings";
|
||||
|
||||
const IntegrationWrapper = (props) => {
|
||||
const {
|
||||
t,
|
||||
tReady,
|
||||
history,
|
||||
loadBaseInfo,
|
||||
enablePlugins,
|
||||
toDefault,
|
||||
isSSOAvailable,
|
||||
} = props;
|
||||
const { t, tReady, history, enablePlugins, toDefault, isSSOAvailable } =
|
||||
props;
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@ -67,7 +60,6 @@ const IntegrationWrapper = (props) => {
|
||||
const currentTab = data.findIndex((item) => path.includes(item.id));
|
||||
if (currentTab !== -1) setCurrentTab(currentTab);
|
||||
|
||||
await loadBaseInfo();
|
||||
setIsLoading(true);
|
||||
};
|
||||
|
||||
@ -91,16 +83,12 @@ const IntegrationWrapper = (props) => {
|
||||
return <Submenu data={data} startSelect={currentTab} onSelect={onSelect} />;
|
||||
};
|
||||
|
||||
export default inject(({ setup, auth, ssoStore }) => {
|
||||
const { initSettings } = setup;
|
||||
export default inject(({ auth, ssoStore }) => {
|
||||
const { load: toDefault } = ssoStore;
|
||||
const { enablePlugins } = auth.settingsStore;
|
||||
const { isSSOAvailable } = auth.currentQuotaStore;
|
||||
|
||||
return {
|
||||
loadBaseInfo: async () => {
|
||||
await initSettings();
|
||||
},
|
||||
enablePlugins,
|
||||
toDefault,
|
||||
isSSOAvailable,
|
||||
|
@ -17,7 +17,7 @@ import { resetSessionStorage } from "../../utils";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
const SecurityWrapper = (props) => {
|
||||
const { t, history, loadBaseInfo } = props;
|
||||
const { t, history, loadBaseInfo, resetIsInit } = props;
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@ -46,6 +46,7 @@ const SecurityWrapper = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetIsInit();
|
||||
resetSessionStorage();
|
||||
};
|
||||
}, []);
|
||||
@ -88,12 +89,13 @@ const SecurityWrapper = (props) => {
|
||||
};
|
||||
|
||||
export default inject(({ setup }) => {
|
||||
const { initSettings } = setup;
|
||||
const { initSettings, resetIsInit } = setup;
|
||||
|
||||
return {
|
||||
loadBaseInfo: async () => {
|
||||
await initSettings();
|
||||
},
|
||||
resetIsInit,
|
||||
};
|
||||
})(
|
||||
withTranslation(["Settings", "Common"])(withRouter(observer(SecurityWrapper)))
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
} from "../pages/PortalSettings/utils";
|
||||
import toastr from "@docspace/components/toast/toastr";
|
||||
import { AutoBackupPeriod } from "@docspace/common/constants";
|
||||
//import api from "@docspace/common/api";
|
||||
import { combineUrl } from "@docspace/common/utils";
|
||||
import config from "PACKAGE_FILE";
|
||||
import { uploadBackup } from "@docspace/common/api/files";
|
||||
|
||||
const { EveryDayType, EveryWeekType } = AutoBackupPeriod;
|
||||
|
||||
@ -617,6 +619,73 @@ class BackupStore {
|
||||
setRestoreResource = (value) => {
|
||||
this.restoreResource = value;
|
||||
};
|
||||
|
||||
setChunkUploadSize = (chunkUploadSize) => {
|
||||
this.chunkUploadSize = chunkUploadSize;
|
||||
};
|
||||
|
||||
uploadFileChunks = async (requestsDataArray, url) => {
|
||||
const length = requestsDataArray.length;
|
||||
let res;
|
||||
|
||||
for (let index = 0; index < length; index++) {
|
||||
res = await uploadBackup(
|
||||
combineUrl(window.DocSpaceConfig?.proxy?.url, config.homepage, url),
|
||||
requestsDataArray[index]
|
||||
);
|
||||
|
||||
if (!res) return false;
|
||||
|
||||
if (res.data.Message || !res.data.Success) return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
uploadLocalFile = async () => {
|
||||
try {
|
||||
const url = "/backupFileUpload.ashx";
|
||||
|
||||
const res = await uploadBackup(
|
||||
combineUrl(
|
||||
window.DocSpaceConfig?.proxy?.url,
|
||||
config.homepage,
|
||||
`${url}?init=true&totalSize=${this.restoreResource.size}`
|
||||
)
|
||||
);
|
||||
|
||||
if (!res) return false;
|
||||
|
||||
if (res.data.Message || !res.data.Success) return res;
|
||||
|
||||
const chunkUploadSize = res.data.ChunkSize;
|
||||
|
||||
const chunks = Math.ceil(
|
||||
this.restoreResource.size / chunkUploadSize,
|
||||
chunkUploadSize
|
||||
);
|
||||
|
||||
const requestsDataArray = [];
|
||||
|
||||
let chunk = 0;
|
||||
|
||||
while (chunk < chunks) {
|
||||
const offset = chunk * chunkUploadSize;
|
||||
const formData = new FormData();
|
||||
formData.append(
|
||||
"file",
|
||||
this.restoreResource.slice(offset, offset + chunkUploadSize)
|
||||
);
|
||||
|
||||
requestsDataArray.push(formData);
|
||||
chunk++;
|
||||
}
|
||||
|
||||
return await this.uploadFileChunks(requestsDataArray, url);
|
||||
} catch (e) {
|
||||
toastr.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default BackupStore;
|
||||
|
@ -99,6 +99,10 @@ class SettingsSetupStore {
|
||||
this.isLoadingDownloadReport = state;
|
||||
};
|
||||
|
||||
resetIsInit = () => {
|
||||
this.isInit = false;
|
||||
};
|
||||
|
||||
setIsInit = (isInit) => {
|
||||
this.isInit = isInit;
|
||||
};
|
||||
|
@ -560,6 +560,10 @@ export function uploadFile(url, data) {
|
||||
return axios.post(url, data);
|
||||
}
|
||||
|
||||
export function uploadBackup(url, data) {
|
||||
return axios.post(url, data);
|
||||
}
|
||||
|
||||
export function downloadFiles(fileIds, folderIds) {
|
||||
const data = { fileIds, folderIds };
|
||||
return request({ method: "put", url: "/files/fileops/bulkdownload", data });
|
||||
|
@ -2,6 +2,7 @@ import ShareGoogleReactSvgUrl from "PUBLIC_DIR/images/share.google.react.svg?url
|
||||
import ShareFacebookReactSvgUrl from "PUBLIC_DIR/images/share.facebook.react.svg?url";
|
||||
import ShareTwitterReactSvgUrl from "PUBLIC_DIR/images/share.twitter.react.svg?url";
|
||||
import ShareLinkedinReactSvgUrl from "PUBLIC_DIR/images/share.linkedin.react.svg?url";
|
||||
import ShareMicrosoftReactSvgUrl from "PUBLIC_DIR/images/share.microsoft.react.svg?url";
|
||||
|
||||
export const LANGUAGE = "asc_language";
|
||||
export const COOKIE_EXPIRATION_YEAR = 31536000000;
|
||||
@ -247,6 +248,10 @@ export const providersData = Object.freeze({
|
||||
label: "linkedin",
|
||||
icon: ShareLinkedinReactSvgUrl,
|
||||
},
|
||||
microsoft: {
|
||||
label: "microsoft",
|
||||
icon: ShareMicrosoftReactSvgUrl,
|
||||
},
|
||||
});
|
||||
export const LoaderStyle = {
|
||||
title: "",
|
||||
|
@ -283,6 +283,8 @@ export function getProviderTranslation(provider, t, linked = false) {
|
||||
return t("Common:SignInWithTwitter");
|
||||
case "linkedin":
|
||||
return t("Common:SignInWithLinkedIn");
|
||||
case "microsoft":
|
||||
return t("Common:SignInWithMicrosoft");
|
||||
case "sso":
|
||||
return t("Common:SignInWithSso");
|
||||
}
|
||||
|
@ -94,12 +94,18 @@ internal class OneDriveStorage
|
||||
|
||||
public async Task<bool> CheckAccessAsync()
|
||||
{
|
||||
var request = await OnedriveClient
|
||||
.Drive
|
||||
.Request()
|
||||
.GetAsync();
|
||||
|
||||
return request != null;
|
||||
try
|
||||
{
|
||||
var request = await OnedriveClient
|
||||
.Drive
|
||||
.Request()
|
||||
.GetAsync();
|
||||
return request != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -325,7 +325,7 @@ public class UserController : PeopleControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
if (inDto.IsUser)
|
||||
if (inDto.IsUser.GetValueOrDefault(false))
|
||||
{
|
||||
_messageService.Send(MessageAction.GuestCreated, _messageTarget.Create(user.Id), user.DisplayUserName(false, _displayUserSettingsHelper));
|
||||
}
|
||||
@ -1354,20 +1354,23 @@ public class UserController : PeopleControllerBase
|
||||
// change user type
|
||||
var canBeGuestFlag = !user.IsOwner(Tenant) && !_userManager.IsDocSpaceAdmin(user) && user.GetListAdminModules(_webItemSecurity, _webItemManager).Count == 0 && !user.IsMe(_authContext);
|
||||
|
||||
if (inDto.IsUser && !_userManager.IsUser(user) && canBeGuestFlag)
|
||||
if (inDto.IsUser.HasValue)
|
||||
{
|
||||
await _countUserChecker.CheckAppend();
|
||||
await _userManager.AddUserIntoGroup(user.Id, Constants.GroupUser.ID);
|
||||
_webItemSecurityCache.ClearCache(Tenant.Id);
|
||||
}
|
||||
var isUser = inDto.IsUser.Value;
|
||||
if (isUser && !_userManager.IsUser(user) && canBeGuestFlag)
|
||||
{
|
||||
await _countUserChecker.CheckAppend();
|
||||
await _userManager.AddUserIntoGroup(user.Id, Constants.GroupUser.ID);
|
||||
_webItemSecurityCache.ClearCache(Tenant.Id);
|
||||
}
|
||||
|
||||
if (!self && !inDto.IsUser && _userManager.IsUser(user))
|
||||
{
|
||||
await _countPaidUserChecker.CheckAppend();
|
||||
await _userManager.RemoveUserFromGroup(user.Id, Constants.GroupUser.ID);
|
||||
_webItemSecurityCache.ClearCache(Tenant.Id);
|
||||
if (!self && !isUser && _userManager.IsUser(user))
|
||||
{
|
||||
await _countPaidUserChecker.CheckAppend();
|
||||
await _userManager.RemoveUserFromGroup(user.Id, Constants.GroupUser.ID);
|
||||
_webItemSecurityCache.ClearCache(Tenant.Id);
|
||||
}
|
||||
}
|
||||
|
||||
await _userManager.UpdateUserInfoWithSyncCardDavAsync(user);
|
||||
|
||||
_messageService.Send(MessageAction.UserUpdated, _messageTarget.Create(user.Id), user.DisplayUserName(false, _displayUserSettingsHelper), user.Id);
|
||||
|
@ -36,7 +36,7 @@ public class MemberRequestDto
|
||||
|
||||
/// <summary>Specifies if this is a guest or a user</summary>
|
||||
/// <type>System.Boolean, System</type>
|
||||
public bool IsUser { get; set; }
|
||||
public bool? IsUser { get; set; }
|
||||
|
||||
/// <summary>Email</summary>
|
||||
/// <type>System.String, System</type>
|
||||
|
6
public/images/share.microsoft.react.svg
Normal file
6
public/images/share.microsoft.react.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0H9.50383V9.50383H0V0Z" fill="#F25022"/>
|
||||
<path d="M10.4961 0H20V9.50383H10.4961V0Z" fill="#7FBA00"/>
|
||||
<path d="M0 10.4961H9.50383V19.9999H0V10.4961Z" fill="#00A4EF"/>
|
||||
<path d="M10.4961 10.4961H20V19.9999H10.4961V10.4961Z" fill="#FFB900"/>
|
||||
</svg>
|
After Width: | Height: | Size: 353 B |
@ -239,6 +239,7 @@
|
||||
"SignInWithFacebook": "Sign in with Facebook",
|
||||
"SignInWithGoogle": "Sign in with Google",
|
||||
"SignInWithLinkedIn": "Sign in with LinkedIn",
|
||||
"SignInWithMicrosoft": "Sign in with Microsoft",
|
||||
"SignInWithSso": "Sign in with SSO",
|
||||
"SignInWithTwitter": "Sign in with Twitter",
|
||||
"Size": "Size",
|
||||
|
Loading…
Reference in New Issue
Block a user