OAuth session persists via DPAPI; sign-in lives in the Start Stream dialog; add xUnit test project (6 passing)

This commit is contained in:
2026-08-06 09:38:35 -07:00
parent 8f0ecd3796
commit 4afe11e718
15 changed files with 432 additions and 67 deletions
+55
View File
@@ -0,0 +1,55 @@
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using ytLive.Models;
namespace ytLive.Helpers;
/// <summary>
/// Persists the OAuth session (channel + tokens) to a DPAPI-protected file in
/// %APPDATA%\ytLlive\ytLlive.auth so sign-in survives restarts. The whole
/// payload is protected with DataProtectionScope.CurrentUser — only the
/// signed-in Windows user can decrypt it.
/// </summary>
public static class TokenStore
{
public static string DefaultPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ytLlive",
"ytLlive.auth");
public static void Save(YouTubeChannel channel, string? path = null)
{
var json = JsonSerializer.Serialize(channel);
var plain = System.Text.Encoding.UTF8.GetBytes(json);
var protectedBytes = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser);
var file = path ?? DefaultPath;
Directory.CreateDirectory(Path.GetDirectoryName(file)!);
File.WriteAllBytes(file, protectedBytes);
}
public static YouTubeChannel? Load(string? path = null)
{
var file = path ?? DefaultPath;
if (!File.Exists(file)) return null;
try
{
var protectedBytes = File.ReadAllBytes(file);
var plain = ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser);
var json = System.Text.Encoding.UTF8.GetString(plain);
return JsonSerializer.Deserialize<YouTubeChannel>(json);
}
catch
{
return null;
}
}
public static void Clear(string? path = null)
{
var file = path ?? DefaultPath;
if (File.Exists(file)) File.Delete(file);
}
}
+1
View File
@@ -9,6 +9,7 @@ Cross-cutting utilities. See [`schema.md`](../schema.md) for the memory-map conv
| `AppLog.cs` | File logger to `%APPDATA%\ytLlive\startup.log`; startup checkpoints + unhandled-exception capture (the crash this map is named for) |
| `FocusPreservingListBox.cs` | `ListBox` subclass that keeps selection/focus coherent when the selected item is deleted; `IsSelectable` predicate skips hidden scenes; drag-reorder guard |
| `OAuthCredentials.cs` | Baked-in Google OAuth client ID/secret (desktop app; loopback callback) |
| `TokenStore.cs` | DPAPI-protected OAuth session persistence (`%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope); `Save`/`Load`/`Clear` — sign-in survives restarts |
| `ImageCache.cs` | Image byte caching (assets live in the DB) |
| `InverseBoolToVisibilityConverter.cs` / `NotNullToVisibilityConverter.cs` | XAML value converters for visibility bindings |