Abstraction of authenticators. Reworked Gmail authentication.
This commit is contained in:
17
Wino.Authentication/BaseAuthenticator.cs
Normal file
17
Wino.Authentication/BaseAuthenticator.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace Wino.Authentication
|
||||||
|
{
|
||||||
|
public abstract class BaseAuthenticator
|
||||||
|
{
|
||||||
|
public abstract MailProviderType ProviderType { get; }
|
||||||
|
protected IAuthenticatorConfig AuthenticatorConfig { get; }
|
||||||
|
|
||||||
|
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
|
||||||
|
{
|
||||||
|
|
||||||
|
AuthenticatorConfig = authenticatorConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Wino.Authentication/GmailAuthenticator.cs
Normal file
53
Wino.Authentication/GmailAuthenticator.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Google.Apis.Auth.OAuth2;
|
||||||
|
using Google.Apis.Util.Store;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
|
|
||||||
|
namespace Wino.Authentication
|
||||||
|
{
|
||||||
|
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
|
||||||
|
{
|
||||||
|
private const string FileDataStoreFolder = "WinoGmailStore";
|
||||||
|
|
||||||
|
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
|
||||||
|
public bool ProposeCopyAuthURL { get; set; }
|
||||||
|
|
||||||
|
public override MailProviderType ProviderType => MailProviderType.Gmail;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the token information for the given account.
|
||||||
|
/// For gmail, interactivity is automatically handled when you get the token.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">Account to get token for.</param>
|
||||||
|
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
||||||
|
=> GetTokenInformationAsync(account);
|
||||||
|
|
||||||
|
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
var userCredential = await GetGoogleUserCredentialAsync(account);
|
||||||
|
|
||||||
|
if (userCredential.Token.IsStale)
|
||||||
|
{
|
||||||
|
await userCredential.RefreshTokenAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
|
||||||
|
{
|
||||||
|
ClientId = ClientId
|
||||||
|
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(FileDataStoreFolder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Wino.Authentication/Office365Authenticator.cs
Normal file
16
Wino.Authentication/Office365Authenticator.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace Wino.Authentication
|
||||||
|
{
|
||||||
|
public class Office365Authenticator : OutlookAuthenticator
|
||||||
|
{
|
||||||
|
public Office365Authenticator(INativeAppService nativeAppService,
|
||||||
|
IApplicationConfiguration applicationConfiguration,
|
||||||
|
IAuthenticatorConfig authenticatorConfig) : base(nativeAppService, applicationConfiguration, authenticatorConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MailProviderType ProviderType => MailProviderType.Office365;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,16 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.Identity.Client;
|
using Microsoft.Identity.Client;
|
||||||
using Microsoft.Identity.Client.Broker;
|
using Microsoft.Identity.Client.Broker;
|
||||||
using Microsoft.Identity.Client.Extensions.Msal;
|
using Microsoft.Identity.Client.Extensions.Msal;
|
||||||
using Wino.Core.Authenticators.Base;
|
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Exceptions;
|
using Wino.Core.Domain.Exceptions;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Extensions;
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Mail
|
namespace Wino.Authentication
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
||||||
/// Authenticator for Outlook Mail provider.
|
|
||||||
/// Token cache is managed by MSAL, not by Wino.
|
|
||||||
/// </summary>
|
|
||||||
public class OutlookAuthenticator : OutlookAuthenticatorBase
|
|
||||||
{
|
{
|
||||||
private const string TokenCacheFileName = "OutlookCache.bin";
|
private const string TokenCacheFileName = "OutlookCache.bin";
|
||||||
private bool isTokenCacheAttached = false;
|
private bool isTokenCacheAttached = false;
|
||||||
@@ -27,27 +21,14 @@ namespace Wino.Core.Authenticators.Mail
|
|||||||
// Outlook
|
// Outlook
|
||||||
private const string Authority = "https://login.microsoftonline.com/common";
|
private const string Authority = "https://login.microsoftonline.com/common";
|
||||||
|
|
||||||
public override string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208";
|
|
||||||
|
|
||||||
private readonly string[] MailScope =
|
|
||||||
[
|
|
||||||
"email",
|
|
||||||
"mail.readwrite",
|
|
||||||
"offline_access",
|
|
||||||
"mail.send",
|
|
||||||
"Mail.Send.Shared",
|
|
||||||
"Mail.ReadWrite.Shared",
|
|
||||||
"User.Read"
|
|
||||||
];
|
|
||||||
|
|
||||||
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
||||||
|
|
||||||
private readonly IPublicClientApplication _publicClientApplication;
|
private readonly IPublicClientApplication _publicClientApplication;
|
||||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||||
|
|
||||||
public OutlookAuthenticator(ITokenService tokenService,
|
public OutlookAuthenticator(INativeAppService nativeAppService,
|
||||||
INativeAppService nativeAppService,
|
IApplicationConfiguration applicationConfiguration,
|
||||||
IApplicationConfiguration applicationConfiguration) : base(tokenService)
|
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
|
||||||
{
|
{
|
||||||
_applicationConfiguration = applicationConfiguration;
|
_applicationConfiguration = applicationConfiguration;
|
||||||
|
|
||||||
@@ -59,7 +40,7 @@ namespace Wino.Core.Authenticators.Mail
|
|||||||
ListOperatingSystemAccounts = true,
|
ListOperatingSystemAccounts = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var outlookAppBuilder = PublicClientApplicationBuilder.Create(ClientId)
|
var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
||||||
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
|
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
|
||||||
.WithBroker(options)
|
.WithBroker(options)
|
||||||
.WithDefaultRedirectUri()
|
.WithDefaultRedirectUri()
|
||||||
@@ -68,7 +49,9 @@ namespace Wino.Core.Authenticators.Mail
|
|||||||
_publicClientApplication = outlookAppBuilder.Build();
|
_publicClientApplication = outlookAppBuilder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<TokenInformation> GetTokenAsync(MailAccount account)
|
public string[] Scope => AuthenticatorConfig.OutlookScope;
|
||||||
|
|
||||||
|
private async Task EnsureTokenCacheAttachedAsync()
|
||||||
{
|
{
|
||||||
if (!isTokenCacheAttached)
|
if (!isTokenCacheAttached)
|
||||||
{
|
{
|
||||||
@@ -78,23 +61,29 @@ namespace Wino.Core.Authenticators.Mail
|
|||||||
|
|
||||||
isTokenCacheAttached = true;
|
isTokenCacheAttached = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
await EnsureTokenCacheAttachedAsync();
|
||||||
|
|
||||||
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address);
|
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address);
|
||||||
|
|
||||||
// TODO: Handle it from the server.
|
if (storedAccount == null)
|
||||||
if (storedAccount == null) throw new AuthenticationAttentionException(account);
|
return await GenerateTokenInformationAsync(account);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var authResult = await _publicClientApplication.AcquireTokenSilent(MailScope, storedAccount).ExecuteAsync();
|
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
|
||||||
|
|
||||||
return authResult.CreateTokenInformation() ?? throw new Exception("Failed to get Outlook token.");
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||||
}
|
}
|
||||||
catch (MsalUiRequiredException)
|
catch (MsalUiRequiredException)
|
||||||
{
|
{
|
||||||
// Somehow MSAL is not able to refresh the token silently.
|
// Somehow MSAL is not able to refresh the token silently.
|
||||||
// Force interactive login.
|
// Force interactive login.
|
||||||
return await GenerateTokenAsync(account, true);
|
|
||||||
|
return await GenerateTokenInformationAsync(account);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -102,22 +91,26 @@ namespace Wino.Core.Authenticators.Mail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await EnsureTokenCacheAttachedAsync();
|
||||||
|
|
||||||
var authResult = await _publicClientApplication
|
var authResult = await _publicClientApplication
|
||||||
.AcquireTokenInteractive(MailScope)
|
.AcquireTokenInteractive(Scope)
|
||||||
.ExecuteAsync();
|
.ExecuteAsync();
|
||||||
|
|
||||||
var tokenInformation = authResult.CreateTokenInformation();
|
// If the account is null, it means it's the initial creation of it.
|
||||||
|
// If not, make sure the authenticated user address matches the username.
|
||||||
|
// When people refresh their token, accounts must match.
|
||||||
|
|
||||||
if (saveToken)
|
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await SaveTokenInternalAsync(account, tokenInformation);
|
throw new AuthenticationException("Authenticated address does not match with your account address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenInformation;
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||||
}
|
}
|
||||||
catch (MsalClientException msalClientException)
|
catch (MsalClientException msalClientException)
|
||||||
{
|
{
|
||||||
24
Wino.Authentication/Wino.Authentication.csproj
Normal file
24
Wino.Authentication/Wino.Authentication.csproj
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RootNamespace>Wino.Authentication</RootNamespace>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
|
<Platforms>AnyCPU;x64;x86</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.3.2" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||||
|
<PackageReference Include="Google.Apis.Auth" Version="1.68.0" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.1" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.66.1" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.66.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
using Wino.Core.Domain.Models.Authentication;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Shared
|
|
||||||
{
|
|
||||||
public class TokenInformation : TokenInformationBase
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unique object storage for authenticators if needed.
|
|
||||||
/// </summary>
|
|
||||||
public string UniqueId { get; set; }
|
|
||||||
public string Address { get; set; }
|
|
||||||
|
|
||||||
public void RefreshTokens(TokenInformationBase tokenInformationBase)
|
|
||||||
{
|
|
||||||
if (tokenInformationBase == null)
|
|
||||||
throw new ArgumentNullException(nameof(tokenInformationBase));
|
|
||||||
|
|
||||||
AccessToken = tokenInformationBase.AccessToken;
|
|
||||||
RefreshToken = tokenInformationBase.RefreshToken;
|
|
||||||
ExpiresAt = tokenInformationBase.ExpiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// Creates new account with the given server information if any.
|
/// Creates new account with the given server information if any.
|
||||||
/// Also sets the account as Startup account if there are no accounts.
|
/// Also sets the account as Startup account if there are no accounts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation);
|
Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fixed authentication errors for account by forcing interactive login.
|
/// Fixed authentication errors for account by forcing interactive login.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces
|
namespace Wino.Core.Domain.Interfaces
|
||||||
{
|
{
|
||||||
@@ -11,26 +12,28 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
MailProviderType ProviderType { get; }
|
MailProviderType ProviderType { get; }
|
||||||
|
|
||||||
/// <summary>
|
Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account);
|
||||||
/// Gets the token from the cache if exists.
|
|
||||||
/// If the token is expired, tries to refresh.
|
|
||||||
/// This can throw AuthenticationAttentionException if silent refresh fails.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="account">Account to get token for.</param>
|
|
||||||
/// <returns>Valid token info to be used in integrators.</returns>
|
|
||||||
Task<TokenInformation> GetTokenAsync(MailAccount account);
|
|
||||||
|
|
||||||
/// <summary>
|
Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account);
|
||||||
/// Initial creation of token. Requires user interaction.
|
|
||||||
/// This will cache the token but still returns for account creation
|
|
||||||
/// since account address is required.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Freshly created TokenInformation..</returns>
|
|
||||||
Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
|
||||||
|
|
||||||
/// <summary>
|
///// <summary>
|
||||||
/// ClientId in case of needed for authorization/authentication.
|
///// Gets the token for the given account from the cache.
|
||||||
/// </summary>
|
///// Forces interactive login if the token is not found.
|
||||||
string ClientId { get; }
|
///// </summary>
|
||||||
|
///// <param name="account">Account to get access token for.</param>
|
||||||
|
///// <returns>Access token</returns>
|
||||||
|
//Task<string> GetTokenAsync(MailAccount account);
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// Forces an interactive login to get the token for the given account.
|
||||||
|
///// </summary>
|
||||||
|
///// <param name="account">Account to get access token for.</param>
|
||||||
|
///// <returns>Access token</returns>
|
||||||
|
//// Task<string> GenerateTokenAsync(MailAccount account);
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// ClientId in case of needed for authorization/authentication.
|
||||||
|
///// </summary>
|
||||||
|
//string ClientId { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs
Normal file
10
Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
public interface IAuthenticatorConfig
|
||||||
|
{
|
||||||
|
string OutlookAuthenticatorClientId { get; }
|
||||||
|
string[] OutlookScope { get; }
|
||||||
|
string GmailAuthenticatorClientId { get; }
|
||||||
|
string[] GmailScope { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@
|
|||||||
{
|
{
|
||||||
bool ProposeCopyAuthURL { get; set; }
|
bool ProposeCopyAuthURL { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IImapAuthenticator : IAuthenticator { }
|
public interface IImapAuthenticator : IAuthenticator { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Models.Authorization;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces
|
namespace Wino.Core.Domain.Interfaces
|
||||||
{
|
{
|
||||||
@@ -13,13 +11,6 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
Task LaunchFileAsync(string filePath);
|
Task LaunchFileAsync(string filePath);
|
||||||
Task<bool> LaunchUriAsync(Uri uri);
|
Task<bool> LaunchUriAsync(Uri uri);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Launches the default browser with the specified uri and waits for protocol activation to finish.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="authenticator"></param>
|
|
||||||
/// <returns>Response callback from the browser.</returns>
|
|
||||||
Task<Uri> GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finalizes GetAuthorizationResponseUriAsync for current IAuthenticator.
|
/// Finalizes GetAuthorizationResponseUriAsync for current IAuthenticator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -32,11 +23,6 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
|
|
||||||
Task PinAppToTaskbarAsync();
|
Task PinAppToTaskbarAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Some cryptographic shit is needed for requesting Google authentication in UWP.
|
|
||||||
/// </summary>
|
|
||||||
GoogleAuthorizationRequest GetGoogleAuthorizationRequest();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the function that returns a pointer for main window hwnd for UWP.
|
/// Gets or sets the function that returns a pointer for main window hwnd for UWP.
|
||||||
/// This is used to display WAM broker dialog on running UWP app called by a windowless server code.
|
/// This is used to display WAM broker dialog on running UWP app called by a windowless server code.
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="SenderName">Display sender name for the account.</param>
|
/// <param name="SenderName">Display sender name for the account.</param>
|
||||||
/// <param name="Base64ProfilePictureData">Base 64 encoded profile picture data of the account. Thumbnail size.</param>
|
/// <param name="Base64ProfilePictureData">Base 64 encoded profile picture data of the account. Thumbnail size.</param>
|
||||||
public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
|
/// <param name="AccountAddress">Address of the profile.</param>
|
||||||
|
public record ProfileInformation(string SenderName, string Base64ProfilePictureData, string AccountAddress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Authentication
|
|
||||||
{
|
|
||||||
public class TokenInformationBase
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UTC date for token expiration.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime ExpiresAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the value indicating whether the token is expired or not.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs
Normal file
11
Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Wino.Core.Domain.Models.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Previously known as TokenInformation.
|
||||||
|
/// We used to store this model in the database.
|
||||||
|
/// Now we store it in the memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AccessToken">Access token/</param>
|
||||||
|
/// <param name="AccountAddress">Address of the authenticated user.</param>
|
||||||
|
public record TokenInformationEx(string AccessToken, string AccountAddress);
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nito.AsyncEx;
|
|
||||||
using Windows.ApplicationModel;
|
using Windows.ApplicationModel;
|
||||||
using Windows.Foundation.Metadata;
|
using Windows.Foundation.Metadata;
|
||||||
using Windows.Security.Authentication.Web;
|
using Windows.Security.Authentication.Web;
|
||||||
@@ -11,8 +9,6 @@ using Windows.Storage;
|
|||||||
using Windows.Storage.Streams;
|
using Windows.Storage.Streams;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
using Windows.UI.Shell;
|
using Windows.UI.Shell;
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Exceptions;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Authorization;
|
using Wino.Core.Domain.Models.Authorization;
|
||||||
|
|
||||||
@@ -156,26 +152,6 @@ namespace Wino.Services
|
|||||||
await taskbarManager.RequestPinCurrentAppAsync();
|
await taskbarManager.RequestPinCurrentAppAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> GetAuthorizationResponseUriAsync(IAuthenticator authenticator,
|
|
||||||
string authorizationUri,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (authorizationCompletedTaskSource != null)
|
|
||||||
{
|
|
||||||
authorizationCompletedTaskSource.TrySetException(new AuthenticationException(Translator.Exception_AuthenticationCanceled));
|
|
||||||
authorizationCompletedTaskSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
authorizationCompletedTaskSource = new TaskCompletionSource<Uri>();
|
|
||||||
|
|
||||||
bool isLaunched = await Launcher.LaunchUriAsync(new Uri(authorizationUri)).AsTask(cancellationToken);
|
|
||||||
|
|
||||||
if (!isLaunched)
|
|
||||||
throw new WinoServerException("Failed to launch Google Authentication dialog.");
|
|
||||||
|
|
||||||
return await authorizationCompletedTaskSource.Task.WaitAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ContinueAuthorization(Uri authorizationResponseUri)
|
public void ContinueAuthorization(Uri authorizationResponseUri)
|
||||||
{
|
{
|
||||||
if (authorizationCompletedTaskSource != null)
|
if (authorizationCompletedTaskSource != null)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
|
||||||
xmlns:coreControls="using:Wino.Core.UWP.Controls"
|
xmlns:coreControls="using:Wino.Core.UWP.Controls"
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:local="using:Wino.Core.UWP.Styles"
|
xmlns:local="using:Wino.Core.UWP.Styles"
|
||||||
xmlns:menu="using:Wino.Core.MenuItems"
|
xmlns:menu="using:Wino.Core.MenuItems"
|
||||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Base
|
|
||||||
{
|
|
||||||
public abstract class BaseAuthenticator
|
|
||||||
{
|
|
||||||
public abstract MailProviderType ProviderType { get; }
|
|
||||||
|
|
||||||
protected ITokenService TokenService { get; }
|
|
||||||
|
|
||||||
protected BaseAuthenticator(ITokenService tokenService)
|
|
||||||
{
|
|
||||||
TokenService = tokenService;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Task SaveTokenInternalAsync(MailAccount account, TokenInformation tokenInformation)
|
|
||||||
=> TokenService.SaveTokenInformationAsync(account.Id, tokenInformation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Base
|
|
||||||
{
|
|
||||||
public abstract class GmailAuthenticatorBase : BaseAuthenticator, IGmailAuthenticator
|
|
||||||
{
|
|
||||||
protected GmailAuthenticatorBase(ITokenService tokenService) : base(tokenService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract string ClientId { get; }
|
|
||||||
public bool ProposeCopyAuthURL { get; set; }
|
|
||||||
|
|
||||||
public abstract Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
|
||||||
|
|
||||||
public abstract Task<TokenInformation> GetTokenAsync(MailAccount account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Base
|
|
||||||
{
|
|
||||||
public abstract class OutlookAuthenticatorBase : BaseAuthenticator, IOutlookAuthenticator
|
|
||||||
{
|
|
||||||
protected OutlookAuthenticatorBase(ITokenService tokenService) : base(tokenService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract string ClientId { get; }
|
|
||||||
|
|
||||||
public abstract Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
|
||||||
|
|
||||||
public abstract Task<TokenInformation> GetTokenAsync(MailAccount account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Authenticators.Base;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Calendar
|
|
||||||
{
|
|
||||||
public class OutlookAuthenticator : OutlookAuthenticatorBase
|
|
||||||
{
|
|
||||||
public OutlookAuthenticator(ITokenService tokenService) : base(tokenService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ClientId => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
|
||||||
|
|
||||||
public override Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<TokenInformation> GetTokenAsync(MailAccount account)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Wino.Core.Authenticators.Base;
|
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Exceptions;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Authentication;
|
|
||||||
using Wino.Core.Domain.Models.Authorization;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
using Wino.Messaging.UI;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Mail
|
|
||||||
{
|
|
||||||
public class GmailAuthenticator : GmailAuthenticatorBase
|
|
||||||
{
|
|
||||||
public override string ClientId { get; } = "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
|
|
||||||
|
|
||||||
private const string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
|
|
||||||
private const string RefreshTokenEndpoint = "https://oauth2.googleapis.com/token";
|
|
||||||
private const string UserInfoEndpoint = "https://gmail.googleapis.com/gmail/v1/users/me/profile";
|
|
||||||
|
|
||||||
public override MailProviderType ProviderType => MailProviderType.Gmail;
|
|
||||||
|
|
||||||
private readonly INativeAppService _nativeAppService;
|
|
||||||
|
|
||||||
public GmailAuthenticator(ITokenService tokenService, INativeAppService nativeAppService) : base(tokenService)
|
|
||||||
{
|
|
||||||
_nativeAppService = nativeAppService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs tokenization code exchange and retrieves the actual Access - Refresh tokens from Google
|
|
||||||
/// after redirect uri returns from browser.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="tokenizationRequest">Tokenization request.</param>
|
|
||||||
/// <exception cref="GoogleAuthenticationException">In case of network or parsing related error.</exception>
|
|
||||||
private async Task<TokenInformation> PerformCodeExchangeAsync(GoogleTokenizationRequest tokenizationRequest)
|
|
||||||
{
|
|
||||||
var uri = tokenizationRequest.BuildRequest();
|
|
||||||
|
|
||||||
var content = new StringContent(uri, Encoding.UTF8, "application/x-www-form-urlencoded");
|
|
||||||
|
|
||||||
var handler = new HttpClientHandler()
|
|
||||||
{
|
|
||||||
AllowAutoRedirect = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = new HttpClient(handler);
|
|
||||||
|
|
||||||
var response = await client.PostAsync(TokenEndpoint, content);
|
|
||||||
string responseString = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed);
|
|
||||||
|
|
||||||
var parsed = JsonNode.Parse(responseString).AsObject();
|
|
||||||
|
|
||||||
if (parsed.ContainsKey("error"))
|
|
||||||
throw new GoogleAuthenticationException(parsed["error"]["message"].GetValue<string>());
|
|
||||||
|
|
||||||
var accessToken = parsed["access_token"].GetValue<string>();
|
|
||||||
var refreshToken = parsed["refresh_token"].GetValue<string>();
|
|
||||||
var expiresIn = parsed["expires_in"].GetValue<long>();
|
|
||||||
|
|
||||||
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
|
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
|
||||||
|
|
||||||
// Get basic user info for UserName.
|
|
||||||
|
|
||||||
var userinfoResponse = await client.GetAsync(UserInfoEndpoint);
|
|
||||||
string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
var parsedUserInfo = JsonNode.Parse(userinfoResponseContent).AsObject();
|
|
||||||
|
|
||||||
if (parsedUserInfo.ContainsKey("error"))
|
|
||||||
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].GetValue<string>());
|
|
||||||
|
|
||||||
var username = parsedUserInfo["emailAddress"].GetValue<string>();
|
|
||||||
|
|
||||||
return new TokenInformation()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Address = username,
|
|
||||||
AccessToken = accessToken,
|
|
||||||
RefreshToken = refreshToken,
|
|
||||||
ExpiresAt = expirationDate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async override Task<TokenInformation> GetTokenAsync(MailAccount account)
|
|
||||||
{
|
|
||||||
var cachedToken = await TokenService.GetTokenInformationAsync(account.Id)
|
|
||||||
?? throw new AuthenticationAttentionException(account);
|
|
||||||
|
|
||||||
if (cachedToken.IsExpired)
|
|
||||||
{
|
|
||||||
// Refresh token with new exchanges.
|
|
||||||
// No need to check Username for account.
|
|
||||||
|
|
||||||
var refreshedTokenInfoBase = await RefreshTokenAsync(cachedToken.RefreshToken);
|
|
||||||
|
|
||||||
cachedToken.RefreshTokens(refreshedTokenInfoBase);
|
|
||||||
|
|
||||||
// Save new token and return.
|
|
||||||
await SaveTokenInternalAsync(account, cachedToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async override Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
|
||||||
{
|
|
||||||
var authRequest = _nativeAppService.GetGoogleAuthorizationRequest();
|
|
||||||
|
|
||||||
var authorizationUri = authRequest.BuildRequest(ClientId);
|
|
||||||
|
|
||||||
Uri responseRedirectUri = null;
|
|
||||||
|
|
||||||
if (ProposeCopyAuthURL)
|
|
||||||
{
|
|
||||||
WeakReferenceMessenger.Default.Send(new CopyAuthURLRequested(authorizationUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
responseRedirectUri = await _nativeAppService.GetAuthorizationResponseUriAsync(this, authorizationUri);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
throw new AuthenticationException(Translator.Exception_AuthenticationCanceled);
|
|
||||||
}
|
|
||||||
|
|
||||||
authRequest.ValidateAuthorizationCode(responseRedirectUri);
|
|
||||||
|
|
||||||
// Start tokenization.
|
|
||||||
var tokenizationRequest = new GoogleTokenizationRequest(authRequest);
|
|
||||||
var tokenInformation = await PerformCodeExchangeAsync(tokenizationRequest);
|
|
||||||
|
|
||||||
if (saveToken)
|
|
||||||
{
|
|
||||||
await SaveTokenInternalAsync(account, tokenInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenInformation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internally exchanges refresh token with a new access token and returns new TokenInformation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="refresh_token">Token to be used in refreshing.</param>
|
|
||||||
/// <returns>New TokenInformationBase that has new tokens and expiration date without a username. This token is not saved to database after returned.</returns>
|
|
||||||
private async Task<TokenInformationBase> RefreshTokenAsync(string refresh_token)
|
|
||||||
{
|
|
||||||
// TODO: This doesn't work.
|
|
||||||
var refreshUri = string.Format("client_id={0}&refresh_token={1}&grant_type=refresh_token", ClientId, refresh_token);
|
|
||||||
|
|
||||||
//Uri.EscapeDataString(refreshUri);
|
|
||||||
var content = new StringContent(refreshUri, Encoding.UTF8, "application/x-www-form-urlencoded");
|
|
||||||
|
|
||||||
var client = new HttpClient();
|
|
||||||
|
|
||||||
var response = await client.PostAsync(RefreshTokenEndpoint, content);
|
|
||||||
|
|
||||||
string responseString = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
var parsed = JsonNode.Parse(responseString).AsObject();
|
|
||||||
|
|
||||||
// TODO: Error parsing is incorrect.
|
|
||||||
if (parsed.ContainsKey("error"))
|
|
||||||
throw new GoogleAuthenticationException(parsed["error_description"].GetValue<string>());
|
|
||||||
|
|
||||||
var accessToken = parsed["access_token"].GetValue<string>();
|
|
||||||
|
|
||||||
string activeRefreshToken = refresh_token;
|
|
||||||
|
|
||||||
// Refresh token might not be returned.
|
|
||||||
// In this case older refresh token is still available for new refreshes.
|
|
||||||
// Only change if provided.
|
|
||||||
|
|
||||||
if (parsed.ContainsKey("refresh_token"))
|
|
||||||
{
|
|
||||||
activeRefreshToken = parsed["refresh_token"].GetValue<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiresIn = parsed["expires_in"].GetValue<long>();
|
|
||||||
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
|
|
||||||
|
|
||||||
return new TokenInformationBase()
|
|
||||||
{
|
|
||||||
AccessToken = accessToken,
|
|
||||||
ExpiresAt = expirationDate,
|
|
||||||
RefreshToken = activeRefreshToken
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Authenticators.Mail
|
|
||||||
{
|
|
||||||
public class Office365Authenticator : OutlookAuthenticator
|
|
||||||
{
|
|
||||||
public Office365Authenticator(ITokenService tokenService, INativeAppService nativeAppService, IApplicationConfiguration applicationConfiguration) : base(tokenService, nativeAppService, applicationConfiguration) { }
|
|
||||||
|
|
||||||
public override MailProviderType ProviderType => MailProviderType.Office365;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Serilog.Core;
|
using Serilog.Core;
|
||||||
using Wino.Core.Authenticators.Mail;
|
using Wino.Authentication;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Integration.Processors;
|
using Wino.Core.Integration.Processors;
|
||||||
using Wino.Core.Integration.Threading;
|
using Wino.Core.Integration.Threading;
|
||||||
@@ -28,7 +28,6 @@ namespace Wino.Core
|
|||||||
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
|
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
|
||||||
services.AddTransient<IOutlookChangeProcessor, OutlookChangeProcessor>();
|
services.AddTransient<IOutlookChangeProcessor, OutlookChangeProcessor>();
|
||||||
|
|
||||||
services.AddTransient<ITokenService, TokenService>();
|
|
||||||
services.AddTransient<IFolderService, FolderService>();
|
services.AddTransient<IFolderService, FolderService>();
|
||||||
services.AddTransient<IMailService, MailService>();
|
services.AddTransient<IMailService, MailService>();
|
||||||
services.AddTransient<IAccountService, AccountService>();
|
services.AddTransient<IAccountService, AccountService>();
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.Identity.Client;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
namespace Wino.Core.Extensions
|
|
||||||
{
|
|
||||||
public static class TokenizationExtensions
|
|
||||||
{
|
|
||||||
public static TokenInformation CreateTokenInformation(this AuthenticationResult clientBuilderResult)
|
|
||||||
{
|
|
||||||
// Plain access token info is not stored for Outlook in Wino's database.
|
|
||||||
// Here we store UniqueId and Access Token in memory only to compare the UniqueId returned from MSAL auth result.
|
|
||||||
|
|
||||||
var tokenInfo = new TokenInformation()
|
|
||||||
{
|
|
||||||
Address = clientBuilderResult.Account.Username,
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
UniqueId = clientBuilderResult.UniqueId,
|
|
||||||
AccessToken = clientBuilderResult.AccessToken
|
|
||||||
};
|
|
||||||
|
|
||||||
return tokenInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,29 @@
|
|||||||
using System;
|
using System.Net.Http;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Google.Apis.Http;
|
using Google.Apis.Http;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Core.Http
|
namespace Wino.Core.Http
|
||||||
{
|
{
|
||||||
internal class GmailClientMessageHandler : ConfigurableMessageHandler
|
internal class GmailClientMessageHandler : ConfigurableMessageHandler
|
||||||
{
|
{
|
||||||
public Func<Task<TokenInformation>> TokenRetrieveDelegate { get; }
|
private readonly IGmailAuthenticator _gmailAuthenticator;
|
||||||
|
private readonly MailAccount _mailAccount;
|
||||||
|
|
||||||
public GmailClientMessageHandler(Func<Task<TokenInformation>> tokenRetrieveDelegate) : base(new HttpClientHandler())
|
public GmailClientMessageHandler(IGmailAuthenticator gmailAuthenticator, MailAccount mailAccount) : base(new HttpClientHandler())
|
||||||
{
|
{
|
||||||
TokenRetrieveDelegate = tokenRetrieveDelegate;
|
_gmailAuthenticator = gmailAuthenticator;
|
||||||
|
_mailAccount = mailAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tokenizationTask = TokenRetrieveDelegate.Invoke();
|
// This call here will automatically trigger Google Auth's interactive login if the token is not found.
|
||||||
var tokenInformation = await tokenizationTask;
|
// or refresh the token based on the FileDataStore.
|
||||||
|
|
||||||
|
var tokenInformation = await _gmailAuthenticator.GetTokenInformationAsync(_mailAccount);
|
||||||
|
|
||||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInformation.AccessToken);
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInformation.AccessToken);
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ namespace Wino.Core.Http
|
|||||||
Dictionary<string, object> additionalAuthenticationContext = null,
|
Dictionary<string, object> additionalAuthenticationContext = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var token = await _authenticator.GetTokenAsync(_account).ConfigureAwait(false);
|
var tokenInfo = await _authenticator.GetTokenInformationAsync(_account);
|
||||||
|
|
||||||
return token?.AccessToken;
|
return tokenInfo.AccessToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,8 +206,9 @@ namespace Wino.Core.Services
|
|||||||
var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);
|
var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);
|
||||||
|
|
||||||
// This will re-generate token.
|
// This will re-generate token.
|
||||||
var token = await authenticator.GenerateTokenAsync(account, true);
|
var token = await authenticator.GenerateTokenInformationAsync(account);
|
||||||
|
|
||||||
|
// TODO: Rest?
|
||||||
Guard.IsNotNull(token);
|
Guard.IsNotNull(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,10 +268,10 @@ namespace Wino.Core.Services
|
|||||||
public async Task DeleteAccountAsync(MailAccount account)
|
public async Task DeleteAccountAsync(MailAccount account)
|
||||||
{
|
{
|
||||||
// TODO: Delete mime messages and attachments.
|
// TODO: Delete mime messages and attachments.
|
||||||
|
// TODO: Delete token cache by underlying provider.
|
||||||
|
|
||||||
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);
|
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);
|
||||||
|
|
||||||
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
|
|
||||||
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
|
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
|
||||||
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
|
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
|
||||||
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
|
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
|
||||||
@@ -333,6 +334,10 @@ namespace Wino.Core.Services
|
|||||||
account.SenderName = profileInformation.SenderName;
|
account.SenderName = profileInformation.SenderName;
|
||||||
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(account.Address))
|
||||||
|
{
|
||||||
|
account.Address = profileInformation.AccountAddress;
|
||||||
|
}
|
||||||
// Forcefully add or update a contact data with the provided information.
|
// Forcefully add or update a contact data with the provided information.
|
||||||
|
|
||||||
var accountContact = new AccountContact()
|
var accountContact = new AccountContact()
|
||||||
@@ -469,7 +474,7 @@ namespace Wino.Core.Services
|
|||||||
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
|
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
|
public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation)
|
||||||
{
|
{
|
||||||
Guard.IsNotNull(account);
|
Guard.IsNotNull(account);
|
||||||
|
|
||||||
@@ -518,12 +523,6 @@ namespace Wino.Core.Services
|
|||||||
|
|
||||||
if (customServerInformation != null)
|
if (customServerInformation != null)
|
||||||
await Connection.InsertAsync(customServerInformation);
|
await Connection.InsertAsync(customServerInformation);
|
||||||
|
|
||||||
// Outlook token cache is managed by MSAL.
|
|
||||||
// Don't save it to database.
|
|
||||||
|
|
||||||
if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
|
|
||||||
await Connection.InsertAsync(tokenInformation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
|
public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using Wino.Core.Authenticators.Mail;
|
using Wino.Authentication;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -10,14 +10,16 @@ namespace Wino.Core.Services
|
|||||||
public class AuthenticationProvider : IAuthenticationProvider
|
public class AuthenticationProvider : IAuthenticationProvider
|
||||||
{
|
{
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly ITokenService _tokenService;
|
|
||||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||||
|
private readonly IAuthenticatorConfig _authenticatorConfig;
|
||||||
|
|
||||||
public AuthenticationProvider(INativeAppService nativeAppService, ITokenService tokenService, IApplicationConfiguration applicationConfiguration)
|
public AuthenticationProvider(INativeAppService nativeAppService,
|
||||||
|
IApplicationConfiguration applicationConfiguration,
|
||||||
|
IAuthenticatorConfig authenticatorConfig)
|
||||||
{
|
{
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
_tokenService = tokenService;
|
|
||||||
_applicationConfiguration = applicationConfiguration;
|
_applicationConfiguration = applicationConfiguration;
|
||||||
|
_authenticatorConfig = authenticatorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAuthenticator GetAuthenticator(MailProviderType providerType)
|
public IAuthenticator GetAuthenticator(MailProviderType providerType)
|
||||||
@@ -25,9 +27,9 @@ namespace Wino.Core.Services
|
|||||||
// TODO: Move DI
|
// TODO: Move DI
|
||||||
return providerType switch
|
return providerType switch
|
||||||
{
|
{
|
||||||
MailProviderType.Outlook => new OutlookAuthenticator(_tokenService, _nativeAppService, _applicationConfiguration),
|
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
||||||
MailProviderType.Office365 => new Office365Authenticator(_tokenService, _nativeAppService, _applicationConfiguration),
|
MailProviderType.Office365 => new Office365Authenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
||||||
MailProviderType.Gmail => new GmailAuthenticator(_tokenService, _nativeAppService),
|
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
|
||||||
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
@@ -35,16 +34,7 @@ namespace Wino.Core.Services
|
|||||||
var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath;
|
var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath;
|
||||||
var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName);
|
var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName);
|
||||||
|
|
||||||
Connection = new SQLiteAsyncConnection(databaseFileName)
|
Connection = new SQLiteAsyncConnection(databaseFileName);
|
||||||
{
|
|
||||||
// Enable for debugging sqlite.
|
|
||||||
Trace = true,
|
|
||||||
Tracer = new Action<string>((t) =>
|
|
||||||
{
|
|
||||||
// Debug.WriteLine(t);
|
|
||||||
// Log.Debug(t);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
await CreateTablesAsync();
|
await CreateTablesAsync();
|
||||||
|
|
||||||
@@ -57,7 +47,6 @@ namespace Wino.Core.Services
|
|||||||
typeof(MailCopy),
|
typeof(MailCopy),
|
||||||
typeof(MailItemFolder),
|
typeof(MailItemFolder),
|
||||||
typeof(MailAccount),
|
typeof(MailAccount),
|
||||||
typeof(TokenInformation),
|
|
||||||
typeof(AccountContact),
|
typeof(AccountContact),
|
||||||
typeof(CustomServerInformation),
|
typeof(CustomServerInformation),
|
||||||
typeof(AccountSignature),
|
typeof(AccountSignature),
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
namespace Wino.Core.Services
|
|
||||||
{
|
|
||||||
public interface ITokenService
|
|
||||||
{
|
|
||||||
Task<TokenInformation> GetTokenInformationAsync(Guid accountId);
|
|
||||||
Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TokenService : BaseDatabaseService, ITokenService
|
|
||||||
{
|
|
||||||
public TokenService(IDatabaseService databaseService) : base(databaseService) { }
|
|
||||||
|
|
||||||
public Task<TokenInformation> GetTokenInformationAsync(Guid accountId)
|
|
||||||
=> Connection.Table<TokenInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
|
||||||
|
|
||||||
public async Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation)
|
|
||||||
{
|
|
||||||
// Delete all tokens for this account.
|
|
||||||
await Connection.Table<TokenInformation>().DeleteAsync(a => a.AccountId == accountId);
|
|
||||||
|
|
||||||
// Save new token info to the account.
|
|
||||||
tokenInformation.AccountId = accountId;
|
|
||||||
|
|
||||||
await Connection.InsertOrReplaceAsync(tokenInformation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -119,6 +119,11 @@ namespace Wino.Core.Synchronizers
|
|||||||
{
|
{
|
||||||
Account.SenderName = profileInformation.SenderName;
|
Account.SenderName = profileInformation.SenderName;
|
||||||
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
|
||||||
|
{
|
||||||
|
Account.Address = profileInformation.AccountAddress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return profileInformation;
|
return profileInformation;
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
private readonly GmailService _gmailService;
|
private readonly GmailService _gmailService;
|
||||||
private readonly PeopleServiceService _peopleService;
|
private readonly PeopleServiceService _peopleService;
|
||||||
|
|
||||||
private readonly IAuthenticator _authenticator;
|
private readonly IGmailAuthenticator _authenticator;
|
||||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||||
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
||||||
|
|
||||||
public GmailSynchronizer(MailAccount account,
|
public GmailSynchronizer(MailAccount account,
|
||||||
IAuthenticator authenticator,
|
IGmailAuthenticator authenticator,
|
||||||
IGmailChangeProcessor gmailChangeProcessor) : base(account)
|
IGmailChangeProcessor gmailChangeProcessor) : base(account)
|
||||||
{
|
{
|
||||||
var messageHandler = new GmailClientMessageHandler(() => _authenticator.GetTokenAsync(Account));
|
var messageHandler = new GmailClientMessageHandler(authenticator, account);
|
||||||
|
|
||||||
var initializer = new BaseClientService.Initializer()
|
var initializer = new BaseClientService.Initializer()
|
||||||
{
|
{
|
||||||
@@ -77,8 +77,12 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
var profileRequest = _peopleService.People.Get("people/me");
|
var profileRequest = _peopleService.People.Get("people/me");
|
||||||
profileRequest.PersonFields = "names,photos";
|
profileRequest.PersonFields = "names,photos";
|
||||||
|
|
||||||
string senderName = string.Empty, base64ProfilePicture = string.Empty;
|
string senderName = string.Empty, base64ProfilePicture = string.Empty, address = string.Empty;
|
||||||
|
|
||||||
|
var gmailUserData = _gmailService.Users.GetProfile("me");
|
||||||
|
var gmailProfile = await gmailUserData.ExecuteAsync();
|
||||||
|
|
||||||
|
address = gmailProfile.EmailAddress;
|
||||||
var userProfile = await profileRequest.ExecuteAsync();
|
var userProfile = await profileRequest.ExecuteAsync();
|
||||||
|
|
||||||
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
|
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
|
||||||
@@ -90,7 +94,7 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
|
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProfileInformation(senderName, base64ProfilePicture);
|
return new ProfileInformation(senderName, base64ProfilePicture, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task SynchronizeAliasesAsync()
|
protected override async Task SynchronizeAliasesAsync()
|
||||||
|
|||||||
@@ -533,20 +533,20 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the user's display name.
|
/// Get the user's display name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Display name of the user.</returns>
|
/// <returns>Display name and address of the user.</returns>
|
||||||
private async Task<string> GetSenderNameAsync()
|
private async Task<Tuple<string, string>> GetDisplayNameAndAddressAsync()
|
||||||
{
|
{
|
||||||
var userInfo = await _graphClient.Me.GetAsync();
|
var userInfo = await _graphClient.Me.GetAsync();
|
||||||
|
|
||||||
return userInfo.DisplayName;
|
return new Tuple<string, string>(userInfo.DisplayName, userInfo.Mail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<ProfileInformation> GetProfileInformationAsync()
|
public override async Task<ProfileInformation> GetProfileInformationAsync()
|
||||||
{
|
{
|
||||||
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
|
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
|
||||||
var senderName = await GetSenderNameAsync().ConfigureAwait(false);
|
var displayNameAndAddress = await GetDisplayNameAndAddressAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
return new ProfileInformation(senderName, profilePictureData);
|
return new ProfileInformation(displayNameAndAddress.Item1, profilePictureData, displayNameAndAddress.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using Wino.Core.Domain.Entities.Shared;
|
|||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Exceptions;
|
using Wino.Core.Domain.Exceptions;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.ViewModels;
|
using Wino.Core.ViewModels;
|
||||||
@@ -107,7 +108,7 @@ namespace Wino.Mail.ViewModels
|
|||||||
creationDialog.ShowDialog(accountCreationCancellationTokenSource);
|
creationDialog.ShowDialog(accountCreationCancellationTokenSource);
|
||||||
creationDialog.State = AccountCreationDialogState.SigningIn;
|
creationDialog.State = AccountCreationDialogState.SigningIn;
|
||||||
|
|
||||||
TokenInformation tokenInformation = null;
|
string tokenInformation = string.Empty;
|
||||||
|
|
||||||
// Custom server implementation requires more async waiting.
|
// Custom server implementation requires more async waiting.
|
||||||
if (creationDialog is ICustomServerAccountCreationDialog customServerDialog)
|
if (creationDialog is ICustomServerAccountCreationDialog customServerDialog)
|
||||||
@@ -129,24 +130,26 @@ namespace Wino.Mail.ViewModels
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For OAuth authentications, we just generate token and assign it to the MailAccount.
|
// OAuth authentication is handled here.
|
||||||
|
// Server authenticates, returns the token info here.
|
||||||
|
|
||||||
var tokenInformationResponse = await WinoServerConnectionManager
|
var tokenInformationResponse = await WinoServerConnectionManager
|
||||||
.GetResponseAsync<TokenInformation, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
|
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
|
||||||
createdAccount,
|
createdAccount,
|
||||||
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
|
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
|
||||||
|
|
||||||
if (creationDialog.State == AccountCreationDialogState.Canceled)
|
if (creationDialog.State == AccountCreationDialogState.Canceled)
|
||||||
throw new AccountSetupCanceledException();
|
throw new AccountSetupCanceledException();
|
||||||
|
|
||||||
tokenInformationResponse.ThrowIfFailed();
|
createdAccount.Address = tokenInformationResponse.Data.AccountAddress;
|
||||||
|
|
||||||
tokenInformation = tokenInformationResponse.Data;
|
tokenInformationResponse.ThrowIfFailed();
|
||||||
createdAccount.Address = tokenInformation.Address;
|
|
||||||
tokenInformation.AccountId = createdAccount.Id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await AccountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
|
// Address is still doesn't have a value for API synchronizers.
|
||||||
|
// It'll be synchronized with profile information.
|
||||||
|
|
||||||
|
await AccountService.CreateAccountAsync(createdAccount, customServerInformation);
|
||||||
|
|
||||||
// Local account has been created.
|
// Local account has been created.
|
||||||
|
|
||||||
@@ -172,6 +175,11 @@ namespace Wino.Mail.ViewModels
|
|||||||
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
|
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
|
||||||
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
|
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress))
|
||||||
|
{
|
||||||
|
createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress;
|
||||||
|
}
|
||||||
|
|
||||||
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
|
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ namespace Wino
|
|||||||
services.AddSingleton<IMailDialogService, DialogService>();
|
services.AddSingleton<IMailDialogService, DialogService>();
|
||||||
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
|
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
|
||||||
services.AddTransient<IProviderService, ProviderService>();
|
services.AddTransient<IProviderService, ProviderService>();
|
||||||
|
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterViewModels(IServiceCollection services)
|
private void RegisterViewModels(IServiceCollection services)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<Applications>
|
<Applications>
|
||||||
<Application Id="App"
|
<Application Id="App"
|
||||||
Executable="$targetnametoken$.exe"
|
Executable="$targetnametoken$.exe"
|
||||||
EntryPoint="Wino.App">
|
EntryPoint="Wino.Mail.App">
|
||||||
<uap:VisualElements
|
<uap:VisualElements
|
||||||
DisplayName="Wino Mail"
|
DisplayName="Wino Mail"
|
||||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
|||||||
29
Wino.Mail/Services/MailAuthenticatorConfiguration.cs
Normal file
29
Wino.Mail/Services/MailAuthenticatorConfiguration.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace Wino.Services
|
||||||
|
{
|
||||||
|
public class MailAuthenticatorConfiguration : IAuthenticatorConfig
|
||||||
|
{
|
||||||
|
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
|
||||||
|
|
||||||
|
public string[] OutlookScope => new string[]
|
||||||
|
{
|
||||||
|
"email",
|
||||||
|
"mail.readwrite",
|
||||||
|
"offline_access",
|
||||||
|
"mail.send",
|
||||||
|
"Mail.Send.Shared",
|
||||||
|
"Mail.ReadWrite.Shared",
|
||||||
|
"User.Read"
|
||||||
|
};
|
||||||
|
|
||||||
|
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
|
||||||
|
|
||||||
|
public string[] GmailScope => new string[]
|
||||||
|
{
|
||||||
|
"https://mail.google.com/",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
"https://www.googleapis.com/auth/gmail.labels"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
<ProjectGuid>{68A432B8-C1B7-494C-8D6D-230788EA683E}</ProjectGuid>
|
<ProjectGuid>{68A432B8-C1B7-494C-8D6D-230788EA683E}</ProjectGuid>
|
||||||
<OutputType>AppContainerExe</OutputType>
|
<OutputType>AppContainerExe</OutputType>
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
<RootNamespace>Wino</RootNamespace>
|
<RootNamespace>Wino.Mail</RootNamespace>
|
||||||
<AssemblyName>Wino</AssemblyName>
|
<AssemblyName>Wino.Mail</AssemblyName>
|
||||||
<DefaultLanguage>en-US</DefaultLanguage>
|
<DefaultLanguage>en-US</DefaultLanguage>
|
||||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||||
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
|
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
|
||||||
@@ -259,6 +259,7 @@
|
|||||||
<Compile Include="Selectors\MailItemDisplayModePreviewTemplateSelector.cs" />
|
<Compile Include="Selectors\MailItemDisplayModePreviewTemplateSelector.cs" />
|
||||||
<Compile Include="Selectors\MailItemDisplaySelector.cs" />
|
<Compile Include="Selectors\MailItemDisplaySelector.cs" />
|
||||||
<Compile Include="Services\DialogService.cs" />
|
<Compile Include="Services\DialogService.cs" />
|
||||||
|
<Compile Include="Services\MailAuthenticatorConfiguration.cs" />
|
||||||
<Compile Include="Services\NavigationService.cs" />
|
<Compile Include="Services\NavigationService.cs" />
|
||||||
<Compile Include="Services\ProviderService.cs" />
|
<Compile Include="Services\ProviderService.cs" />
|
||||||
<Compile Include="Services\SettingsBuilderService.cs" />
|
<Compile Include="Services\SettingsBuilderService.cs" />
|
||||||
@@ -565,7 +566,6 @@
|
|||||||
<Content Include="JS\editor.html" />
|
<Content Include="JS\editor.html" />
|
||||||
<Content Include="JS\editor.js" />
|
<Content Include="JS\editor.js" />
|
||||||
<Content Include="JS\global.css" />
|
<Content Include="JS\global.css" />
|
||||||
<Content Include="Assets\WinoIcons.ttf" />
|
|
||||||
<Content Include="JS\libs\jodit.min.css" />
|
<Content Include="JS\libs\jodit.min.css" />
|
||||||
<Content Include="JS\libs\jodit.min.js" />
|
<Content Include="JS\libs\jodit.min.js" />
|
||||||
<Content Include="JS\reader.html" />
|
<Content Include="JS\reader.html" />
|
||||||
@@ -623,9 +623,7 @@
|
|||||||
<Name>Windows Desktop Extensions for the UWP</Name>
|
<Name>Windows Desktop Extensions for the UWP</Name>
|
||||||
</SDKReference>
|
</SDKReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup />
|
||||||
<Folder Include="Helpers\" />
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -83,6 +83,19 @@ namespace Wino.Server
|
|||||||
|
|
||||||
services.AddSingleton<IServerMessageHandlerFactory>(serverMessageHandlerFactory);
|
services.AddSingleton<IServerMessageHandlerFactory>(serverMessageHandlerFactory);
|
||||||
|
|
||||||
|
// Server type related services.
|
||||||
|
// TODO: Better abstraction.
|
||||||
|
|
||||||
|
if (WinoServerType == WinoAppType.Mail)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Calendar config will be added here.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +148,8 @@ namespace Wino.Server
|
|||||||
{
|
{
|
||||||
string processName = WinoServerType == WinoAppType.Mail ? "Wino.Mail" : "Wino.Calendar";
|
string processName = WinoServerType == WinoAppType.Mail ? "Wino.Mail" : "Wino.Calendar";
|
||||||
|
|
||||||
|
var processs = Process.GetProcesses();
|
||||||
|
|
||||||
var proc = Process.GetProcessesByName(processName).FirstOrDefault() ?? throw new Exception($"{processName} client is not running.");
|
var proc = Process.GetProcessesByName(processName).FirstOrDefault() ?? throw new Exception($"{processName} client is not running.");
|
||||||
|
|
||||||
for (IntPtr appWindow = FindWindowEx(IntPtr.Zero, IntPtr.Zero, FRAME_WINDOW, null); appWindow != IntPtr.Zero;
|
for (IntPtr appWindow = FindWindowEx(IntPtr.Zero, IntPtr.Zero, FRAME_WINDOW, null); appWindow != IntPtr.Zero;
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
using Wino.Core.Domain.Models.Server;
|
using Wino.Core.Domain.Models.Server;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
using Wino.Server.Core;
|
using Wino.Server.Core;
|
||||||
|
|
||||||
namespace Wino.Server.MessageHandlers
|
namespace Wino.Server.MessageHandlers
|
||||||
{
|
{
|
||||||
public class AuthenticationHandler : ServerMessageHandler<AuthorizationRequested, TokenInformation>
|
public class AuthenticationHandler : ServerMessageHandler<AuthorizationRequested, TokenInformationEx>
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationProvider _authenticationProvider;
|
private readonly IAuthenticationProvider _authenticationProvider;
|
||||||
|
|
||||||
public override WinoServerResponse<TokenInformation> FailureDefaultResponse(Exception ex)
|
public override WinoServerResponse<TokenInformationEx> FailureDefaultResponse(Exception ex)
|
||||||
=> WinoServerResponse<TokenInformation>.CreateErrorResponse(ex.Message);
|
=> WinoServerResponse<TokenInformationEx>.CreateErrorResponse(ex.Message);
|
||||||
|
|
||||||
public AuthenticationHandler(IAuthenticationProvider authenticationProvider)
|
public AuthenticationHandler(IAuthenticationProvider authenticationProvider)
|
||||||
{
|
{
|
||||||
_authenticationProvider = authenticationProvider;
|
_authenticationProvider = authenticationProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<WinoServerResponse<TokenInformation>> HandleAsync(AuthorizationRequested message,
|
protected override async Task<WinoServerResponse<TokenInformationEx>> HandleAsync(AuthorizationRequested message,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var authenticator = _authenticationProvider.GetAuthenticator(message.MailProviderType);
|
var authenticator = _authenticationProvider.GetAuthenticator(message.MailProviderType);
|
||||||
@@ -36,10 +36,21 @@ namespace Wino.Server.MessageHandlers
|
|||||||
gmailAuthenticator.ProposeCopyAuthURL = true;
|
gmailAuthenticator.ProposeCopyAuthURL = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not save the token here. Call is coming from account creation and things are atomic there.
|
TokenInformationEx generatedToken = null;
|
||||||
var generatedToken = await authenticator.GenerateTokenAsync(message.CreatedAccount, saveToken: false);
|
|
||||||
|
|
||||||
return WinoServerResponse<TokenInformation>.CreateSuccessResponse(generatedToken);
|
if (message.CreatedAccount != null)
|
||||||
|
{
|
||||||
|
generatedToken = await authenticator.GetTokenInformationAsync(message.CreatedAccount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Initial authentication request.
|
||||||
|
// There is no account to get token for.
|
||||||
|
|
||||||
|
generatedToken = await authenticator.GenerateTokenInformationAsync(message.CreatedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WinoServerResponse<TokenInformationEx>.CreateSuccessResponse(generatedToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,22 +26,7 @@ namespace Wino.Server
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public Task LaunchWinoAsync()
|
public Task LaunchWinoAsync()
|
||||||
{
|
{
|
||||||
//var opt = new SynchronizationOptions()
|
|
||||||
//{
|
|
||||||
// Type = Wino.Core.Domain.Enums.SynchronizationType.Full,
|
|
||||||
// AccountId = Guid.Parse("b3620ce7-8a69-4d81-83d5-a94bbe177431")
|
|
||||||
//};
|
|
||||||
|
|
||||||
//var req = new NewSynchronizationRequested(opt, Wino.Core.Domain.Enums.SynchronizationSource.Server);
|
|
||||||
//WeakReferenceMessenger.Default.Send(req);
|
|
||||||
|
|
||||||
// return Task.CompletedTask;
|
|
||||||
|
|
||||||
return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask();
|
return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask();
|
||||||
//await _notificationBuilder.CreateNotificationsAsync(Guid.Empty, new List<IMailItem>()
|
|
||||||
//{
|
|
||||||
// new MailCopy(){ UniqueId = Guid.Parse("8f25d2a0-4448-4fee-96a9-c9b25a19e866")}
|
|
||||||
//});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<None Remove="Images\Wino_Icon.ico" />
|
<None Remove="Images\Wino_Icon.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Wino.Mail\Services\MailAuthenticatorConfiguration.cs" Link="Services\MailAuthenticatorConfiguration.cs" />
|
||||||
<Compile Include="..\Wino.Core.UWP\Services\ConfigurationService.cs" Link="Services\ConfigurationService.cs" />
|
<Compile Include="..\Wino.Core.UWP\Services\ConfigurationService.cs" Link="Services\ConfigurationService.cs" />
|
||||||
<Compile Include="..\Wino.Core.UWP\Services\NativeAppService.cs" Link="Services\NativeAppService.cs" />
|
<Compile Include="..\Wino.Core.UWP\Services\NativeAppService.cs" Link="Services\NativeAppService.cs" />
|
||||||
<Compile Include="..\Wino.Core.UWP\Services\PreferencesService.cs" Link="Services\PreferencesService.cs" />
|
<Compile Include="..\Wino.Core.UWP\Services\PreferencesService.cs" Link="Services\PreferencesService.cs" />
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
|
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||||
|
|||||||
23
Wino.sln
23
Wino.sln
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.ViewModels", "Win
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{A4DBA01A-F315-49E0-8428-BB99D32B20F9}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -306,6 +308,26 @@ Global
|
|||||||
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x64.Build.0 = Release|Any CPU
|
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.ActiveCfg = Release|Any CPU
|
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.Build.0 = Release|Any CPU
|
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM.Build.0 = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.Build.0 = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -320,6 +342,7 @@ Global
|
|||||||
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
|
{A4DBA01A-F315-49E0-8428-BB99D32B20F9} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}
|
SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}
|
||||||
|
|||||||
Reference in New Issue
Block a user