Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions desktop/Backend-Rust/src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,14 +652,50 @@ async fn generate_custom_token(

tracing::info!("Firebase sign-in successful, UID: {}", firebase_uid);

// For custom token generation, we need Firebase Admin SDK
// In Rust, we'd need to use the service account to create a custom token
// For now, return an error indicating this needs server-side implementation
// The Python version uses firebase_admin.auth.create_custom_token()

// TODO: Implement custom token generation using service account
// This requires signing a JWT with the service account private key
Err("Custom token generation requires Firebase Admin SDK - not yet implemented in Rust".into())
// Generate Firebase custom token using service account credentials from GOOGLE_APPLICATION_CREDENTIALS
let sa_path = state
.config
.google_application_credentials
.as_ref()
.ok_or("GOOGLE_APPLICATION_CREDENTIALS not configured")?;

#[derive(Deserialize)]
struct ServiceAccount {
client_email: String,
private_key: String,
project_id: String,
}

let sa_json = tokio::fs::read_to_string(sa_path).await?;
let sa: ServiceAccount = serde_json::from_str(&sa_json)?;

#[derive(Serialize)]
struct FirebaseCustomTokenClaims<'a> {
iss: &'a str,
sub: &'a str,
aud: &'a str,
uid: &'a str,
iat: i64,
exp: i64,
}

let now = Utc::now().timestamp();
let claims = FirebaseCustomTokenClaims {
iss: &sa.client_email,
sub: &sa.client_email,
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
uid: &firebase_uid,
iat: now,
exp: now + 3600,
};

let mut header = Header::new(Algorithm::RS256);
header.kid = None;

let key = EncodingKey::from_rsa_pem(sa.private_key.as_bytes())?;
let token = encode(&header, &claims, &key)?;

Ok(token)
}

fn render_auth_callback(code: &str, state: &str, redirect_uri: &str, error: Option<&str>) -> String {
Expand Down
9 changes: 9 additions & 0 deletions windows/App/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Application x:Class="Omi.Windows.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Omi.Windows.App"
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>
13 changes: 13 additions & 0 deletions windows/App/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Configuration;
using System.Data;
using System.Windows;

namespace Omi.Windows.App;

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}

10 changes: 10 additions & 0 deletions windows/App/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Windows;

[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
31 changes: 31 additions & 0 deletions windows/App/FloatingBar/FloatingBarWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Window x:Class="Omi.Windows.App.FloatingBar.FloatingBarWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Omi Floating Bar"
Width="400"
Height="80"
Topmost="True"
WindowStyle="None"
ResizeMode="CanResizeWithGrip"
AllowsTransparency="True"
Background="#AA202020"
ShowInTaskbar="False">
<Border CornerRadius="8" Background="#DD202020" Padding="8">
<DockPanel>
<Button Content="🎙"
Width="32"
Height="32"
Margin="0,0,8,0"
DockPanel.Dock="Left"
ToolTip="Push-to-talk (Ctrl+Alt+O)" />
<TextBox x:Name="QueryTextBox"
VerticalContentAlignment="Center"
Background="#FF303030"
Foreground="White"
BorderThickness="0"
Padding="4"
Text="Posez une question à Omi..." />
</DockPanel>
</Border>
</Window>

12 changes: 12 additions & 0 deletions windows/App/FloatingBar/FloatingBarWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Windows;

namespace Omi.Windows.App.FloatingBar;

public partial class FloatingBarWindow : Window
{
public FloatingBarWindow()
{
InitializeComponent();
}
}

31 changes: 31 additions & 0 deletions windows/App/Infrastructure/EnvConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;

namespace Omi.Windows.App.Infrastructure;

public static class EnvConfig
{
// Point par défaut vers la prod, surchargeable via variable d'env
private const string DefaultApiBaseUrl = "https://api.omi.me/";
private const string DefaultFirebaseApiKey = "";

public static string ApiBaseUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("OMI_API_BASE_URL");
if (string.IsNullOrWhiteSpace(fromEnv))
{
return DefaultApiBaseUrl;
}

fromEnv = fromEnv.TrimEnd('/') + "/";
return fromEnv;
}
}

public static string FirebaseApiKey =>
Environment.GetEnvironmentVariable("OMI_FIREBASE_API_KEY")
?? Environment.GetEnvironmentVariable("FIREBASE_API_KEY")
?? DefaultFirebaseApiKey;
}

31 changes: 31 additions & 0 deletions windows/App/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Window x:Class="Omi.Windows.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Omi.Windows.App"
mc:Ignorable="d"
Title="Omi for Windows (Phase 1)" Height="450" Width="800">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button Content="Démarrer l'enregistrement"
Width="200"
Margin="0,0,8,0"
Click="OnStartRecordingClick" />
<Button Content="Arrêter"
Width="100"
Click="OnStopRecordingClick" />
</StackPanel>

