WIP: ios loggin
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
<ApplicationTitle>DiunaBI</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.bimit.diunabi</ApplicationId>
|
||||
<ApplicationId>com.diunabi</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
@@ -41,7 +41,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Platform)' == 'iPhone'">
|
||||
<ApplicationId>com.bimit.diunabi</ApplicationId>
|
||||
<ApplicationId>com.diunabi</ApplicationId>
|
||||
<CodesignKey>Apple Development: Michal Zielinski (2F35ZHMBTB)</CodesignKey>
|
||||
<CodesignProvision>bimai-local</CodesignProvision>
|
||||
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using DiunaBI.UI.Mobile;
|
||||
using DiunaBI.UI.Mobile.Services;
|
||||
using DiunaBI.UI.Shared.Extensions;
|
||||
using DiunaBI.UI.Shared.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -30,6 +31,7 @@ public static class MauiProgram
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<IGoogleAuthService, MobileGoogleAuthService>();
|
||||
|
||||
var baseUrl = GetApiBaseUrl();
|
||||
builder.Services.AddSharedServices(baseUrl);
|
||||
|
||||
@@ -30,5 +30,18 @@
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Ta aplikacja potrzebuje dostępu do kamery w celu skanowania kodów EAN produktów.</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.diunabi</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.diunabi</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using DiunaBI.UI.Shared.Services;
|
||||
using Microsoft.Maui.Authentication;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DiunaBI.UI.Mobile.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Mobile implementation of Google authentication using WebAuthenticator
|
||||
/// Opens OAuth in native browser
|
||||
/// </summary>
|
||||
public class MobileGoogleAuthService : IGoogleAuthService
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private const string GoogleClientId = "784258364493-t8g2bq0utgm9ac3pr8umov6i76uls65s.apps.googleusercontent.com";
|
||||
private const string RedirectUri = "com.diunabi:/oauth2redirect";
|
||||
|
||||
public MobileGoogleAuthService(AuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
public async Task<bool> SignInAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("🔐 Starting mobile Google authentication...");
|
||||
|
||||
// Build Google OAuth URL - use code flow with iOS URL scheme
|
||||
var state = Guid.NewGuid().ToString("N");
|
||||
var codeVerifier = GenerateCodeVerifier();
|
||||
var codeChallenge = GenerateCodeChallenge(codeVerifier);
|
||||
|
||||
var authUrl = "https://accounts.google.com/o/oauth2/v2/auth" +
|
||||
$"?client_id={GoogleClientId}" +
|
||||
$"&redirect_uri={Uri.EscapeDataString(RedirectUri)}" +
|
||||
$"&response_type=code" +
|
||||
$"&scope={Uri.EscapeDataString("openid profile email")}" +
|
||||
$"&state={state}" +
|
||||
$"&code_challenge={codeChallenge}" +
|
||||
$"&code_challenge_method=S256";
|
||||
|
||||
Console.WriteLine($"📱 Opening browser for Google OAuth...");
|
||||
|
||||
var result = await WebAuthenticator.Default.AuthenticateAsync(
|
||||
new Uri(authUrl),
|
||||
new Uri(RedirectUri));
|
||||
|
||||
Console.WriteLine($"✅ Got response from Google OAuth");
|
||||
|
||||
// Extract authorization code
|
||||
if (result.Properties.TryGetValue("code", out var code))
|
||||
{
|
||||
Console.WriteLine($"✅ Got authorization code");
|
||||
|
||||
// Exchange code for ID token
|
||||
var idToken = await ExchangeCodeForToken(code, codeVerifier);
|
||||
|
||||
if (string.IsNullOrEmpty(idToken))
|
||||
{
|
||||
Console.WriteLine("❌ Failed to exchange code for token");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"✅ Got ID token, length: {idToken.Length}");
|
||||
|
||||
// Decode the JWT to get user info
|
||||
var userInfo = DecodeJwtPayload(idToken);
|
||||
var root = userInfo.RootElement;
|
||||
|
||||
var fullName = root.GetProperty("name").GetString() ?? "";
|
||||
var email = root.GetProperty("email").GetString() ?? "";
|
||||
var avatarUrl = root.TryGetProperty("picture", out var pic) ? pic.GetString() ?? "" : "";
|
||||
|
||||
Console.WriteLine($"👤 User: {fullName} ({email})");
|
||||
|
||||
// Validate with backend
|
||||
(bool success, string? errorMessage) = await _authService.ValidateWithBackendAsync(
|
||||
idToken, fullName, email, avatarUrl);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("✅ Backend validation successful");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"❌ Backend validation failed: {errorMessage}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("❌ No authorization code in OAuth response");
|
||||
return false;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Console.WriteLine("ℹ️ User cancelled authentication");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Mobile authentication error: {ex.Message}");
|
||||
Console.WriteLine($"Stack: {ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> ExchangeCodeForToken(string code, string codeVerifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
var tokenRequest = new Dictionary<string, string>
|
||||
{
|
||||
{ "code", code },
|
||||
{ "client_id", GoogleClientId },
|
||||
{ "redirect_uri", RedirectUri },
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code_verifier", codeVerifier }
|
||||
};
|
||||
|
||||
var response = await httpClient.PostAsync(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
new FormUrlEncodedContent(tokenRequest));
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"❌ Token exchange failed: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var doc = JsonDocument.Parse(json);
|
||||
|
||||
return doc.RootElement.GetProperty("id_token").GetString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Error exchanging code: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateCodeVerifier()
|
||||
{
|
||||
var bytes = new byte[32];
|
||||
System.Security.Cryptography.RandomNumberGenerator.Fill(bytes);
|
||||
return Convert.ToBase64String(bytes)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
|
||||
private string GenerateCodeChallenge(string codeVerifier)
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var bytes = System.Text.Encoding.ASCII.GetBytes(codeVerifier);
|
||||
var hash = sha256.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
|
||||
private JsonDocument DecodeJwtPayload(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 3)
|
||||
throw new Exception("Invalid JWT format");
|
||||
|
||||
var payload = parts[1];
|
||||
// Add padding if needed
|
||||
var paddingNeeded = (4 - (payload.Length % 4)) % 4;
|
||||
payload += new string('=', paddingNeeded);
|
||||
|
||||
// Convert from base64url to base64
|
||||
payload = payload.Replace('-', '+').Replace('_', '/');
|
||||
|
||||
var jsonBytes = Convert.FromBase64String(payload);
|
||||
return JsonDocument.Parse(jsonBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Error decoding JWT: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@inject IConfiguration Configuration
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IGoogleAuthService? GoogleAuthService
|
||||
|
||||
<MudCard Class="login-card" Elevation="8">
|
||||
<MudCardContent Class="pa-8 d-flex flex-column align-center">
|
||||
@@ -50,7 +51,17 @@
|
||||
if (firstRender)
|
||||
{
|
||||
_instance = this;
|
||||
await InitializeGoogleSignIn();
|
||||
|
||||
// Only initialize JavaScript Google SDK if we're NOT using platform-specific auth (i.e., web)
|
||||
if (GoogleAuthService == null)
|
||||
{
|
||||
await InitializeGoogleSignIn();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("📱 Using platform-specific Google auth service");
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +101,33 @@
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
await JS.InvokeVoidAsync("requestGoogleSignIn");
|
||||
// Use platform-specific auth if available (mobile), otherwise use JavaScript (web)
|
||||
if (GoogleAuthService != null)
|
||||
{
|
||||
Console.WriteLine("📱 Using mobile Google auth");
|
||||
var success = await GoogleAuthService.SignInAsync();
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("✅ Mobile auth successful, navigating...");
|
||||
NavigationManager.NavigateTo("/dashboard", replace: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorMessage = "Login failed. Please try again";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("🌐 Using web JavaScript Google auth");
|
||||
await JS.InvokeVoidAsync("requestGoogleSignIn");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"❌ HandleGoogleSignIn error: {ex.Message}");
|
||||
_errorMessage = "Login error. Please try again";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
13
src/Backend/DiunaBI.UI.Shared/Services/IGoogleAuthService.cs
Normal file
13
src/Backend/DiunaBI.UI.Shared/Services/IGoogleAuthService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-agnostic interface for Google authentication
|
||||
/// </summary>
|
||||
public interface IGoogleAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiate Google Sign-In flow
|
||||
/// </summary>
|
||||
/// <returns>True if authentication was initiated successfully</returns>
|
||||
Task<bool> SignInAsync();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace DiunaBI.UI.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Web implementation of Google authentication using JavaScript SDK
|
||||
/// </summary>
|
||||
public class WebGoogleAuthService : IGoogleAuthService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
public WebGoogleAuthService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<bool> SignInAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("requestGoogleSignIn");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Web Google Sign-In error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user