WIP: ios loggin
Some checks failed
Build Docker Images / test (push) Failing after 27s
Build Docker Images / build-and-push (push) Successful in 1m49s

This commit is contained in:
2025-11-23 15:17:54 +01:00
parent 951855074f
commit 8077826c46
7 changed files with 288 additions and 4 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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();

View 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();
}

View File

@@ -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;
}
}
}