mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-10 13:23:18 +00:00
stub out electron app
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -258,4 +258,7 @@ paket-files/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyc
|
||||
|
||||
build/
|
||||
dist/
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "jslib"]
|
||||
path = jslib
|
||||
url = https://github.com/bitwarden/jslib.git
|
||||
branch = master
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.25420.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-dc", ".", "{A4DE5293-DB47-41D1-8890-7C67B83F663C}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0"
|
||||
Debug.AspNetCompiler.VirtualPath = "/localhost_4405"
|
||||
Debug.AspNetCompiler.PhysicalPath = "."
|
||||
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\"
|
||||
Debug.AspNetCompiler.Updateable = "true"
|
||||
Debug.AspNetCompiler.ForceOverwrite = "true"
|
||||
Debug.AspNetCompiler.FixedNames = "false"
|
||||
Debug.AspNetCompiler.Debug = "True"
|
||||
Release.AspNetCompiler.VirtualPath = "/localhost_4405"
|
||||
Release.AspNetCompiler.PhysicalPath = "."
|
||||
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\"
|
||||
Release.AspNetCompiler.Updateable = "true"
|
||||
Release.AspNetCompiler.ForceOverwrite = "true"
|
||||
Release.AspNetCompiler.FixedNames = "false"
|
||||
Release.AspNetCompiler.Debug = "False"
|
||||
VWDPort = "4405"
|
||||
SlnRelativePath = "."
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.10
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "src\Console\Console.csproj", "{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{AE082484-A34C-4B3A-A69F-49E5EF298B27}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "src\Service\Service.csproj", "{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {52CB25C1-56A6-4FBE-977C-E842EA0AFAD7}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2009
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "src\Console\Console.csproj", "{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{AE082484-A34C-4B3A-A69F-49E5EF298B27}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "src\Service\Service.csproj", "{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}"
|
||||
EndProject
|
||||
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "src\Setup\Setup.vdproj", "{4D852DF8-9327-43D0-93AB-FA68D4F3414B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4D852DF8-9327-43D0-93AB-FA68D4F3414B}.Debug|Any CPU.ActiveCfg = Debug
|
||||
{4D852DF8-9327-43D0-93AB-FA68D4F3414B}.Release|Any CPU.ActiveCfg = Release
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {AB7EF6F7-C38B-4E66-BBF9-1F6915896CB0}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
1
jslib
Submodule
1
jslib
Submodule
Submodule jslib added at 5d3b99ce6f
74
package-lock.json
generated
74
package-lock.json
generated
@@ -955,6 +955,12 @@
|
||||
"isarray": "1.0.0"
|
||||
}
|
||||
},
|
||||
"buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@@ -2723,6 +2729,15 @@
|
||||
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
|
||||
"dev": true
|
||||
},
|
||||
"encoding": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"iconv-lite": "0.4.21"
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
@@ -3718,7 +3733,8 @@
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
@@ -4393,6 +4409,52 @@
|
||||
"retry-axios": "0.3.2"
|
||||
}
|
||||
},
|
||||
"google-fonts-webpack-plugin": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/google-fonts-webpack-plugin/-/google-fonts-webpack-plugin-0.4.4.tgz",
|
||||
"integrity": "sha512-+e2D9/DVBG9EDydRovzoqMZ658SsTBGbC0c65GyZqkwNvdj8vRSYQKXqbz7/yt7QaXsCPT1MpH45r3ivWOitcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "4.17.5",
|
||||
"node-fetch": "1.7.3",
|
||||
"webpack-sources": "0.2.3",
|
||||
"yauzl": "2.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-list-map": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-1.1.2.tgz",
|
||||
"integrity": "sha1-mIkBnRAkzOVc3AaUmDN+9hhqEaE=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
},
|
||||
"webpack-sources": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.2.3.tgz",
|
||||
"integrity": "sha1-F8Yr+vE8cH+dAsR54Nzd6DgGl/s=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-list-map": "1.1.2",
|
||||
"source-map": "0.5.7"
|
||||
}
|
||||
},
|
||||
"yauzl": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz",
|
||||
"integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-crc32": "0.2.13",
|
||||
"fd-slicer": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"google-p12-pem": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.2.tgz",
|
||||
@@ -5980,6 +6042,16 @@
|
||||
"semver": "5.5.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"encoding": "0.1.12",
|
||||
"is-stream": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"font-awesome": "4.7.0",
|
||||
"google-fonts-webpack-plugin": "^0.4.4",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"node-loader": "^0.6.0",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<RootNamespace>Bit.Console</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<RootNamespace>Bit.Core</RootNamespace>
|
||||
<AssemblyName>Core</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Apis.Admin.Directory.directory_v1" Version="1.31.0.1061" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="1.6.2" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.17.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
|
||||
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
|
||||
<Reference Include="System.DirectoryServices" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum DirectoryType : byte
|
||||
{
|
||||
ActiveDirectory = 0,
|
||||
AzureActiveDirectory = 1,
|
||||
Other = 2,
|
||||
GSuite = 3
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum OrganizationUserStatusType : byte
|
||||
{
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum OrganizationUserType : byte
|
||||
{
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum TwoFactorProviderType : byte
|
||||
{
|
||||
Authenticator = 0,
|
||||
Email = 1,
|
||||
Duo = 2,
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
[Flags]
|
||||
public enum UserAccountControl : int
|
||||
{
|
||||
AccountDisabled = 0x00000002,
|
||||
LockOut = 0x00000010,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ApiError
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ApiResult<T>
|
||||
{
|
||||
private List<ApiError> m_errors = new List<ApiError>();
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
public T Result { get; set; }
|
||||
public IEnumerable<ApiError> Errors => m_errors;
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
|
||||
public static ApiResult<T> Success(T result, HttpStatusCode statusCode)
|
||||
{
|
||||
return new ApiResult<T>
|
||||
{
|
||||
Succeeded = true,
|
||||
Result = result,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
public static ApiResult<T> Failed(HttpStatusCode statusCode, params ApiError[] errors)
|
||||
{
|
||||
var result = new ApiResult<T>
|
||||
{
|
||||
Succeeded = false,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
|
||||
if(errors != null)
|
||||
{
|
||||
result.m_errors.AddRange(errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiResult
|
||||
{
|
||||
private List<ApiError> m_errors = new List<ApiError>();
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
public IEnumerable<ApiError> Errors => m_errors;
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
|
||||
public static ApiResult Success(HttpStatusCode statusCode)
|
||||
{
|
||||
return new ApiResult
|
||||
{
|
||||
Succeeded = true,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
public static ApiResult Failed(HttpStatusCode statusCode, params ApiError[] errors)
|
||||
{
|
||||
var result = new ApiResult
|
||||
{
|
||||
Succeeded = false,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
|
||||
if(errors != null)
|
||||
{
|
||||
result.m_errors.AddRange(errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class AzureConfiguration
|
||||
{
|
||||
public string Tenant { get; set; } = "yourcompany.onmicrosoft.com";
|
||||
public string Id { get; set; }
|
||||
public EncryptedData Secret { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class EncryptedData
|
||||
{
|
||||
public EncryptedData() { }
|
||||
|
||||
public EncryptedData(byte[] plainValue)
|
||||
{
|
||||
IV = RandomBytes();
|
||||
#if NET461
|
||||
Value = ProtectedData.Protect(plainValue, IV, DataProtectionScope.LocalMachine);
|
||||
#endif
|
||||
}
|
||||
|
||||
public EncryptedData(string plainValue)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(plainValue);
|
||||
IV = RandomBytes();
|
||||
#if NET461
|
||||
Value = ProtectedData.Protect(bytes, IV, DataProtectionScope.LocalMachine);
|
||||
#endif
|
||||
}
|
||||
|
||||
public byte[] Value { get; set; }
|
||||
public byte[] IV { get; set; }
|
||||
|
||||
public byte[] Decrypt()
|
||||
{
|
||||
#if NET461
|
||||
return ProtectedData.Unprotect(Value, IV, DataProtectionScope.LocalMachine);
|
||||
#else
|
||||
return new byte[0];
|
||||
#endif
|
||||
}
|
||||
|
||||
public string DecryptToString()
|
||||
{
|
||||
#if NET461
|
||||
var bytes = ProtectedData.Unprotect(Value, IV, DataProtectionScope.LocalMachine);
|
||||
#else
|
||||
var bytes = new byte[0];
|
||||
#endif
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
private byte[] RandomBytes()
|
||||
{
|
||||
var entropy = new byte[16];
|
||||
new RNGCryptoServiceProvider().GetBytes(entropy);
|
||||
return entropy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public abstract class Entry
|
||||
{
|
||||
public string ReferenceId { get; set; }
|
||||
public string ExternalId { get; set; }
|
||||
public DateTime? CreationDate { get; set; }
|
||||
public DateTime? RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
public class GroupEntry : Entry
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public HashSet<string> UserMemberExternalIds { get; set; } = new HashSet<string>();
|
||||
public HashSet<string> GroupMemberReferenceIds { get; set; } = new HashSet<string>();
|
||||
}
|
||||
|
||||
public class UserEntry : Entry
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ErrorResponse
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }
|
||||
// For use in development environments.
|
||||
public string ExceptionMessage { get; set; }
|
||||
public string ExceptionStackTrace { get; set; }
|
||||
public string InnerExceptionMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class GSuiteConfiguration
|
||||
{
|
||||
public string SecretFile { get; set; } = "client_secret.json";
|
||||
public string Customer { get; set; }
|
||||
public string Domain { get; set; } = "yourcompany.com";
|
||||
public string AdminUser { get; set; } = "adminuser@yourcompany.com";
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Bit.Core.Services;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ImportRequest
|
||||
{
|
||||
public ImportRequest(List<GroupEntry> groups, List<UserEntry> users)
|
||||
{
|
||||
Groups = groups?.Select(g => new Group(g)).ToArray() ?? new Group[] { };
|
||||
Users = users?.Select(u => new User(u)).ToArray() ?? new User[] { };
|
||||
}
|
||||
|
||||
public Group[] Groups { get; set; }
|
||||
public User[] Users { get; set; }
|
||||
|
||||
public class Group
|
||||
{
|
||||
public Group(GroupEntry entry)
|
||||
{
|
||||
Name = entry.Name;
|
||||
ExternalId = entry.ExternalId;
|
||||
Users = entry.UserMemberExternalIds;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string ExternalId { get; set; }
|
||||
public IEnumerable<string> Users { get; set; }
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
public User(UserEntry entry)
|
||||
{
|
||||
Email = entry.Email;
|
||||
Deleted = (SettingsService.Instance.Sync.RemoveDisabledUsers && entry.Disabled) || entry.Deleted;
|
||||
ExternalId = entry.ExternalId;
|
||||
}
|
||||
|
||||
public string ExternalId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using Bit.Core.Services;
|
||||
using System;
|
||||
#if NET461
|
||||
using System.DirectoryServices;
|
||||
#endif
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class LdapConfiguration
|
||||
{
|
||||
public string Address { get; set; }
|
||||
public string Port { get; set; } = "389";
|
||||
public string Path { get; set; }
|
||||
public string Username { get; set; }
|
||||
public EncryptedData Password { get; set; }
|
||||
public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory;
|
||||
|
||||
#if NET461
|
||||
public DirectoryEntry GetUserDirectoryEntry()
|
||||
{
|
||||
return GetPathedDirectoryEntry(SettingsService.Instance.Sync.Ldap.UserPath);
|
||||
}
|
||||
|
||||
public DirectoryEntry GetGroupDirectoryEntry()
|
||||
{
|
||||
return GetPathedDirectoryEntry(SettingsService.Instance.Sync.Ldap.GroupPath);
|
||||
}
|
||||
|
||||
public DirectoryEntry GetPathedDirectoryEntry(string pathPrefix = null)
|
||||
{
|
||||
var path = Path;
|
||||
if(!string.IsNullOrWhiteSpace(pathPrefix))
|
||||
{
|
||||
path = string.Concat(pathPrefix, ",", path);
|
||||
}
|
||||
|
||||
return GetDirectoryEntry(path);
|
||||
}
|
||||
|
||||
public DirectoryEntry GetBasePathDirectoryEntry()
|
||||
{
|
||||
var path = Path.Substring(Path.IndexOf("dc=", StringComparison.InvariantCultureIgnoreCase));
|
||||
return GetDirectoryEntry(path);
|
||||
}
|
||||
|
||||
public DirectoryEntry GetDirectoryEntry(string path = null)
|
||||
{
|
||||
if(Password == null && string.IsNullOrWhiteSpace(Username))
|
||||
{
|
||||
return new DirectoryEntry(ServerPath(path));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DirectoryEntry(ServerPath(path), Username, Password.DecryptToString(), AuthenticationTypes.None);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private string ServerPath(string path)
|
||||
{
|
||||
return $"LDAP://{Address}:{Port}/{path}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class LoginResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0;
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public List<Organization> Organizations { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class Organization
|
||||
{
|
||||
public Organization() { }
|
||||
|
||||
public Organization(ProfileOrganizationResponseModel org)
|
||||
{
|
||||
Name = org.Name;
|
||||
Id = org.Id;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ProfileOrganizationResponseModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Key { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ProfileResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ServerConfiguration
|
||||
{
|
||||
public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory;
|
||||
public LdapConfiguration Ldap { get; set; }
|
||||
public AzureConfiguration Azure { get; set; }
|
||||
public GSuiteConfiguration GSuite { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class SyncConfiguration
|
||||
{
|
||||
public SyncConfiguration() { }
|
||||
|
||||
public SyncConfiguration(DirectoryType type)
|
||||
{
|
||||
Ldap = new LdapSyncConfiguration(type);
|
||||
|
||||
switch(type)
|
||||
{
|
||||
case DirectoryType.ActiveDirectory:
|
||||
break;
|
||||
case DirectoryType.AzureActiveDirectory:
|
||||
break;
|
||||
case DirectoryType.Other:
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Depending on what server type you are using, filters are be one of the following:
|
||||
*
|
||||
* 1. ActiveDirectory or Other
|
||||
* - LDAP query/filter syntax
|
||||
* - Read more at: http://bit.ly/2qyLpzW
|
||||
* - ex. "(&(givenName=John)(|(l=Dallas)(l=Austin)))"
|
||||
*
|
||||
* 2. AzureActiveDirectory
|
||||
* - OData syntax for a Microsoft Graph query parameter '$filter'
|
||||
* - Read more at http://bit.ly/2q3FOOD
|
||||
* - ex. "startswith(displayName,'J')"
|
||||
*
|
||||
* 3. G Suite
|
||||
* - Group Filter
|
||||
* - Custom filtering syntax that allows you to exclude or include a comma separated list of group names.
|
||||
* - ex. "include:Group A,Sales People,My Other Group"
|
||||
* or "exclude:Group C,Developers,Some Other Group"
|
||||
* - User Filter
|
||||
* - Custom filtering syntax that allows you to exclude or include a comma separated list of group names.
|
||||
* - Allows you to concatenate a G Suite Admin API user search query to the end of the filter after delimiting
|
||||
* the include/exclude filter with a pipe (|).
|
||||
* - Read more at http://bit.ly/2rlTskX
|
||||
* - ex.
|
||||
* or "include:joe@company.com,bill@company.com,tom@company.com"
|
||||
* or "exclude:john@company.com,bill@company.com|orgName=Engineering orgTitle:Manager"
|
||||
* or "|orgName=Engineering orgTitle:Manager"
|
||||
*/
|
||||
|
||||
public string GroupFilter { get; set; }
|
||||
public string UserFilter { get; set; }
|
||||
|
||||
public bool SyncGroups { get; set; } = true;
|
||||
public bool SyncUsers { get; set; } = true;
|
||||
public int IntervalMinutes { get; set; } = 5;
|
||||
public bool RemoveDisabledUsers { get; set; }
|
||||
public LdapSyncConfiguration Ldap { get; set; } = new LdapSyncConfiguration();
|
||||
|
||||
public class LdapSyncConfiguration
|
||||
{
|
||||
public LdapSyncConfiguration() { }
|
||||
|
||||
public LdapSyncConfiguration(DirectoryType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case DirectoryType.ActiveDirectory:
|
||||
CreationDateAttribute = "whenCreated";
|
||||
RevisionDateAttribute = "whenChanged";
|
||||
UserEmailPrefixAttribute = "sAMAccountName";
|
||||
UserPath = "CN=Users";
|
||||
GroupPath = "CN=Users";
|
||||
break;
|
||||
case DirectoryType.Other:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public string UserPath { get; set; }
|
||||
public string GroupPath { get; set; }
|
||||
public string UserObjectClass { get; set; } = "person";
|
||||
public string GroupObjectClass { get; set; } = "group";
|
||||
public string MemberAttribute { get; set; } = "member";
|
||||
public string GroupNameAttribute { get; set; } = "name";
|
||||
public string UserEmailAttribute { get; set; } = "mail";
|
||||
public bool EmailPrefixSuffix { get; set; } = false;
|
||||
public string UserEmailPrefixAttribute { get; set; } = "cn";
|
||||
public string UserEmailSuffix { get; set; } = "@companyname.com";
|
||||
public string CreationDateAttribute { get; set; }
|
||||
public string RevisionDateAttribute { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class SyncResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public List<GroupEntry> Groups { get; set; } = new List<GroupEntry>();
|
||||
public List<UserEntry> Users { get; set; } = new List<UserEntry>();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class TokenRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Token { get; set; }
|
||||
public TwoFactorProviderType? Provider { get; set; }
|
||||
public bool Remember { get; set; }
|
||||
|
||||
public IDictionary<string, string> ToIdentityTokenRequest()
|
||||
{
|
||||
var dict = new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "password" },
|
||||
{ "username", Email },
|
||||
{ "password", MasterPasswordHash },
|
||||
{ "scope", "api offline_access" },
|
||||
{ "client_id", "connector" }
|
||||
};
|
||||
|
||||
if(Token != null && Provider.HasValue)
|
||||
{
|
||||
dict.Add("TwoFactorToken", Token);
|
||||
dict.Add("TwoFactorProvider", ((byte)(Provider.Value)).ToString());
|
||||
dict.Add("TwoFactorRemember", Remember ? "1" : "0");
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class TokenResponse
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
[JsonProperty("expires_in")]
|
||||
public long ExpiresIn { get; set; }
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string TwoFactorToken { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class TwoFactorEmailRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class ApiService
|
||||
{
|
||||
private static ApiService _instance;
|
||||
|
||||
private ApiService()
|
||||
{
|
||||
Client = new HttpClient();
|
||||
}
|
||||
|
||||
public static ApiService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new ApiService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpClient Client { get; private set; }
|
||||
|
||||
public virtual async Task<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj)
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(string.Concat(SettingsService.Instance.IdentityBaseUrl, "/connect/token")),
|
||||
Content = new FormUrlEncodedContent(requestObj.ToIdentityTokenRequest())
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorResponse = JObject.Parse(responseContent);
|
||||
if(errorResponse["TwoFactorProviders2"] != null)
|
||||
{
|
||||
return ApiResult<TokenResponse>.Success(new TokenResponse
|
||||
{
|
||||
TwoFactorProviders2 = errorResponse["TwoFactorProviders2"]
|
||||
.ToObject<Dictionary<TwoFactorProviderType, Dictionary<string, object>>>()
|
||||
}, response.StatusCode);
|
||||
}
|
||||
|
||||
return await HandleErrorAsync<TokenResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<TokenResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult> PostImportAsync(ImportRequest requestObj)
|
||||
{
|
||||
var tokenStateResponse = await HandleTokenStateAsync();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
var stringContent = JsonConvert.SerializeObject(requestObj);
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/organizations/",
|
||||
SettingsService.Instance.Organization.Id, "/import")),
|
||||
Content = new StringContent(stringContent, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
|
||||
requestMessage.Headers.Add("Authorization", $"Bearer {TokenService.Instance.AccessToken}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return ApiResult.Success(response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<ProfileResponse>> GetProfileAsync()
|
||||
{
|
||||
var tokenStateResponse = await HandleTokenStateAsync<ProfileResponse>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
var requestMessage = new HttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/accounts/profile")),
|
||||
};
|
||||
|
||||
requestMessage.Headers.Add("Authorization", $"Bearer {TokenService.Instance.AccessToken}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<ProfileResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<ProfileResponse>(responseContent);
|
||||
return ApiResult<ProfileResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<ProfileResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult> PostTwoFactorSendEmailLoginAsync(TwoFactorEmailRequest requestObj)
|
||||
{
|
||||
var stringContent = JsonConvert.SerializeObject(requestObj);
|
||||
|
||||
var requestMessage = new HttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/two-factor/send-email-login")),
|
||||
Content = new StringContent(stringContent, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return ApiResult.Success(response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException();
|
||||
}
|
||||
}
|
||||
|
||||
protected ApiResult HandledWebException()
|
||||
{
|
||||
return ApiResult.Failed(HttpStatusCode.BadGateway,
|
||||
new ApiError { Message = "There is a problem connecting to the server." });
|
||||
}
|
||||
|
||||
protected ApiResult<T> HandledWebException<T>()
|
||||
{
|
||||
return ApiResult<T>.Failed(HttpStatusCode.BadGateway,
|
||||
new ApiError { Message = "There is a problem connecting to the server." });
|
||||
}
|
||||
|
||||
protected async Task<ApiResult> HandleTokenStateAsync()
|
||||
{
|
||||
return await HandleTokenStateAsync(
|
||||
() => ApiResult.Success(HttpStatusCode.OK),
|
||||
() => HandledWebException(),
|
||||
(r) => HandleErrorAsync(r));
|
||||
}
|
||||
|
||||
protected async Task<ApiResult<T>> HandleTokenStateAsync<T>()
|
||||
{
|
||||
return await HandleTokenStateAsync(
|
||||
() => ApiResult<T>.Success(default(T), HttpStatusCode.OK),
|
||||
() => HandledWebException<T>(),
|
||||
(r) => HandleErrorAsync<T>(r));
|
||||
}
|
||||
|
||||
private async Task<T> HandleTokenStateAsync<T>(Func<T> success, Func<T> webException,
|
||||
Func<HttpResponseMessage, Task<T>> error)
|
||||
{
|
||||
if(TokenService.Instance.AccessTokenNeedsRefresh && !string.IsNullOrWhiteSpace(TokenService.Instance.RefreshToken))
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(string.Concat(SettingsService.Instance.IdentityBaseUrl, "/connect/token")),
|
||||
Content = new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "client_id", "connector" },
|
||||
{ "refresh_token", TokenService.Instance.RefreshToken }
|
||||
})
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
if(response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
response.StatusCode = HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
return await error.Invoke(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
TokenService.Instance.AccessToken = tokenResponse.AccessToken;
|
||||
TokenService.Instance.RefreshToken = tokenResponse.RefreshToken;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return webException.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
return success.Invoke();
|
||||
}
|
||||
|
||||
protected async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
|
||||
return ApiResult<T>.Failed(response.StatusCode, errors.ToArray());
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
return ApiResult<T>.Failed(response.StatusCode,
|
||||
new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
protected async Task<ApiResult> HandleErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
|
||||
return ApiResult.Failed(response.StatusCode, errors.ToArray());
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
return ApiResult.Failed(response.StatusCode,
|
||||
new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
private async Task<List<ApiError>> ParseErrorsAsync(HttpResponseMessage response)
|
||||
{
|
||||
var errors = new List<ApiError>();
|
||||
var statusCode = (int)response.StatusCode;
|
||||
if(statusCode >= 400 && statusCode <= 500)
|
||||
{
|
||||
ErrorResponse errorResponseModel = null;
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
if(!string.IsNullOrWhiteSpace(responseContent))
|
||||
{
|
||||
var errorResponse = JObject.Parse(responseContent);
|
||||
if(errorResponse["ErrorModel"] != null && errorResponse["ErrorModel"]["Message"] != null)
|
||||
{
|
||||
errorResponseModel = errorResponse["ErrorModel"].ToObject<ErrorResponse>();
|
||||
}
|
||||
else if(errorResponse["Message"] != null)
|
||||
{
|
||||
errorResponseModel = errorResponse.ToObject<ErrorResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
if(errorResponseModel != null)
|
||||
{
|
||||
if((errorResponseModel.ValidationErrors?.Count ?? 0) > 0)
|
||||
{
|
||||
foreach(var valError in errorResponseModel.ValidationErrors)
|
||||
{
|
||||
foreach(var errorMessage in valError.Value)
|
||||
{
|
||||
errors.Add(new ApiError { Message = errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new ApiError { Message = errorResponseModel.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(errors.Count == 0)
|
||||
{
|
||||
errors.Add(new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AuthService
|
||||
{
|
||||
private static AuthService _instance;
|
||||
|
||||
private AuthService() { }
|
||||
|
||||
public static AuthService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new AuthService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Authenticated => !string.IsNullOrWhiteSpace(TokenService.Instance.AccessToken);
|
||||
public bool OrganizationSet => SettingsService.Instance.Organization != null;
|
||||
|
||||
public void LogOut()
|
||||
{
|
||||
TokenService.Instance.AccessToken = null;
|
||||
TokenService.Instance.RefreshToken = null;
|
||||
}
|
||||
|
||||
public async Task<LoginResult> LogInAsync(string email, string masterPassword)
|
||||
{
|
||||
var normalizedEmail = email.Trim().ToLower();
|
||||
var key = Crypto.MakeKeyFromPassword(masterPassword, normalizedEmail);
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = normalizedEmail,
|
||||
MasterPasswordHash = Crypto.HashPasswordBase64(key, masterPassword)
|
||||
};
|
||||
|
||||
var response = await ApiService.Instance.PostTokenAsync(request);
|
||||
|
||||
masterPassword = null;
|
||||
key = null;
|
||||
|
||||
var result = new LoginResult();
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
if(response.Result.TwoFactorProviders2 != null && response.Result.TwoFactorProviders2.Count > 0)
|
||||
{
|
||||
result.TwoFactorProviders = response.Result.TwoFactorProviders2;
|
||||
result.MasterPasswordHash = request.MasterPasswordHash;
|
||||
return result;
|
||||
}
|
||||
|
||||
return await ProcessLogInSuccessAsync(response.Result);
|
||||
}
|
||||
|
||||
public async Task<LoginResult> LogInTwoFactorAsync(TwoFactorProviderType type, string token, string email,
|
||||
string masterPassword)
|
||||
{
|
||||
var normalizedEmail = email.Trim().ToLower();
|
||||
var key = Crypto.MakeKeyFromPassword(masterPassword, normalizedEmail);
|
||||
|
||||
var result = await LogInTwoFactorWithHashAsync(type, token, email, Crypto.HashPasswordBase64(key, masterPassword));
|
||||
|
||||
key = null;
|
||||
masterPassword = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LoginResult> LogInTwoFactorWithHashAsync(TwoFactorProviderType type, string token, string email,
|
||||
string masterPasswordHash)
|
||||
{
|
||||
if(type == TwoFactorProviderType.Email || type == TwoFactorProviderType.Authenticator)
|
||||
{
|
||||
token = token.Trim().Replace(" ", "");
|
||||
}
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = email.Trim().ToLower(),
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Token = token,
|
||||
Provider = type,
|
||||
Remember = false
|
||||
};
|
||||
|
||||
var response = await ApiService.Instance.PostTokenAsync(request);
|
||||
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
var result = new LoginResult();
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
return await ProcessLogInSuccessAsync(response.Result);
|
||||
}
|
||||
|
||||
private async Task<LoginResult> ProcessLogInSuccessAsync(TokenResponse response)
|
||||
{
|
||||
TokenService.Instance.AccessToken = response.AccessToken;
|
||||
TokenService.Instance.RefreshToken = response.RefreshToken;
|
||||
|
||||
var result = new LoginResult();
|
||||
|
||||
var profile = await ApiService.Instance.GetProfileAsync();
|
||||
if(profile.Succeeded)
|
||||
{
|
||||
var adminOrgs = profile.Result.Organizations.Where(o =>
|
||||
o.Status == OrganizationUserStatusType.Confirmed &&
|
||||
o.Type != OrganizationUserType.User);
|
||||
if(!adminOrgs.Any())
|
||||
{
|
||||
LogOut();
|
||||
result.Success = false;
|
||||
result.ErrorMessage = "You are not an admin of any organizations.";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Organizations = adminOrgs.Select(o => new Organization(o)).ToList();
|
||||
if(result.Organizations.Count == 1)
|
||||
{
|
||||
SettingsService.Instance.Organization = new Organization(adminOrgs.First());
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogOut();
|
||||
result.Success = false;
|
||||
result.ErrorMessage = "Could not load profile.";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Graph;
|
||||
using System.Linq;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureDirectoryService : IDirectoryService
|
||||
{
|
||||
private static AzureDirectoryService _instance;
|
||||
private static GraphServiceClient _graphClient;
|
||||
|
||||
private AzureDirectoryService()
|
||||
{
|
||||
_graphClient = new GraphServiceClient(new AzureAuthenticationProvider());
|
||||
}
|
||||
|
||||
public static IDirectoryService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new AzureDirectoryService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
|
||||
{
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
{
|
||||
throw new ApplicationException("Not logged in or have an org set.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Azure == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
List<UserEntry> users = null;
|
||||
if(SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
users = await GetUsersAsync(force);
|
||||
}
|
||||
|
||||
List<GroupEntry> groups = null;
|
||||
if(SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.GroupFilter);
|
||||
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false), filter);
|
||||
|
||||
if(filter != null && users != null)
|
||||
{
|
||||
users = users.Where(u => u.Disabled || u.Deleted ||
|
||||
groups.Any(g => g.UserMemberExternalIds.Contains(u.ExternalId))).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
|
||||
}
|
||||
|
||||
private async static Task<List<GroupEntry>> GetGroupsAsync(bool force, Tuple<bool, HashSet<string>> filter)
|
||||
{
|
||||
if(!SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
throw new ApplicationException("Not configured to sync groups.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Azure == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
if(!AuthService.Instance.Authenticated)
|
||||
{
|
||||
throw new ApplicationException("Not authenticated.");
|
||||
}
|
||||
|
||||
var entries = new List<GroupEntry>();
|
||||
var changedGroupIds = new List<string>();
|
||||
var getFullResults = SettingsService.Instance.GroupDeltaToken == null || force;
|
||||
|
||||
try
|
||||
{
|
||||
var delataRequest = _graphClient.Groups.Delta().Request();
|
||||
if(!getFullResults)
|
||||
{
|
||||
delataRequest.QueryOptions.Add(new QueryOption("$deltatoken", SettingsService.Instance.GroupDeltaToken));
|
||||
}
|
||||
|
||||
var groupsDelta = await delataRequest.GetAsync();
|
||||
while(true)
|
||||
{
|
||||
if(getFullResults)
|
||||
{
|
||||
foreach(var group in groupsDelta)
|
||||
{
|
||||
if(FilterOutResult(filter, group.DisplayName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = await BuildGroupAsync(group);
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
changedGroupIds.AddRange(groupsDelta.Select(g => g.Id));
|
||||
}
|
||||
|
||||
if(groupsDelta.NextPageRequest == null)
|
||||
{
|
||||
object deltaLink;
|
||||
if(groupsDelta.AdditionalData.TryGetValue("@odata.deltaLink", out deltaLink))
|
||||
{
|
||||
var deltaUriQuery = new Uri(deltaLink.ToString()).ParseQueryString();
|
||||
if(deltaUriQuery["$deltatoken"] != null)
|
||||
{
|
||||
SettingsService.Instance.GroupDeltaToken = deltaUriQuery["$deltatoken"];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupsDelta = await groupsDelta.NextPageRequest.GetAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if(getFullResults || !changedGroupIds.Any())
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
var groups = await _graphClient.Groups.Request().GetAsync();
|
||||
while(true)
|
||||
{
|
||||
foreach(var group in groups)
|
||||
{
|
||||
if(FilterOutResult(filter, group.DisplayName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = await BuildGroupAsync(group);
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
if(groups.NextPageRequest == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
groups = await groups.NextPageRequest.GetAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async static Task<GroupEntry> BuildGroupAsync(Group group)
|
||||
{
|
||||
var entry = new GroupEntry
|
||||
{
|
||||
ReferenceId = group.Id,
|
||||
ExternalId = group.Id,
|
||||
Name = group.DisplayName
|
||||
};
|
||||
|
||||
var members = await _graphClient.Groups[group.Id].Members.Request().Select("id").GetAsync();
|
||||
foreach(var member in members)
|
||||
{
|
||||
if(member is User)
|
||||
{
|
||||
entry.UserMemberExternalIds.Add(member.Id);
|
||||
}
|
||||
else if(member is Group)
|
||||
{
|
||||
entry.GroupMemberReferenceIds.Add(member.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async static Task<List<UserEntry>> GetUsersAsync(bool force)
|
||||
{
|
||||
if(!SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
throw new ApplicationException("Not configured to sync users.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Azure == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
if(!AuthService.Instance.Authenticated)
|
||||
{
|
||||
throw new ApplicationException("Not authenticated.");
|
||||
}
|
||||
|
||||
var entries = new List<UserEntry>();
|
||||
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.UserFilter);
|
||||
|
||||
var userRequest = _graphClient.Users.Delta();
|
||||
IUserDeltaCollectionPage users = null;
|
||||
|
||||
if(!force && SettingsService.Instance.UserDeltaToken != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var delataRequest = userRequest.Request();
|
||||
delataRequest.QueryOptions.Add(new QueryOption("$deltatoken", SettingsService.Instance.UserDeltaToken));
|
||||
users = await delataRequest.GetAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
users = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(users == null)
|
||||
{
|
||||
users = await userRequest.Request().GetAsync();
|
||||
}
|
||||
|
||||
while(true)
|
||||
{
|
||||
foreach(var user in users)
|
||||
{
|
||||
var entry = new UserEntry
|
||||
{
|
||||
ReferenceId = user.Id,
|
||||
ExternalId = user.Id,
|
||||
Email = user.Mail ?? user.UserPrincipalName,
|
||||
Disabled = !user.AccountEnabled.GetValueOrDefault(true)
|
||||
};
|
||||
|
||||
if(FilterOutResult(filter, entry.Email))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(user.AdditionalData.TryGetValue("@removed", out object deleted) && deleted.ToString().Contains("changed"))
|
||||
{
|
||||
entry.Deleted = true;
|
||||
}
|
||||
else if(!entry.Disabled && (entry?.Email?.Contains("#") ?? true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
if(users.NextPageRequest == null)
|
||||
{
|
||||
if(users.AdditionalData.TryGetValue("@odata.deltaLink", out object deltaLink))
|
||||
{
|
||||
var deltaUriQuery = new Uri(deltaLink.ToString()).ParseQueryString();
|
||||
if(deltaUriQuery["$deltatoken"] != null)
|
||||
{
|
||||
SettingsService.Instance.UserDeltaToken = deltaUriQuery["$deltatoken"];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
users = await users.NextPageRequest.GetAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static Tuple<bool, HashSet<string>> CreateSetFromFilter(string filter)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = filter.Split(':');
|
||||
if(parts.Length != 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var exclude = true;
|
||||
if(string.Equals(parts[0].Trim(), "include", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
exclude = false;
|
||||
}
|
||||
else if(string.Equals(parts[0].Trim(), "exclude", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
exclude = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new HashSet<string>(parts[1].Split(',').Select(p => p.Trim()));
|
||||
return new Tuple<bool, HashSet<string>>(exclude, list);
|
||||
}
|
||||
|
||||
private static bool FilterOutResult(Tuple<bool, HashSet<string>> filter, string result)
|
||||
{
|
||||
if(filter != null)
|
||||
{
|
||||
// excluded
|
||||
if(filter.Item1 && filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// included
|
||||
else if(!filter.Item1 && !filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Bit.Core.Utilities;
|
||||
#if NET461
|
||||
using System.ServiceProcess;
|
||||
#endif
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class ControllerService
|
||||
{
|
||||
private static ControllerService _instance;
|
||||
|
||||
private ControllerService()
|
||||
{
|
||||
#if NET461
|
||||
Controller = new ServiceController(Constants.ProgramName);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static ControllerService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new ControllerService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#if NET461
|
||||
public ServiceController Controller { get; private set; }
|
||||
public ServiceControllerStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
Controller.Refresh();
|
||||
return Controller.Status;
|
||||
}
|
||||
}
|
||||
public bool Running => Status == ServiceControllerStatus.Running;
|
||||
public bool Paused => Status == ServiceControllerStatus.Paused;
|
||||
public bool Stopped => Status == ServiceControllerStatus.Stopped;
|
||||
public bool Pending =>
|
||||
Status == ServiceControllerStatus.ContinuePending ||
|
||||
Status == ServiceControllerStatus.PausePending ||
|
||||
Status == ServiceControllerStatus.StartPending ||
|
||||
Status == ServiceControllerStatus.StopPending;
|
||||
#endif
|
||||
public string StatusString
|
||||
{
|
||||
get
|
||||
{
|
||||
#if NET461
|
||||
return Controller == null ? "Unavailable" : Status.ToString();
|
||||
#else
|
||||
return "Unavailable";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
#if NET461
|
||||
if(Controller == null || !Stopped)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Controller.Start();
|
||||
return true;
|
||||
#else
|
||||
throw new System.Exception("Controller unavailable.");
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool Stop()
|
||||
{
|
||||
#if NET461
|
||||
if(Controller == null || !Controller.CanStop)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Controller.Stop();
|
||||
|
||||
return true;
|
||||
#else
|
||||
throw new System.Exception("Controller unavailable.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Google.Apis.Admin.Directory.directory_v1;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using System.IO;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Linq;
|
||||
using Google.Apis.Admin.Directory.directory_v1.Data;
|
||||
using Google.Apis.Requests;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class GSuiteDirectoryService : IDirectoryService
|
||||
{
|
||||
private static GSuiteDirectoryService _instance;
|
||||
private static DirectoryService _service;
|
||||
|
||||
private GSuiteDirectoryService()
|
||||
{
|
||||
ICredential creds;
|
||||
|
||||
var secretFilePath = Path.Combine(Constants.BaseStoragePath, SettingsService.Instance.Server.GSuite.SecretFile);
|
||||
using(var stream = new FileStream(secretFilePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
var scopes = new List<string>
|
||||
{
|
||||
DirectoryService.Scope.AdminDirectoryUserReadonly,
|
||||
DirectoryService.Scope.AdminDirectoryGroupReadonly,
|
||||
DirectoryService.Scope.AdminDirectoryGroupMemberReadonly
|
||||
};
|
||||
|
||||
creds = GoogleCredential.FromStream(stream)
|
||||
.CreateScoped(scopes)
|
||||
.CreateWithUser(SettingsService.Instance.Server.GSuite.AdminUser);
|
||||
}
|
||||
|
||||
_service = new DirectoryService(new BaseClientService.Initializer
|
||||
{
|
||||
HttpClientInitializer = creds,
|
||||
ApplicationName = Constants.ProgramName
|
||||
});
|
||||
}
|
||||
|
||||
public static IDirectoryService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new GSuiteDirectoryService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
|
||||
{
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
{
|
||||
throw new ApplicationException("Not logged in or have an org set.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.GSuite == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
List<UserEntry> users = null;
|
||||
if(SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
users = await GetUsersAsync(force);
|
||||
}
|
||||
|
||||
List<GroupEntry> groups = null;
|
||||
if(SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false));
|
||||
}
|
||||
|
||||
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
|
||||
}
|
||||
|
||||
private Task<List<GroupEntry>> GetGroupsAsync(bool force)
|
||||
{
|
||||
var entries = new List<GroupEntry>();
|
||||
|
||||
var request = _service.Groups.List();
|
||||
request.Domain = SettingsService.Instance.Server.GSuite.Domain;
|
||||
request.Customer = SettingsService.Instance.Server.GSuite.Customer;
|
||||
|
||||
var pageStreamer = new PageStreamer<Group, GroupsResource.ListRequest, Groups, string>(
|
||||
(req, token) => req.PageToken = token,
|
||||
res => res.NextPageToken,
|
||||
res => res.GroupsValue);
|
||||
|
||||
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.GroupFilter);
|
||||
foreach(var group in pageStreamer.Fetch(request))
|
||||
{
|
||||
if(FilterOutResult(filter, group.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = BuildGroup(group);
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
return Task.FromResult(entries);
|
||||
}
|
||||
|
||||
private static GroupEntry BuildGroup(Group group)
|
||||
{
|
||||
var entry = new GroupEntry
|
||||
{
|
||||
ReferenceId = group.Id,
|
||||
ExternalId = group.Id,
|
||||
Name = group.Name
|
||||
};
|
||||
|
||||
var memberRequest = _service.Members.List(group.Id);
|
||||
var pageStreamer = new PageStreamer<Member, MembersResource.ListRequest, Members, string>(
|
||||
(req, token) => req.PageToken = token,
|
||||
res => res.NextPageToken,
|
||||
res => res.MembersValue);
|
||||
|
||||
foreach(var member in pageStreamer.Fetch(memberRequest))
|
||||
{
|
||||
if(!member.Role.Equals("member", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
!member.Status.Equals("active", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(member.Type.Equals("user", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
entry.UserMemberExternalIds.Add(member.Id);
|
||||
}
|
||||
else if(member.Type.Equals("group", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
entry.GroupMemberReferenceIds.Add(member.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private Task<List<UserEntry>> GetUsersAsync(bool force)
|
||||
{
|
||||
var entries = new List<UserEntry>();
|
||||
var query = CreateGSuiteQueryFromFilter(SettingsService.Instance.Sync.UserFilter);
|
||||
|
||||
var request = _service.Users.List();
|
||||
request.Domain = SettingsService.Instance.Server.GSuite.Domain;
|
||||
request.Customer = SettingsService.Instance.Server.GSuite.Customer;
|
||||
request.Query = query;
|
||||
|
||||
var pageStreamer = new PageStreamer<User, UsersResource.ListRequest, Users, string>(
|
||||
(req, token) => req.PageToken = token,
|
||||
res => res.NextPageToken,
|
||||
res => res.UsersValue);
|
||||
|
||||
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.UserFilter);
|
||||
foreach(var user in pageStreamer.Fetch(request))
|
||||
{
|
||||
if(FilterOutResult(filter, user.PrimaryEmail))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = BuildUser(user, false);
|
||||
if(entry != null)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
var deletedRequest = _service.Users.List();
|
||||
deletedRequest.Domain = SettingsService.Instance.Server.GSuite.Domain;
|
||||
deletedRequest.Customer = SettingsService.Instance.Server.GSuite.Customer;
|
||||
deletedRequest.Query = query;
|
||||
deletedRequest.ShowDeleted = "true";
|
||||
|
||||
var deletedPageStreamer = new PageStreamer<User, UsersResource.ListRequest, Users, string>(
|
||||
(req, token) => req.PageToken = token,
|
||||
res => res.NextPageToken,
|
||||
res => res.UsersValue);
|
||||
|
||||
foreach(var user in deletedPageStreamer.Fetch(deletedRequest))
|
||||
{
|
||||
if(FilterOutResult(filter, user.PrimaryEmail))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = BuildUser(user, true);
|
||||
if(entry != null)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(entries);
|
||||
}
|
||||
|
||||
private UserEntry BuildUser(User user, bool deleted)
|
||||
{
|
||||
var entry = new UserEntry
|
||||
{
|
||||
ReferenceId = user.Id,
|
||||
ExternalId = user.Id,
|
||||
Email = user.PrimaryEmail,
|
||||
Disabled = user.Suspended.GetValueOrDefault(false),
|
||||
Deleted = deleted,
|
||||
CreationDate = user.CreationTime
|
||||
};
|
||||
|
||||
if(string.IsNullOrWhiteSpace(entry.Email) && !entry.Deleted)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private string CreateGSuiteQueryFromFilter(string filter)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var mainParts = filter.Split('|');
|
||||
if(mainParts.Count() < 2 || string.IsNullOrWhiteSpace(mainParts[1]))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return mainParts[1].Trim();
|
||||
}
|
||||
|
||||
private Tuple<bool, HashSet<string>> CreateSetFromFilter(string filter)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var mainParts = filter.Split('|');
|
||||
if(mainParts.Count() < 1 || string.IsNullOrWhiteSpace(mainParts[0]))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = mainParts[0].Split(':');
|
||||
if(parts.Count() != 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var exclude = true;
|
||||
if(string.Equals(parts[0].Trim(), "include", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
exclude = false;
|
||||
}
|
||||
else if(string.Equals(parts[0].Trim(), "exclude", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
exclude = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new HashSet<string>(parts[1].Split(',').Select(p => p.Trim()));
|
||||
return new Tuple<bool, HashSet<string>>(exclude, list);
|
||||
}
|
||||
|
||||
private bool FilterOutResult(Tuple<bool, HashSet<string>> filter, string result)
|
||||
{
|
||||
if(filter != null)
|
||||
{
|
||||
// excluded
|
||||
if(filter.Item1 && filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// included
|
||||
else if(!filter.Item1 && !filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IDirectoryService
|
||||
{
|
||||
Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false);
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
#if NET461
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.DirectoryServices;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class LdapDirectoryService : IDirectoryService
|
||||
{
|
||||
private static LdapDirectoryService _instance;
|
||||
|
||||
private LdapDirectoryService() { }
|
||||
|
||||
public static IDirectoryService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new LdapDirectoryService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
|
||||
{
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
{
|
||||
throw new ApplicationException("Not logged in or have an org set.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Ldap == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
List<UserEntry> users = null;
|
||||
if(SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
users = await GetUsersAsync(force);
|
||||
}
|
||||
|
||||
List<GroupEntry> groups = null;
|
||||
if(SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false));
|
||||
}
|
||||
|
||||
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
|
||||
}
|
||||
|
||||
private static Task<List<GroupEntry>> GetGroupsAsync(bool force = false)
|
||||
{
|
||||
if(!SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
throw new ApplicationException("Not configured to sync groups.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Ldap == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
if(!AuthService.Instance.Authenticated)
|
||||
{
|
||||
throw new ApplicationException("Not authenticated.");
|
||||
}
|
||||
|
||||
var groupEntry = SettingsService.Instance.Server.Ldap.GetGroupDirectoryEntry();
|
||||
|
||||
var originalFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.GroupObjectClass,
|
||||
SettingsService.Instance.Sync.GroupFilter);
|
||||
|
||||
var filter = originalFilter;
|
||||
var revisionFilter = BuildRevisionFilter(filter, force, SettingsService.Instance.LastGroupSyncDate);
|
||||
var searchSinceRevision = filter != revisionFilter;
|
||||
filter = revisionFilter;
|
||||
|
||||
Console.WriteLine("Group search: {0} => {1}", groupEntry.Path, filter);
|
||||
var searcher = new DirectorySearcher(groupEntry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var initialSearchGroupIds = new List<string>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
initialSearchGroupIds.Add(DNFromPath(item.Path));
|
||||
}
|
||||
|
||||
if(searchSinceRevision && !initialSearchGroupIds.Any())
|
||||
{
|
||||
return Task.FromResult(new List<GroupEntry>());
|
||||
}
|
||||
else if(searchSinceRevision)
|
||||
{
|
||||
searcher = new DirectorySearcher(groupEntry, originalFilter);
|
||||
result = searcher.FindAll();
|
||||
}
|
||||
|
||||
var userEntry = SettingsService.Instance.Server.Ldap.GetUserDirectoryEntry();
|
||||
var userFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass,
|
||||
SettingsService.Instance.Sync.UserFilter);
|
||||
var userSearcher = new DirectorySearcher(userEntry, userFilter);
|
||||
var userResult = userSearcher.FindAll();
|
||||
|
||||
var userIdsDict = MakeIdIndex(userResult);
|
||||
|
||||
var groups = new List<GroupEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var group = BuildGroup(item, userIdsDict);
|
||||
if(group == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.Add(group);
|
||||
}
|
||||
|
||||
return Task.FromResult(groups);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> MakeIdIndex(SearchResultCollection result)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var referenceId = DNFromPath(item.Path);
|
||||
var externalId = referenceId;
|
||||
|
||||
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
|
||||
{
|
||||
externalId = item.Properties["objectGUID"][0].FromGuidToString();
|
||||
}
|
||||
|
||||
dict.Add(referenceId, externalId);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static GroupEntry BuildGroup(SearchResult item, Dictionary<string, string> userIndex)
|
||||
{
|
||||
var group = new GroupEntry
|
||||
{
|
||||
ReferenceId = DNFromPath(item.Path)
|
||||
};
|
||||
|
||||
if(group.ReferenceId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// External Id
|
||||
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
|
||||
{
|
||||
group.ExternalId = item.Properties["objectGUID"][0].FromGuidToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
group.ExternalId = group.ReferenceId;
|
||||
}
|
||||
|
||||
// Name
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.GroupNameAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.Ldap.GroupNameAttribute].Count > 0)
|
||||
{
|
||||
group.Name = item.Properties[SettingsService.Instance.Sync.Ldap.GroupNameAttribute][0].ToString();
|
||||
}
|
||||
else if(item.Properties.Contains("cn") && item.Properties["cn"].Count > 0)
|
||||
{
|
||||
group.Name = item.Properties["cn"][0].ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dates
|
||||
group.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.CreationDateAttribute);
|
||||
group.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute);
|
||||
|
||||
// Members
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.MemberAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.Ldap.MemberAttribute].Count > 0)
|
||||
{
|
||||
foreach(var member in item.Properties[SettingsService.Instance.Sync.Ldap.MemberAttribute])
|
||||
{
|
||||
var memberDn = member.ToString();
|
||||
if(userIndex.ContainsKey(memberDn) && !group.UserMemberExternalIds.Contains(userIndex[memberDn]))
|
||||
{
|
||||
group.UserMemberExternalIds.Add(userIndex[memberDn]);
|
||||
}
|
||||
else if(!group.GroupMemberReferenceIds.Contains(memberDn))
|
||||
{
|
||||
group.GroupMemberReferenceIds.Add(memberDn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static Task<List<UserEntry>> GetUsersAsync(bool force = false)
|
||||
{
|
||||
if(!SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
throw new ApplicationException("Not configured to sync users.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server?.Ldap == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for directory server.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
throw new ApplicationException("No configuration for sync.");
|
||||
}
|
||||
|
||||
if(!AuthService.Instance.Authenticated)
|
||||
{
|
||||
throw new ApplicationException("Not authenticated.");
|
||||
}
|
||||
|
||||
var userEntry = SettingsService.Instance.Server.Ldap.GetUserDirectoryEntry();
|
||||
var filter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass,
|
||||
SettingsService.Instance.Sync.UserFilter);
|
||||
filter = BuildRevisionFilter(filter, force, SettingsService.Instance.LastUserSyncDate);
|
||||
|
||||
Console.WriteLine("User search: {0} => {1}", userEntry.Path, filter);
|
||||
var searcher = new DirectorySearcher(userEntry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var users = new List<UserEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var user = BuildUser(item, false);
|
||||
if(user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
users.Add(user);
|
||||
}
|
||||
|
||||
// Deleted users
|
||||
if(SettingsService.Instance.Server.Type == DirectoryType.ActiveDirectory)
|
||||
{
|
||||
var deletedEntry = SettingsService.Instance.Server.Ldap.GetBasePathDirectoryEntry();
|
||||
var deletedFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass, "(isDeleted=TRUE)");
|
||||
deletedFilter = BuildRevisionFilter(deletedFilter, force, SettingsService.Instance.LastUserSyncDate);
|
||||
|
||||
var deletedSearcher = new DirectorySearcher(deletedEntry, deletedFilter);
|
||||
deletedSearcher.Tombstone = true;
|
||||
var deletedResult = deletedSearcher.FindAll();
|
||||
foreach(SearchResult item in deletedResult)
|
||||
{
|
||||
var user = BuildUser(item, true);
|
||||
if(user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
users.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(users);
|
||||
}
|
||||
|
||||
private static string BuildBaseFilter(string objectClass, string subFilter)
|
||||
{
|
||||
var filter = BuildObjectClassFilter(objectClass);
|
||||
if(!string.IsNullOrWhiteSpace(subFilter))
|
||||
{
|
||||
filter = string.Format("(&{0}{1})", filter, subFilter);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private static string BuildObjectClassFilter(string objectClass)
|
||||
{
|
||||
return string.Format("(&(objectClass={0}))", objectClass);
|
||||
}
|
||||
|
||||
private static string BuildRevisionFilter(string baseFilter, bool force, DateTime? lastRevisionDate)
|
||||
{
|
||||
if(!force && lastRevisionDate.HasValue &&
|
||||
!string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute))
|
||||
{
|
||||
baseFilter = string.Format("(&{0}({1}>={2}))",
|
||||
baseFilter,
|
||||
SettingsService.Instance.Sync.Ldap.RevisionDateAttribute,
|
||||
lastRevisionDate.Value.ToGeneralizedTimeUTC());
|
||||
}
|
||||
|
||||
return baseFilter;
|
||||
}
|
||||
|
||||
private static UserEntry BuildUser(SearchResult item, bool deleted)
|
||||
{
|
||||
var user = new UserEntry
|
||||
{
|
||||
ReferenceId = DNFromPath(item.Path),
|
||||
Deleted = deleted
|
||||
};
|
||||
|
||||
if(user.ReferenceId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// External Id
|
||||
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
|
||||
{
|
||||
user.ExternalId = item.Properties["objectGUID"][0].FromGuidToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.ExternalId = user.ReferenceId;
|
||||
}
|
||||
|
||||
user.Disabled = EntryDisabled(item);
|
||||
|
||||
// Email
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.UserEmailAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailAttribute].Count > 0)
|
||||
{
|
||||
user.Email = item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailAttribute][0]
|
||||
.ToString()
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(user.Email) && SettingsService.Instance.Sync.Ldap.EmailPrefixSuffix &&
|
||||
item.Properties.Contains(SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute].Count > 0 &&
|
||||
!string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.Ldap.UserEmailSuffix))
|
||||
{
|
||||
user.Email = string.Concat(
|
||||
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute][0].ToString(),
|
||||
SettingsService.Instance.Sync.Ldap.UserEmailSuffix).ToLowerInvariant();
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(user.Email) && !user.Deleted)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dates
|
||||
user.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.CreationDateAttribute);
|
||||
user.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private static bool EntryDisabled(SearchResult item)
|
||||
{
|
||||
if(!item.Properties.Contains("userAccountControl") || item.Properties["userAccountControl"].Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UserAccountControl control;
|
||||
if(!Enum.TryParse(item.Properties["userAccountControl"][0].ToString(), out control))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (control & UserAccountControl.AccountDisabled) == UserAccountControl.AccountDisabled;
|
||||
}
|
||||
|
||||
private static string DNFromPath(string path)
|
||||
{
|
||||
var dn = new Uri(path).Segments?.LastOrDefault();
|
||||
if(dn == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return WebUtility.UrlDecode(dn);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,266 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class SettingsService
|
||||
{
|
||||
private static SettingsService _instance;
|
||||
private static object _locker = new object();
|
||||
|
||||
private SettingsModel _settings;
|
||||
|
||||
private SettingsService() { }
|
||||
|
||||
public static SettingsService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new SettingsService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private SettingsModel Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
var filePath = $"{Constants.BaseStoragePath}\\settings.json";
|
||||
if(_settings == null && File.Exists(filePath))
|
||||
{
|
||||
var serializer = new JsonSerializer();
|
||||
using(var s = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using(var sr = new StreamReader(s, Encoding.UTF8))
|
||||
using(var jsonTextReader = new JsonTextReader(sr))
|
||||
{
|
||||
_settings = serializer.Deserialize<SettingsModel>(jsonTextReader);
|
||||
}
|
||||
|
||||
return _settings;
|
||||
}
|
||||
|
||||
InitSettings();
|
||||
return _settings;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
lock(_locker)
|
||||
{
|
||||
if(!Directory.Exists(Constants.BaseStoragePath))
|
||||
{
|
||||
Directory.CreateDirectory(Constants.BaseStoragePath);
|
||||
}
|
||||
|
||||
var filePath = $"{Constants.BaseStoragePath}\\settings.json";
|
||||
using(var s = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
using(var sw = new StreamWriter(s, Encoding.UTF8))
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_settings, Formatting.Indented);
|
||||
sw.Write(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitSettings()
|
||||
{
|
||||
if(_settings == null)
|
||||
{
|
||||
_settings = new SettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
public string ApiBaseUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.ApiBaseUrl;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.ApiBaseUrl = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public string IdentityBaseUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.IdentityBaseUrl;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.IdentityBaseUrl = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public EncryptedData AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.AccessToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.AccessToken = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public EncryptedData RefreshToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.RefreshToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.RefreshToken = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public Organization Organization
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.Organization;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.Organization = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public ServerConfiguration Server
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.Server;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.Server = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public SyncConfiguration Sync
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.Sync;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.Sync = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastGroupSyncDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.LastGroupSyncDate;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.LastGroupSyncDate = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastUserSyncDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.LastUserSyncDate;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.LastUserSyncDate = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public string GroupDeltaToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.GroupDeltaToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.GroupDeltaToken = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public string UserDeltaToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.UserDeltaToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.UserDeltaToken = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public string LastSyncHash
|
||||
{
|
||||
get
|
||||
{
|
||||
return Settings.LastSyncHash;
|
||||
}
|
||||
set
|
||||
{
|
||||
InitSettings();
|
||||
_settings.LastSyncHash = value;
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public class SettingsModel
|
||||
{
|
||||
public string ApiBaseUrl { get; set; } = "https://api.bitwarden.com";
|
||||
public string IdentityBaseUrl { get; set; } = "https://identity.bitwarden.com";
|
||||
public EncryptedData AccessToken { get; set; }
|
||||
public EncryptedData RefreshToken { get; set; }
|
||||
public ServerConfiguration Server { get; set; } = new ServerConfiguration();
|
||||
public SyncConfiguration Sync { get; set; } = new SyncConfiguration(Enums.DirectoryType.ActiveDirectory);
|
||||
public Organization Organization { get; set; } = new Organization();
|
||||
public DateTime? LastGroupSyncDate { get; set; }
|
||||
public DateTime? LastUserSyncDate { get; set; }
|
||||
public string GroupDeltaToken { get; set; }
|
||||
public string UserDeltaToken { get; set; }
|
||||
public string LastSyncHash { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class TokenService
|
||||
{
|
||||
private static TokenService _instance;
|
||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private string _accessToken;
|
||||
private dynamic _decodedAccessToken;
|
||||
|
||||
private TokenService() { }
|
||||
|
||||
public static TokenService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new TokenService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public string AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_accessToken != null)
|
||||
{
|
||||
return _accessToken;
|
||||
}
|
||||
|
||||
var encBytes = SettingsService.Instance.AccessToken;
|
||||
if(encBytes?.Value != null)
|
||||
{
|
||||
_accessToken = Encoding.ASCII.GetString(encBytes.Decrypt());
|
||||
}
|
||||
|
||||
return _accessToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
_accessToken = value;
|
||||
if(_accessToken == null)
|
||||
{
|
||||
SettingsService.Instance.AccessToken = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(_accessToken);
|
||||
SettingsService.Instance.AccessToken = new EncryptedData(bytes);
|
||||
bytes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime AccessTokenExpiration
|
||||
{
|
||||
get
|
||||
{
|
||||
var decoded = DecodeAccessToken();
|
||||
if(decoded?["exp"] == null)
|
||||
{
|
||||
throw new InvalidOperationException("No exp in token.");
|
||||
}
|
||||
|
||||
return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>()));
|
||||
}
|
||||
}
|
||||
|
||||
public bool AccessTokenExpired => DateTime.UtcNow < AccessTokenExpiration;
|
||||
public TimeSpan AccessTokenTimeRemaining => AccessTokenExpiration - DateTime.UtcNow;
|
||||
public bool AccessTokenNeedsRefresh => AccessTokenTimeRemaining.TotalMinutes < 5;
|
||||
public string AccessTokenUserId => DecodeAccessToken()?["sub"].Value<string>();
|
||||
public string AccessTokenEmail => DecodeAccessToken()?["email"].Value<string>();
|
||||
public string AccessTokenName => DecodeAccessToken()?["name"].Value<string>();
|
||||
|
||||
public string RefreshToken
|
||||
{
|
||||
get
|
||||
{
|
||||
var encData = SettingsService.Instance.RefreshToken;
|
||||
if(encData != null)
|
||||
{
|
||||
return Encoding.ASCII.GetString(encData.Decrypt());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
SettingsService.Instance.RefreshToken = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(value);
|
||||
SettingsService.Instance.RefreshToken = new EncryptedData(bytes);
|
||||
bytes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JObject DecodeAccessToken()
|
||||
{
|
||||
if(_decodedAccessToken != null)
|
||||
{
|
||||
return _decodedAccessToken;
|
||||
}
|
||||
|
||||
if(AccessToken == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} not found.");
|
||||
}
|
||||
|
||||
var parts = AccessToken.Split('.');
|
||||
if(parts.Length != 3)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
|
||||
}
|
||||
|
||||
var decodedBytes = Base64UrlDecode(parts[1]);
|
||||
if(decodedBytes == null || decodedBytes.Length < 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
|
||||
}
|
||||
|
||||
_decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length));
|
||||
return _decodedAccessToken;
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string input)
|
||||
{
|
||||
var output = input;
|
||||
// 62nd char of encoding
|
||||
output = output.Replace('-', '+');
|
||||
// 63rd char of encoding
|
||||
output = output.Replace('_', '/');
|
||||
// Pad with trailing '='s
|
||||
switch(output.Length % 4)
|
||||
{
|
||||
case 0:
|
||||
// No pad chars in this case
|
||||
break;
|
||||
case 2:
|
||||
// Two pad chars
|
||||
output += "=="; break;
|
||||
case 3:
|
||||
// One pad char
|
||||
output += "="; break;
|
||||
default:
|
||||
throw new InvalidOperationException("Illegal base64url string!");
|
||||
}
|
||||
|
||||
// Standard base64 decoder
|
||||
return Convert.FromBase64String(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public class AzureAuthenticationProvider : IAuthenticationProvider
|
||||
{
|
||||
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
|
||||
{
|
||||
if(SettingsService.Instance.Server?.Azure == null)
|
||||
{
|
||||
throw new ApplicationException("No server configuration.");
|
||||
}
|
||||
|
||||
var authContext = new AuthenticationContext(
|
||||
$"https://login.windows.net/{SettingsService.Instance.Server.Azure.Tenant}/oauth2/token");
|
||||
var secret = SettingsService.Instance.Server.Azure.Secret.DecryptToString();
|
||||
var creds = new ClientCredential(SettingsService.Instance.Server.Azure.Id, secret);
|
||||
var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
|
||||
request.Headers.Add("Authorization", $"Bearer {authResult.AccessToken}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public static string BaseStoragePath = string.Concat(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"\\Bitwarden\\Directory Connector");
|
||||
|
||||
public const string ProgramName = "Bitwarden Directory Connector";
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Crypto
|
||||
{
|
||||
public static byte[] MakeKeyFromPassword(string password, string salt)
|
||||
{
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
if(salt == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(salt));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var saltBytes = Encoding.UTF8.GetBytes(salt);
|
||||
|
||||
var keyBytes = DeriveKey(passwordBytes, saltBytes, 5000);
|
||||
|
||||
password = null;
|
||||
passwordBytes = null;
|
||||
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
public static string MakeKeyFromPasswordBase64(string password, string salt)
|
||||
{
|
||||
var key = MakeKeyFromPassword(password, salt);
|
||||
password = null;
|
||||
return Convert.ToBase64String(key);
|
||||
}
|
||||
|
||||
public static byte[] HashPassword(byte[] key, string password)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
var hashBytes = DeriveKey(key, passwordBytes, 1);
|
||||
|
||||
password = null;
|
||||
key = null;
|
||||
|
||||
return hashBytes;
|
||||
}
|
||||
|
||||
public static string HashPasswordBase64(byte[] key, string password)
|
||||
{
|
||||
var hash = HashPassword(key, password);
|
||||
password = null;
|
||||
key = null;
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
private static byte[] DeriveKey(byte[] password, byte[] salt, int rounds)
|
||||
{
|
||||
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
|
||||
generator.Init(password, salt, rounds);
|
||||
var key = ((KeyParameter)generator.GenerateDerivedMacParameters(256)).GetKey();
|
||||
password = null;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
#if NET461
|
||||
using System.DirectoryServices;
|
||||
#endif
|
||||
using System.Globalization;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
private const string GeneralizedTimeFormat = "yyyyMMddHHmmss.f'Z'";
|
||||
|
||||
public static DateTime ToDateTime(this string generalizedTimeString)
|
||||
{
|
||||
return DateTime.ParseExact(generalizedTimeString, GeneralizedTimeFormat, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string ToGeneralizedTimeUTC(this DateTime date)
|
||||
{
|
||||
return date.ToString("yyyyMMddHHmmss.f'Z'");
|
||||
}
|
||||
|
||||
#if NET461
|
||||
public static DateTime? ParseDateTime(this ResultPropertyCollection collection, string dateKey)
|
||||
{
|
||||
DateTime date;
|
||||
if(collection.Contains(dateKey) && collection[dateKey].Count > 0 &&
|
||||
DateTime.TryParse(collection[dateKey][0].ToString(), out date))
|
||||
{
|
||||
return date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static NameValueCollection ParseQueryString(this Uri uri)
|
||||
{
|
||||
var queryParameters = new NameValueCollection();
|
||||
var querySegments = uri.Query.Split('&');
|
||||
foreach(var segment in querySegments)
|
||||
{
|
||||
var parts = segment.Split('=');
|
||||
if(parts.Length > 0)
|
||||
{
|
||||
var key = parts[0].Trim(new char[] { '?', ' ' });
|
||||
var val = parts[1].Trim();
|
||||
queryParameters.Add(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return queryParameters;
|
||||
}
|
||||
|
||||
public static string FromGuidToString(this object property)
|
||||
{
|
||||
var propBytes = property as byte[];
|
||||
if(propBytes != null)
|
||||
{
|
||||
return new Guid(propBytes).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return property.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Helpers
|
||||
{
|
||||
public static bool IsAdministrator()
|
||||
{
|
||||
#if NET461
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Sync
|
||||
{
|
||||
public static async Task<SyncResult> SyncAllAsync(bool force = false, bool sendToServer = true)
|
||||
{
|
||||
var startingGroupDelta = SettingsService.Instance.GroupDeltaToken;
|
||||
var startingUserDelta = SettingsService.Instance.UserDeltaToken;
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var entriesResult = await GetDirectoryService().GetEntriesAsync(force);
|
||||
var groups = entriesResult.Item1;
|
||||
var users = entriesResult.Item2;
|
||||
|
||||
if(groups?.Any() ?? false)
|
||||
{
|
||||
FlattenUsersToGroups(groups, null, groups);
|
||||
}
|
||||
|
||||
if(!sendToServer)
|
||||
{
|
||||
RestoreDeltas(startingGroupDelta, startingUserDelta);
|
||||
}
|
||||
|
||||
if(!sendToServer || ((groups?.Count ?? 0) == 0 && (users?.Count ?? 0) == 0))
|
||||
{
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
|
||||
var request = new ImportRequest(groups, users);
|
||||
var json = JsonConvert.SerializeObject(request);
|
||||
var hash = ComputeHash(string.Concat(SettingsService.Instance.ApiBaseUrl, json));
|
||||
|
||||
if(hash == SettingsService.Instance.LastSyncHash)
|
||||
{
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
|
||||
var response = await ApiService.Instance.PostImportAsync(request);
|
||||
if(response.Succeeded)
|
||||
{
|
||||
SettingsService.Instance.LastSyncHash = hash;
|
||||
|
||||
if(SettingsService.Instance.Sync.SyncGroups)
|
||||
{
|
||||
SettingsService.Instance.LastGroupSyncDate = now;
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync.SyncUsers)
|
||||
{
|
||||
SettingsService.Instance.LastUserSyncDate = now;
|
||||
}
|
||||
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
RestoreDeltas(startingGroupDelta, startingUserDelta);
|
||||
return new SyncResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = response.Errors.FirstOrDefault()?.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
RestoreDeltas(startingGroupDelta, startingUserDelta);
|
||||
return new SyncResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = e.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IDirectoryService GetDirectoryService()
|
||||
{
|
||||
switch(SettingsService.Instance.Server.Type)
|
||||
{
|
||||
case Enums.DirectoryType.AzureActiveDirectory:
|
||||
return AzureDirectoryService.Instance;
|
||||
case Enums.DirectoryType.GSuite:
|
||||
return GSuiteDirectoryService.Instance;
|
||||
default:
|
||||
#if NET461
|
||||
return LdapDirectoryService.Instance;
|
||||
#else
|
||||
throw new Exception("LdapDirectoryService not supported.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlattenUsersToGroups(List<GroupEntry> currentGroups, List<string> currentGroupsUsers,
|
||||
List<GroupEntry> allGroups)
|
||||
{
|
||||
foreach(var group in currentGroups)
|
||||
{
|
||||
var groupsInThisGroup = allGroups.Where(g => group.GroupMemberReferenceIds.Contains(g.ReferenceId)).ToList();
|
||||
var usersInThisGroup = group.UserMemberExternalIds.ToList();
|
||||
|
||||
if(currentGroupsUsers != null)
|
||||
{
|
||||
foreach(var id in currentGroupsUsers)
|
||||
{
|
||||
if(!group.UserMemberExternalIds.Contains(id))
|
||||
{
|
||||
group.UserMemberExternalIds.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
usersInThisGroup.AddRange(currentGroupsUsers);
|
||||
}
|
||||
|
||||
// Recurse it
|
||||
FlattenUsersToGroups(groupsInThisGroup, usersInThisGroup, allGroups);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreDeltas(string groupDelta, string userDelta)
|
||||
{
|
||||
if(SettingsService.Instance.Server.Type != Enums.DirectoryType.AzureActiveDirectory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsService.Instance.GroupDeltaToken = groupDelta;
|
||||
SettingsService.Instance.UserDeltaToken = userDelta;
|
||||
}
|
||||
|
||||
private static string ComputeHash(string value)
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string result = null;
|
||||
using(var hash = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
var hashBytes = hash.ComputeHash(bytes);
|
||||
result = Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.ServiceProcess;
|
||||
using System.Configuration.Install;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Service
|
||||
{
|
||||
[RunInstaller(true)]
|
||||
[DesignerCategory("Code")]
|
||||
public class Installer : System.Configuration.Install.Installer
|
||||
{
|
||||
private IContainer _components = null;
|
||||
private ServiceProcessInstaller _serviceProcessInstaller;
|
||||
private ServiceInstaller _serviceInstaller;
|
||||
|
||||
public Installer()
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
_components = new Container();
|
||||
_serviceProcessInstaller = new ServiceProcessInstaller();
|
||||
_serviceInstaller = new ServiceInstaller();
|
||||
|
||||
_serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
|
||||
_serviceProcessInstaller.AfterInstall += new InstallEventHandler(AfterInstalled);
|
||||
_serviceProcessInstaller.BeforeInstall += new InstallEventHandler(BeforeInstalled);
|
||||
|
||||
_serviceInstaller.ServiceName = Constants.ProgramName;
|
||||
_serviceInstaller.Description = "Sync directory groups and users to your Bitwarden organization.";
|
||||
Installers.AddRange(new System.Configuration.Install.Installer[] { _serviceProcessInstaller, _serviceInstaller });
|
||||
}
|
||||
|
||||
private void AfterInstalled(object sender, InstallEventArgs e)
|
||||
{
|
||||
var info = new DirectoryInfo(Constants.BaseStoragePath);
|
||||
if(!info.Exists)
|
||||
{
|
||||
info.Create();
|
||||
}
|
||||
|
||||
var sec = info.GetAccessControl();
|
||||
AddPermission(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), sec);
|
||||
AddPermission(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), sec);
|
||||
AddPermission(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), sec);
|
||||
AddPermission(new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), sec);
|
||||
AddPermission(WindowsIdentity.GetCurrent().User, sec);
|
||||
sec.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
|
||||
info.SetAccessControl(sec);
|
||||
}
|
||||
|
||||
private void AddPermission(IdentityReference sid, DirectorySecurity sec)
|
||||
{
|
||||
var rule = new FileSystemAccessRule(
|
||||
sid,
|
||||
FileSystemRights.FullControl | FileSystemRights.Write | FileSystemRights.Read,
|
||||
InheritanceFlags.None,
|
||||
PropagationFlags.NoPropagateInherit,
|
||||
AccessControlType.Allow);
|
||||
sec.AddAccessRule(rule);
|
||||
}
|
||||
|
||||
private void BeforeInstalled(object sender, InstallEventArgs e)
|
||||
{
|
||||
if(EventLog.SourceExists(_serviceInstaller.ServiceName))
|
||||
{
|
||||
EventLog.DeleteEventSource(_serviceInstaller.ServiceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
//DebugMode();
|
||||
|
||||
ServiceBase.Run(new ServiceBase[]
|
||||
{
|
||||
new Service()
|
||||
});
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void DebugMode()
|
||||
{
|
||||
Debugger.Launch();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Service
|
||||
{
|
||||
[DesignerCategory("Code")]
|
||||
public class Service : ServiceBase
|
||||
{
|
||||
private IContainer _components;
|
||||
private EventLog _eventLog;
|
||||
private Timer _timer;
|
||||
|
||||
public Service()
|
||||
{
|
||||
ServiceName = Constants.ProgramName;
|
||||
|
||||
_components = new Container();
|
||||
|
||||
_eventLog = new EventLog();
|
||||
_eventLog.Source = ServiceName;
|
||||
_eventLog.Log = "Application";
|
||||
|
||||
if(!EventLog.SourceExists(_eventLog.Source))
|
||||
{
|
||||
EventLog.CreateEventSource(_eventLog.Source, _eventLog.Log);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if(disposing)
|
||||
{
|
||||
_eventLog?.Dispose();
|
||||
_eventLog = null;
|
||||
|
||||
_components?.Dispose();
|
||||
_components = null;
|
||||
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
_eventLog.WriteEntry("Service started!", EventLogEntryType.Information);
|
||||
|
||||
if(SettingsService.Instance.Server == null)
|
||||
{
|
||||
_eventLog.WriteEntry("Server not configured.", EventLogEntryType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
{
|
||||
_eventLog.WriteEntry("Sync not configured.", EventLogEntryType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
{
|
||||
_eventLog.WriteEntry("Not authenticated with proper organization set.", EventLogEntryType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var intervalMinutes = SettingsService.Instance.Sync.IntervalMinutes;
|
||||
if(intervalMinutes < 5)
|
||||
{
|
||||
intervalMinutes = 5;
|
||||
}
|
||||
|
||||
_eventLog.WriteEntry($"Starting timer with {intervalMinutes} minute interval.", EventLogEntryType.Information);
|
||||
var timerDelegate = new TimerCallback(Callback);
|
||||
_timer = new Timer(timerDelegate, null, 1000, 60 * 1000 * intervalMinutes);
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
_eventLog.WriteEntry("Service stopped!", EventLogEntryType.Information);
|
||||
}
|
||||
|
||||
private void Callback(object stateInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result = Sync.SyncAllAsync(false, true).GetAwaiter().GetResult();
|
||||
sw.Stop();
|
||||
if(result.Success)
|
||||
{
|
||||
_eventLog.WriteEntry($"Synced {result.Groups?.Count ?? 0} groups, {result.Users?.Count ?? 0} users. " +
|
||||
$"The sync took {(int)sw.Elapsed.TotalSeconds} seconds to complete.",
|
||||
EventLogEntryType.SuccessAudit);
|
||||
}
|
||||
else
|
||||
{
|
||||
_eventLog.WriteEntry($"Sync failed after {(int)sw.Elapsed.TotalSeconds} seconds: {result.ErrorMessage}.",
|
||||
EventLogEntryType.FailureAudit);
|
||||
}
|
||||
}
|
||||
catch(ApplicationException e)
|
||||
{
|
||||
_eventLog.WriteEntry($"Sync exception: {e.Message}", EventLogEntryType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
2
src/global.d.ts
vendored
Normal file
2
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare function escape(s: string): string;
|
||||
declare function unescape(s: string): string;
|
||||
14
src/index.html
Normal file
14
src/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Bitwarden</title>
|
||||
<base href="">
|
||||
</head>
|
||||
<body>
|
||||
<app-root>
|
||||
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
|
||||
</app-root>
|
||||
</body>
|
||||
</html>
|
||||
66
src/main.ts
Normal file
66
src/main.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
// import { ElectronMainMessagingService } from 'jslib/electron/services/desktopMainMessaging.service';
|
||||
import { I18nService } from 'jslib/services/i18n.service';
|
||||
|
||||
import { MessagingMain } from './main/messaging.main';
|
||||
|
||||
import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
|
||||
import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
|
||||
import { WindowMain } from 'jslib/electron/window.main';
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
i18nService: I18nService;
|
||||
storageService: ElectronStorageService;
|
||||
|
||||
windowMain: WindowMain;
|
||||
messagingMain: MessagingMain;
|
||||
|
||||
constructor() {
|
||||
// Set paths for portable builds
|
||||
let appDataPath = null;
|
||||
if (process.env.BITWARDEN_APPDATA_DIR != null) {
|
||||
appDataPath = process.env.BITWARDEN_APPDATA_DIR;
|
||||
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) {
|
||||
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-appdata');
|
||||
} else if (process.platform === 'linux' && process.env.SNAP_USER_DATA != null) {
|
||||
appDataPath = path.join(process.env.SNAP_USER_DATA, 'appdata');
|
||||
}
|
||||
|
||||
if (appDataPath != null) {
|
||||
app.setPath('userData', appDataPath);
|
||||
}
|
||||
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
|
||||
|
||||
const args = process.argv.slice(1);
|
||||
const watch = args.some((val) => val === '--watch');
|
||||
|
||||
if (watch) {
|
||||
// tslint:disable-next-line
|
||||
require('electron-reload')(__dirname, {});
|
||||
}
|
||||
|
||||
this.logService = new ElectronLogService(null, app.getPath('userData'));
|
||||
// this.i18nService = new I18nService('en', './locales/');
|
||||
this.storageService = new ElectronStorageService();
|
||||
// this.messagingService = new DesktopMainMessagingService(this);
|
||||
|
||||
this.windowMain = new WindowMain(this.storageService);
|
||||
this.messagingMain = new MessagingMain(this);
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
this.windowMain.init().then(async () => {
|
||||
await this.i18nService.init(app.getLocale());
|
||||
this.messagingMain.init();
|
||||
}, (e: any) => {
|
||||
// tslint:disable-next-line
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const main = new Main();
|
||||
main.bootstrap();
|
||||
74
src/main/messaging.main.ts
Normal file
74
src/main/messaging.main.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
app,
|
||||
ipcMain,
|
||||
} from 'electron';
|
||||
|
||||
import {
|
||||
deletePassword,
|
||||
getPassword,
|
||||
setPassword,
|
||||
} from 'keytar';
|
||||
|
||||
import { Main } from '../main';
|
||||
|
||||
const KeytarService = 'Bitwarden';
|
||||
const SyncInterval = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export class MessagingMain {
|
||||
private syncTimeout: NodeJS.Timer;
|
||||
|
||||
constructor(private main: Main) { }
|
||||
|
||||
init() {
|
||||
this.scheduleNextSync();
|
||||
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message));
|
||||
|
||||
ipcMain.on('keytar', async (event: any, message: any) => {
|
||||
try {
|
||||
let val: string = null;
|
||||
if (message.action && message.key) {
|
||||
if (message.action === 'getPassword') {
|
||||
val = await getPassword(KeytarService, message.key);
|
||||
} else if (message.action === 'setPassword' && message.value) {
|
||||
await setPassword(KeytarService, message.key, message.value);
|
||||
} else if (message.action === 'deletePassword') {
|
||||
await deletePassword(KeytarService, message.key);
|
||||
}
|
||||
}
|
||||
|
||||
event.returnValue = val;
|
||||
} catch {
|
||||
event.returnValue = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMessage(message: any) {
|
||||
switch (message.command) {
|
||||
case 'scheduleNextSync':
|
||||
this.scheduleNextSync();
|
||||
break;
|
||||
case 'updateAppMenu':
|
||||
// this.main.menuMain.updateApplicationMenuState(message.isAuthenticated, message.isLocked);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleNextSync() {
|
||||
if (this.syncTimeout) {
|
||||
global.clearTimeout(this.syncTimeout);
|
||||
}
|
||||
|
||||
this.syncTimeout = global.setTimeout(() => {
|
||||
if (this.main.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.main.windowMain.win.webContents.send('messagingService', {
|
||||
command: 'checkSyncVault',
|
||||
});
|
||||
}, SyncInterval);
|
||||
}
|
||||
}
|
||||
303
src/package-lock.json
generated
Normal file
303
src/package-lock.json
generated
Normal file
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"name": "bitwarden",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "1.0.3"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
|
||||
},
|
||||
"bluebird-lst": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz",
|
||||
"integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==",
|
||||
"requires": {
|
||||
"bluebird": "3.5.1"
|
||||
}
|
||||
},
|
||||
"builder-util-runtime": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.0.tgz",
|
||||
"integrity": "sha512-cROCExnJOJvRD58HHcnrrgyRAoDHGZT0hKox0op7vTuuuRC/1JKMXvSR+Hxy7KWy/aEmKu0HfSqMd4znDEqQsA==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"debug": "3.1.0",
|
||||
"fs-extra-p": "4.5.2",
|
||||
"sax": "1.2.4"
|
||||
}
|
||||
},
|
||||
"conf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
|
||||
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
|
||||
"requires": {
|
||||
"dot-prop": "4.2.0",
|
||||
"env-paths": "1.0.0",
|
||||
"make-dir": "1.2.0",
|
||||
"pkg-up": "2.0.0",
|
||||
"write-file-atomic": "2.3.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
|
||||
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
|
||||
"requires": {
|
||||
"is-obj": "1.0.1"
|
||||
}
|
||||
},
|
||||
"electron-is-dev": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
|
||||
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
|
||||
},
|
||||
"electron-log": {
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz",
|
||||
"integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g=="
|
||||
},
|
||||
"electron-store": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz",
|
||||
"integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==",
|
||||
"requires": {
|
||||
"conf": "1.4.0"
|
||||
}
|
||||
},
|
||||
"electron-updater": {
|
||||
"version": "2.21.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz",
|
||||
"integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"builder-util-runtime": "4.2.0",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"fs-extra-p": "4.5.2",
|
||||
"js-yaml": "3.11.0",
|
||||
"lazy-val": "1.0.3",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"semver": "5.5.0",
|
||||
"source-map-support": "0.5.4"
|
||||
}
|
||||
},
|
||||
"env-paths": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
|
||||
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
|
||||
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||
"requires": {
|
||||
"locate-path": "2.0.0"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"jsonfile": "4.0.0",
|
||||
"universalify": "0.1.1"
|
||||
}
|
||||
},
|
||||
"fs-extra-p": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
|
||||
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"fs-extra": "5.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
|
||||
"integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==",
|
||||
"requires": {
|
||||
"argparse": "1.0.10",
|
||||
"esprima": "4.0.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11"
|
||||
}
|
||||
},
|
||||
"keytar": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz",
|
||||
"integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==",
|
||||
"requires": {
|
||||
"nan": "2.5.1"
|
||||
}
|
||||
},
|
||||
"lazy-val": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz",
|
||||
"integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg=="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||
"requires": {
|
||||
"p-locate": "2.0.0",
|
||||
"path-exists": "3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
|
||||
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
|
||||
"requires": {
|
||||
"pify": "3.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz",
|
||||
"integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
|
||||
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
|
||||
"requires": {
|
||||
"p-try": "1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"requires": {
|
||||
"p-limit": "1.2.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
|
||||
},
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"pkg-up": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
|
||||
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
|
||||
"requires": {
|
||||
"find-up": "2.1.0"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz",
|
||||
"integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==",
|
||||
"requires": {
|
||||
"source-map": "0.6.1"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
|
||||
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"imurmurhash": "0.1.4",
|
||||
"signal-exit": "3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/package.json
Normal file
20
src/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "bitwarden",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "1.0.0",
|
||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/desktop"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-log": "2.2.14",
|
||||
"electron-store": "1.3.0",
|
||||
"electron-updater": "2.21.4",
|
||||
"keytar": "4.1.0"
|
||||
}
|
||||
}
|
||||
75
src/scss/base.scss
Normal file
75
src/scss/base.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
@import "variables.scss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background-color: $background-color-alt2;
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-base;
|
||||
color: $text-color;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: $font-family-sans-serif;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $brand-primary;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: darken($brand-primary, 6%);
|
||||
}
|
||||
}
|
||||
|
||||
input, select, textarea, button {
|
||||
font-size: $font-size-base;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
div:not(.modal)::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
div:not(.modal)::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
div:not(.modal)::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100,100,100,.2);
|
||||
border-radius: 10px;
|
||||
margin-right: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(100,100,100,.4);
|
||||
}
|
||||
}
|
||||
260
src/scss/box.scss
Normal file
260
src/scss/box.scss
Normal file
@@ -0,0 +1,260 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
|
||||
.box-header {
|
||||
margin: 0 10px 5px 10px;
|
||||
color: $gray-light;
|
||||
text-transform: uppercase;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: $gray-light;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.box-content {
|
||||
background: $box-background-color;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
|
||||
|
||||
&.box-content-padded {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.box-content-row {
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
width: calc(100% - 10px);
|
||||
border-bottom: 1px solid $box-border-color;
|
||||
}
|
||||
|
||||
&:first-child, &:last-child {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:before {
|
||||
border: none;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&:hover, &:focus, &.active {
|
||||
background-color: $box-background-hover-color;
|
||||
}
|
||||
|
||||
&.pre {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.row-label, label {
|
||||
font-size: $font-size-small;
|
||||
color: $text-muted;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.text, .detail {
|
||||
display: block;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: $font-size-small;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.img-right {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.box-content-row-flex, &.box-content-row-checkbox, &.box-content-row-input,
|
||||
&.box-content-row-slider, &.box-content-row-multi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&.box-content-row-multi {
|
||||
width: 100%;
|
||||
|
||||
input:not([type="checkbox"]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input + label.sr-only + select {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
> a {
|
||||
padding: 8px 8px 8px 4px;
|
||||
color: $brand-danger;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-checkbox, &.box-content-row-input, &.box-content-row-slider {
|
||||
label, .row-label {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color;
|
||||
display: inline;
|
||||
width: initial;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
> input {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-right: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-input {
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-slider {
|
||||
input[type="range"] {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]), textarea {
|
||||
border: none;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: lighten($gray-light, 35%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid darken($border-color-dark, 7%);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin-left: 5px;
|
||||
|
||||
.row-btn {
|
||||
cursor: pointer;
|
||||
padding: 10px 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: $brand-primary;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: darken($brand-primary, 10%);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $list-icon-color;
|
||||
|
||||
&:hover {
|
||||
color: $list-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-pad .row-btn {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select.field-type {
|
||||
margin: 5px 0 0 25px;
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
|
||||
.row-sub-icon {
|
||||
color: $list-icon-color;
|
||||
}
|
||||
|
||||
.row-sub-label {
|
||||
margin: 0 15px;
|
||||
color: $gray-light;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.condensed .box-content-row, .box-content-row.condensed {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&.no-hover .box-content-row, .box-content-row.no-hover {
|
||||
&:hover, &:focus {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-footer {
|
||||
margin: 5px 10px;
|
||||
font-size: $font-size-small;
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
70
src/scss/buttons.scss
Normal file
70
src/scss/buttons.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.btn, #vault .footer button, .modal-footer button {
|
||||
background-color: $button-backgound-color;
|
||||
border-radius: $border-radius;
|
||||
padding: 7px 15px;
|
||||
border: 1px solid $button-border-color;
|
||||
font-size: $font-size-base;
|
||||
color: $button-color;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
color: $button-color-primary;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: $button-color-danger;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
cursor: pointer;
|
||||
background-color: darken($button-backgound-color, 1.5%);
|
||||
border-color: darken($button-border-color, 17%);
|
||||
color: darken($button-color, 10%);
|
||||
|
||||
&.primary {
|
||||
color: darken($button-color-primary, 6%);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: darken($button-color-danger, 6%);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not([disabled]) {
|
||||
cursor: pointer;
|
||||
background-color: darken($button-backgound-color, 6%);
|
||||
border-color: darken($button-border-color, 25%);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.65;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
&.block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.link {
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.btn {
|
||||
&:focus {
|
||||
outline: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/scss/environment.scss
Normal file
7
src/scss/environment.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
@import "variables.scss";
|
||||
|
||||
html.os_windows {
|
||||
body {
|
||||
border-top: 1px solid $border-color-dark;
|
||||
}
|
||||
}
|
||||
76
src/scss/list.scss
Normal file
76
src/scss/list.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.list > a {
|
||||
display: block;
|
||||
padding: 3px 10px;
|
||||
background-color: white;
|
||||
text-decoration: none;
|
||||
color: $text-color;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
width: calc(100% - 10px);
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
&:last-child:before {
|
||||
border: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&:hover, &:focus, &.active {
|
||||
background-color: $list-item-hover;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-left: 5px solid $brand-primary;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&:focus:not(.active) {
|
||||
border-left: 5px solid $text-muted;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.text, .detail {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: $font-size-small;
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
float: left;
|
||||
height: 36px;
|
||||
width: 34px;
|
||||
margin-left: -5px;
|
||||
color: $text-muted;
|
||||
|
||||
img {
|
||||
border-radius: $border-radius;
|
||||
max-height: 20px;
|
||||
max-width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/scss/misc.scss
Normal file
188
src/scss/misc.scss
Normal file
@@ -0,0 +1,188 @@
|
||||
@import "variables.scss";
|
||||
|
||||
small {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: $brand-primary !important;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: $brand-success !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: $text-muted !important;
|
||||
}
|
||||
|
||||
.text-default {
|
||||
color: $text-color !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
p.lead {
|
||||
font-size: $font-size-large;
|
||||
margin-bottom: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monospaced {
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.totp {
|
||||
.totp-code {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.totp-countdown {
|
||||
margin: 3px 3px 0 0;
|
||||
display: block;
|
||||
user-select: none;
|
||||
|
||||
.totp-sec {
|
||||
font-size: 0.85em;
|
||||
position: absolute;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.totp-circle {
|
||||
stroke: $brand-primary;
|
||||
fill: none;
|
||||
|
||||
&.inner {
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 78.6;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
&.outer {
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 88;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.low {
|
||||
.totp-sec, .totp-code {
|
||||
color: $brand-danger;
|
||||
}
|
||||
|
||||
.totp-circle {
|
||||
stroke: $brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-block {
|
||||
font-size: $font-size-large;
|
||||
word-break: break-all;
|
||||
font-family: $font-family-monospace;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.modal-body & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#duo-frame {
|
||||
background: url('../images/loading.svg') 0 0 no-repeat;
|
||||
height: 330px;
|
||||
margin: 0 -150px 15px -150px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
form, .form {
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
border: 1px solid darken($border-color-dark, 7%);
|
||||
border-radius: $border-radius;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-left: 18px;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
margin-top: 4px;
|
||||
margin-left: -18px;
|
||||
}
|
||||
}
|
||||
|
||||
.help-block {
|
||||
margin-top: 3px;
|
||||
color: $text-muted;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
app-root > #loading {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: $text-muted;
|
||||
}
|
||||
338
src/scss/modal.scss
Normal file
338
src/scss/modal.scss
Normal file
@@ -0,0 +1,338 @@
|
||||
@import "variables.scss";
|
||||
|
||||
$white: white;
|
||||
$black: black;
|
||||
$line-height-base: 14px;
|
||||
$border-radius-lg: $border-radius;
|
||||
|
||||
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss
|
||||
|
||||
$grid-breakpoints: ( xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px ) !default;
|
||||
|
||||
$zindex-modal-backdrop: 1040 !default;
|
||||
$zindex-modal: 1050 !default;
|
||||
|
||||
// Padding applied to the modal body
|
||||
$modal-inner-padding: 10px !default;
|
||||
|
||||
$modal-dialog-margin: .5rem !default;
|
||||
$modal-dialog-margin-y-sm-up: 1.75rem !default;
|
||||
|
||||
$modal-title-line-height: $line-height-base !default;
|
||||
|
||||
$modal-content-bg: $background-color-alt !default;
|
||||
$modal-content-border-color: rgba($black, .2) !default;
|
||||
$modal-content-border-width: 1px !default;
|
||||
$modal-content-box-shadow-xs: none;
|
||||
$modal-content-box-shadow-sm-up: none;
|
||||
|
||||
$modal-backdrop-bg: $black !default;
|
||||
$modal-backdrop-opacity: .5 !default;
|
||||
$modal-header-border-color: $border-color-dark !default;
|
||||
$modal-footer-border-color: $modal-header-border-color !default;
|
||||
$modal-header-border-width: $modal-content-border-width !default;
|
||||
$modal-footer-border-width: $modal-header-border-width !default;
|
||||
$modal-header-padding: 12px !default;
|
||||
|
||||
$modal-lg: 800px !default;
|
||||
$modal-md: 500px !default;
|
||||
$modal-sm: 300px !default;
|
||||
|
||||
$modal-transition: transform .3s ease-out !default;
|
||||
|
||||
$close-font-size: $font-size-base * 1.5 !default;
|
||||
$close-font-weight: bold !default;
|
||||
$close-color: $black !default;
|
||||
$close-text-shadow: 0 1px 0 $white !default;
|
||||
|
||||
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_breakpoints.scss
|
||||
|
||||
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
|
||||
$min: breakpoint-min($name, $breakpoints);
|
||||
|
||||
@if $min {
|
||||
@media (min-width: $min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@else {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
|
||||
$min: map-get($breakpoints, $name);
|
||||
@return if($min != 0, $min, null);
|
||||
}
|
||||
|
||||
// Custom Added CSS animations
|
||||
|
||||
@keyframes modalshow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, -25%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdropshow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_modal.scss
|
||||
|
||||
// .modal-open - body class for killing the scroll
|
||||
// .modal - container to scroll within
|
||||
// .modal-dialog - positioning shell for the actual modal
|
||||
// .modal-content - actual modal w/ bg and corners and stuff
|
||||
|
||||
|
||||
// Kill the scroll on the body
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Container that the modal scrolls within
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: $zindex-modal;
|
||||
//display: none;
|
||||
overflow: hidden;
|
||||
// Prevent Chrome on Windows from adding a focus outline. For details, see
|
||||
// https://github.com/twbs/bootstrap/pull/10951.
|
||||
outline: 0;
|
||||
// We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a
|
||||
// gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342
|
||||
// See also https://github.com/twbs/bootstrap/issues/17695
|
||||
.modal-open & {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Shell div to position the modal with bottom padding
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin: $modal-dialog-margin;
|
||||
// allow clicks to pass through for custom click handling to close modal
|
||||
pointer-events: none;
|
||||
// When fading in the modal, animate it to slide down
|
||||
.modal.fade & {
|
||||
//@include transition($modal-transition);
|
||||
//transform: translate(0, -25%);
|
||||
animation: modalshow 0.3s ease-in;
|
||||
}
|
||||
//.modal.show & {
|
||||
// transform: translate(0, 0);
|
||||
//}
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.modal-dialog-centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: calc(100% - (#{$modal-dialog-margin} * 2));
|
||||
}
|
||||
|
||||
// Actual modal
|
||||
.modal-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
|
||||
// counteract the pointer-events: none; in the .modal-dialog
|
||||
pointer-events: auto;
|
||||
background-color: $modal-content-bg;
|
||||
background-clip: padding-box;
|
||||
border: $modal-content-border-width solid $modal-content-border-color;
|
||||
//@include border-radius($border-radius-lg);
|
||||
//@include box-shadow($modal-content-box-shadow-xs);
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow: $modal-content-box-shadow-xs;
|
||||
// Remove focus outline from opened modal
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
// Modal background
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: $zindex-modal-backdrop;
|
||||
background-color: $modal-backdrop-bg;
|
||||
// Fade for backdrop
|
||||
&.fade {
|
||||
//opacity: 0;
|
||||
animation: backdropshow 0.1s ease-in;
|
||||
}
|
||||
//&.show {
|
||||
// opacity: $modal-backdrop-opacity;
|
||||
//}
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
|
||||
// Modal header
|
||||
// Top section of the modal w/ title and dismiss
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start; // so the close btn always stays on the upper right corner
|
||||
justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends
|
||||
padding: $modal-header-padding $modal-inner-padding;
|
||||
border-bottom: $modal-header-border-width solid $modal-header-border-color;
|
||||
//@include border-top-radius($border-radius-lg);
|
||||
|
||||
.close {
|
||||
padding: $modal-header-padding $modal-inner-padding;
|
||||
// auto on the left force icon to the right even when there is no .modal-title
|
||||
margin: (-$modal-header-padding) (-$modal-inner-padding) (-$modal-header-padding) auto;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: $font-size-base;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title text within header
|
||||
.modal-title {
|
||||
margin-bottom: 0;
|
||||
line-height: $modal-title-line-height;
|
||||
}
|
||||
|
||||
// Modal body
|
||||
// Where all modal content resides (sibling of .modal-header and .modal-footer)
|
||||
.modal-body {
|
||||
position: relative;
|
||||
// Enable `flex-grow: 1` so that the body take up as much space as possible
|
||||
// when should there be a fixed height on `.modal-dialog`.
|
||||
flex: 1 1 auto;
|
||||
padding: $modal-inner-padding;
|
||||
}
|
||||
|
||||
// Footer (for actions)
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center; // vertically center
|
||||
//justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
|
||||
padding: $modal-inner-padding;
|
||||
border-top: $modal-footer-border-width solid $modal-footer-border-color;
|
||||
// Easily place margin between footer elements
|
||||
button {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Measure scrollbar width for padding body during modal show/hide
|
||||
.modal-scrollbar-measure {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
// Scale up the modal
|
||||
@include media-breakpoint-up(sm) {
|
||||
// Automatically set modal's width for larger viewports
|
||||
.modal-dialog {
|
||||
max-width: $modal-md;
|
||||
margin: $modal-dialog-margin-y-sm-up auto;
|
||||
}
|
||||
|
||||
.modal-dialog-centered {
|
||||
min-height: calc(100% - (#{$modal-dialog-margin-y-sm-up} * 2));
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
//@include box-shadow($modal-content-box-shadow-sm-up);
|
||||
box-shadow: $modal-content-box-shadow-sm-up;
|
||||
}
|
||||
|
||||
.modal-sm {
|
||||
max-width: $modal-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.modal-lg {
|
||||
max-width: $modal-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_close.scss
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: $close-font-size;
|
||||
font-weight: $close-font-weight;
|
||||
line-height: 1;
|
||||
color: $close-color;
|
||||
text-shadow: $close-text-shadow;
|
||||
opacity: .5;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $close-color;
|
||||
text-decoration: none;
|
||||
opacity: .75;
|
||||
}
|
||||
// Opinionated: add "hand" cursor to non-disabled .close elements
|
||||
&:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional properties for button version
|
||||
// iOS requires the button element instead of an anchor tag.
|
||||
// If you want the anchor version, it requires `href="#"`.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
|
||||
|
||||
// stylelint-disable property-no-vendor-prefix, selector-no-qualifying-type
|
||||
button.close {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
// stylelint-enable
|
||||
|
||||
// box
|
||||
|
||||
.modal-content .box {
|
||||
margin-top: 20px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
118
src/scss/pages.scss
Normal file
118
src/scss/pages.scss
Normal file
@@ -0,0 +1,118 @@
|
||||
@import "variables.scss";
|
||||
|
||||
#login-page, #lock-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
@media (min-height: 500px) {
|
||||
height: calc(100% + 50px);
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
height: calc(100% + 300px);
|
||||
margin-top: -300px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0 auto 15px;
|
||||
width: 282px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#register-page, #hint-page, #two-factor-page {
|
||||
padding-top: 20px;
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 10px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
#login-page, #register-page, #hint-page, #two-factor-page, #lock-page {
|
||||
.content {
|
||||
width: 300px;
|
||||
|
||||
p {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
p.lead, h1 {
|
||||
font-size: $font-size-large;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.box {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.last {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-options {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.settings-icon {
|
||||
color: #bbbbbb;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
span {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $brand-primary;
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#register-page {
|
||||
.content {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
75
src/scss/plugins.scss
Normal file
75
src/scss/plugins.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome.scss";
|
||||
@import "~angular2-toaster/toaster";
|
||||
|
||||
@import "variables.scss";
|
||||
|
||||
#toast-container {
|
||||
.toast-close-button {
|
||||
right: -0.15em;
|
||||
}
|
||||
|
||||
.toast {
|
||||
opacity: 1 !important;
|
||||
background-image: none !important;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
font-size: 25px;
|
||||
line-height: 20px;
|
||||
float: left;
|
||||
color: #ffffff;
|
||||
padding-right: 10px;
|
||||
margin: auto 0 auto -36px;
|
||||
}
|
||||
|
||||
.toaster-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.toast-danger, &.toast-error {
|
||||
background-image: none !important;
|
||||
background-color: $brand-danger;
|
||||
|
||||
&:before {
|
||||
content: "\f0e7";
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
background-image: none !important;
|
||||
background-color: $brand-warning;
|
||||
|
||||
&:before {
|
||||
content: "\f071";
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
background-image: none !important;
|
||||
background-color: $brand-info;
|
||||
|
||||
&:before {
|
||||
content: "\f05a";
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
background-image: none !important;
|
||||
background-color: $brand-success;
|
||||
|
||||
&:before {
|
||||
content: "\f00C";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/scss/styles.scss
Normal file
11
src/scss/styles.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
@import "variables.scss";
|
||||
@import "base.scss";
|
||||
@import "pages.scss";
|
||||
@import "vault.scss";
|
||||
@import "list.scss";
|
||||
@import "box.scss";
|
||||
@import "buttons.scss";
|
||||
@import "misc.scss";
|
||||
@import "modal.scss";
|
||||
@import "plugins.scss";
|
||||
@import "environment.scss";
|
||||
38
src/scss/variables.scss
Normal file
38
src/scss/variables.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
$font-size-base: 14px;
|
||||
$font-size-large: 18px;
|
||||
$font-size-small: 12px;
|
||||
$text-color: #000000;
|
||||
$background-color: #efeff4;
|
||||
$border-color: #f0f0f0;
|
||||
$border-color-dark: #ddd;
|
||||
$list-item-hover: #fbfbfb;
|
||||
$list-icon-color: #c7c7cd;
|
||||
$border-radius: 3px;
|
||||
$line-height-base: 1.42857143;
|
||||
|
||||
$gray: #555;
|
||||
$gray-light: #777;
|
||||
$text-muted: $gray-light;
|
||||
|
||||
$brand-primary: #3c8dbc;
|
||||
$brand-danger: #dd4b39;
|
||||
$brand-success: #00a65a;
|
||||
$brand-info: #555555;
|
||||
$brand-warning: #bf7e16;
|
||||
$brand-primary-accent: #286090;
|
||||
|
||||
$background-color: white;
|
||||
$background-color-alt: #f9fafc;
|
||||
$background-color-alt2: #ecf0f5;
|
||||
|
||||
$box-background-color: $background-color;
|
||||
$box-background-hover-color: $list-item-hover;
|
||||
$box-border-color: $border-color;
|
||||
|
||||
$button-border-color: darken($border-color-dark, 12%);
|
||||
$button-backgound-color: white;
|
||||
$button-color: lighten($text-color, 40%);
|
||||
$button-color-primary: darken($brand-primary, 8%);
|
||||
$button-color-danger: darken($brand-danger, 10%);
|
||||
269
src/scss/vault.scss
Normal file
269
src/scss/vault.scss
Normal file
@@ -0,0 +1,269 @@
|
||||
@import "variables.scss";
|
||||
|
||||
#vault {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
> #groupings, > #items, > #details, > #logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.inner-content {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
> #groupings {
|
||||
background-color: $background-color-alt;
|
||||
width: 22%;
|
||||
min-width: 175px;
|
||||
max-width: 250px;
|
||||
border-right: 1px solid $border-color-dark;
|
||||
|
||||
.inner-content {
|
||||
padding-bottom: 0;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $gray-light;
|
||||
text-transform: uppercase;
|
||||
font-size: $font-size-base;
|
||||
font-weight: normal;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: lighten($gray-light, 30%);
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $gray-light;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul:not(.fa-ul) {
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul.fa-ul {
|
||||
li {
|
||||
.fa-li {
|
||||
left: -11px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&.active .fa-li {
|
||||
left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0 0 15px 0;
|
||||
|
||||
li {
|
||||
a {
|
||||
padding: 5px 0;
|
||||
color: $text-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
visibility: hidden;
|
||||
margin-left: auto;
|
||||
color: lighten($gray-light, 30%);
|
||||
|
||||
&:hover {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
span {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: darken($background-color-alt, 5%);
|
||||
margin-left: -15px;
|
||||
margin-right: -5px;
|
||||
padding-left: 15px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> #items {
|
||||
background-color: $background-color;
|
||||
width: 28%;
|
||||
min-width: 200px;
|
||||
max-width: 350px;
|
||||
border-right: 1px solid $border-color-dark;
|
||||
|
||||
.no-items {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.fa {
|
||||
margin-bottom: 10px;
|
||||
color: $list-icon-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> #details {
|
||||
background-color: $background-color-alt2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.inner-content {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.box {
|
||||
max-width: 550px;
|
||||
margin: 30px auto 0 auto;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
> form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> #logo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.content {
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
min-width: 320px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 282px;
|
||||
opacity: 0.3;
|
||||
transition: all 1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
min-height: 44px;
|
||||
max-height: 44px;
|
||||
background-color: $brand-primary;
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 1px solid darken($brand-primary, 7%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.header-search {
|
||||
.search {
|
||||
padding: 0 7px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
|
||||
.fa {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 15px;
|
||||
color: lighten($brand-primary, 30%);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background: darken($brand-primary, 8%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px 5px 30px;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&:focus {
|
||||
border-radius: $border-radius;
|
||||
outline: none;
|
||||
background: darken($brand-primary, 10%);
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: lighten($brand-primary, 35%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 50px;
|
||||
background-color: $background-color-alt;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid $border-color-dark;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
"jslib/*": [ "jslib/src/*" ],
|
||||
"@angular/*": [ "node_modules/@angular/*" ],
|
||||
"angular2-toaster": [ "node_modules/angular2-toaster" ],
|
||||
"angulartics2": [ "node_modules/angulartics2" ]
|
||||
"angulartics2": [ "node_modules/angulartics2" ],
|
||||
"electron": [ "node_modules/electron" ]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
@@ -22,6 +23,7 @@
|
||||
"jslib/node_modules",
|
||||
"dist",
|
||||
"jslib/dist",
|
||||
"build"
|
||||
"build",
|
||||
"jslib/spec"
|
||||
]
|
||||
}
|
||||
|
||||
66
webpack.main.js
Normal file
66
webpack.main.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: 'pre',
|
||||
loader: 'tslint-loader'
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
jslib: path.join(__dirname, 'jslib/src')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build')
|
||||
}
|
||||
};
|
||||
|
||||
const main = {
|
||||
target: 'electron-main',
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
entry: {
|
||||
'main': './src/main.ts'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.node$/,
|
||||
loader: 'node-loader'
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([
|
||||
path.resolve(__dirname, 'build/*')
|
||||
]),
|
||||
new CopyWebpackPlugin([
|
||||
'./src/package.json',
|
||||
{ from: './src/images', to: 'images' },
|
||||
{ from: './src/locales', to: 'locales' },
|
||||
]),
|
||||
],
|
||||
externals: [nodeExternals()]
|
||||
};
|
||||
|
||||
module.exports = merge(common, main);
|
||||
144
webpack.renderer.js
Normal file
144
webpack.renderer.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const GoogleFontsPlugin = require("google-fonts-webpack-plugin");
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
||||
|
||||
const isVendorModule = (module) => {
|
||||
if (!module.context) {
|
||||
return false;
|
||||
}
|
||||
return module.context.indexOf('node_modules') !== -1;
|
||||
};
|
||||
|
||||
const extractCss = new ExtractTextPlugin({
|
||||
filename: '[name].css',
|
||||
disable: false,
|
||||
allChunks: true
|
||||
});
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: 'pre',
|
||||
loader: 'tslint-loader'
|
||||
},
|
||||
{
|
||||
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
|
||||
loader: '@ngtools/webpack'
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||
exclude: /.*(fontawesome-webfont)\.svg/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'images/',
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
jslib: path.join(__dirname, 'jslib/src')
|
||||
},
|
||||
symlinks: false,
|
||||
modules: [path.resolve('node_modules')]
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build')
|
||||
}
|
||||
};
|
||||
|
||||
const renderer = {
|
||||
target: 'electron-renderer',
|
||||
node: {
|
||||
__dirname: false
|
||||
},
|
||||
entry: {
|
||||
'app/main': './src/app/main.ts'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
loader: 'html-loader'
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
exclude: /loading.svg/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'fonts/',
|
||||
publicPath: '../'
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: extractCss.extract({
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader',
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
}
|
||||
],
|
||||
publicPath: '../'
|
||||
})
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new GoogleFontsPlugin({
|
||||
fonts: [
|
||||
{
|
||||
family: 'Open Sans',
|
||||
variants: ['300', '300italic', '400', '400italic', '600', '600italic',
|
||||
'700', '700italic', '800', '800italic'],
|
||||
subsets: ['cyrillic', 'cyrillic-ext', 'greek', 'greek-ext', 'latin', 'latin-ext']
|
||||
}
|
||||
],
|
||||
formats: ['woff2'],
|
||||
path: 'fonts/',
|
||||
filename: 'css/fonts.css'
|
||||
}),
|
||||
new AngularCompilerPlugin({
|
||||
tsConfigPath: 'tsconfig.json',
|
||||
entryModule: 'src/app/app.module#AppModule',
|
||||
sourceMap: true
|
||||
}),
|
||||
// ref: https://github.com/angular/angular/issues/20357
|
||||
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/,
|
||||
path.resolve(__dirname, './src')),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'app/vendor',
|
||||
chunks: ['app/main'],
|
||||
minChunks: isVendorModule
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
filename: 'index.html',
|
||||
chunks: ['app/vendor', 'app/main']
|
||||
}),
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
filename: '[name].js.map',
|
||||
include: ['app/main.js']
|
||||
}),
|
||||
extractCss
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = merge(common, renderer);
|
||||
Reference in New Issue
Block a user