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