<TextBox Grid.Row="1"
Text="{Binding LastTranscript, Mode=OneWay}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
IsReadOnly="True" />
</Grid>
</Window>
89 changes: 89 additions & 0 deletions windows/App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Windows;
using System.Windows.Interop;
using Omi.Windows.App.Services.Audio;
using Omi.Windows.App.Services.Auth;
using Omi.Windows.App.Services.Hotkeys;
using Omi.Windows.App.Services.Notifications;
using Omi.Windows.App.Services.Transcription;
using Omi.Windows.App.ViewModels;
using Omi.Windows.App.FloatingBar;

namespace Omi.Windows.App;

public partial class MainWindow : Window
{
private readonly CaptureViewModel _captureViewModel;
private readonly GlobalHotkeyManager _hotkeyManager;
private readonly WindowsNotificationService _notificationService = new();
private FloatingBarWindow? _floatingBar;

public MainWindow()
{
InitializeComponent();

// Wiring minimal manuel pour la phase 1
var authService = new AuthService(new System.Net.Http.HttpClient());
var sttClient = new SttWebSocketClient(authService);
var audioService = new AudioCaptureService();
_captureViewModel = new CaptureViewModel(audioService, sttClient);

DataContext = _captureViewModel;

if (!authService.IsAuthenticated)
{
var tokenWindow = new TokenInputWindow(authService);
tokenWindow.ShowDialog();
}

Loaded += OnLoaded;
Closed += OnClosed;

_hotkeyManager = new GlobalHotkeyManager(ToggleFloatingBar);
}

private void OnLoaded(object sender, RoutedEventArgs e)
{
var source = (HwndSource)PresentationSource.FromVisual(this)!;
_hotkeyManager.Register(source);

_notificationService.ShowInfo("Omi pour Windows", "App démarrée. Raccourci barre flottante : Ctrl+Alt+O.");
}

private void OnClosed(object? sender, EventArgs e)
{
_hotkeyManager.Dispose();
_floatingBar?.Close();
}

private void ToggleFloatingBar()
{
if (_floatingBar is { IsVisible: true })
{
_floatingBar.Hide();
return;
}

if (_floatingBar is null)
{
_floatingBar = new FloatingBarWindow
{
Left = SystemParameters.WorkArea.Right - 420,
Top = SystemParameters.WorkArea.Bottom - 120
};
}

_floatingBar.Show();
_floatingBar.Activate();
}

private async void OnStartRecordingClick(object sender, RoutedEventArgs e)
{
await _captureViewModel.StartAsync();
}

private async void OnStopRecordingClick(object sender, RoutedEventArgs e)
{
await _captureViewModel.StopAsync();
}
}
17 changes: 17 additions & 0 deletions windows/App/Omi.Windows.App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
</ItemGroup>

</Project>
72 changes: 72 additions & 0 deletions windows/App/Services/Api/HttpApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Omi.Windows.App.Infrastructure;

namespace Omi.Windows.App.Services.Api;

public class HttpApiClient
{
private readonly HttpClient _httpClient;
private readonly IAuthTokenProvider _authTokenProvider;

public HttpApiClient(HttpClient httpClient, IAuthTokenProvider authTokenProvider)
{
_httpClient = httpClient;
_authTokenProvider = authTokenProvider;
_httpClient.BaseAddress ??= new Uri(EnvConfig.ApiBaseUrl);
}

public async Task<T?> GetAsync<T>(string path, bool requireAuth = true, CancellationToken ct = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, path);
await EnrichHeadersAsync(request, requireAuth, ct).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct).ConfigureAwait(false);
}

public async Task<TResponse?> PostJsonAsync<TRequest, TResponse>(
string path,
TRequest body,
bool requireAuth = true,
CancellationToken ct = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, path)
{
Content = JsonContent.Create(body)
};

await EnrichHeadersAsync(request, requireAuth, ct).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
if (response.Content.Headers.ContentLength is 0)
{
return default;
}
return await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: ct).ConfigureAwait(false);
}

private async Task EnrichHeadersAsync(HttpRequestMessage request, bool requireAuth, CancellationToken ct)
{
request.Headers.Add("X-App-Platform", "windows");
request.Headers.Add("X-App-Version", "0.1.0");
request.Headers.Add("X-Request-Start-Time", (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0).ToString("F3"));

if (!requireAuth)
{
return;
}

var token = await _authTokenProvider.GetIdTokenAsync(ct).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
}

10 changes: 10 additions & 0 deletions windows/App/Services/Api/IAuthTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace Omi.Windows.App.Services.Api;

public interface IAuthTokenProvider
{
Task<string?> GetIdTokenAsync(CancellationToken ct = default);
}

Loading