From 8489ad015f37088c8221253e4fd4453111bef005 Mon Sep 17 00:00:00 2001 From: zsolt Date: Thu, 13 Aug 2026 01:35:50 +0200 Subject: [PATCH] Add app configuration endpoint --- OtthonApi/Program.cs | 140 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/OtthonApi/Program.cs b/OtthonApi/Program.cs index 92dbfcc..b6a0dfb 100644 --- a/OtthonApi/Program.cs +++ b/OtthonApi/Program.cs @@ -121,6 +121,36 @@ app.MapPost("/devices/{id:int}/approve", async (int id, OtthonDbContext db) => return Results.Redirect("/devices"); }).RequireAuthorization().DisableAntiforgery(); +app.MapGet("/settings", async (OtthonDbContext db) => +{ + var googleClientId = await GetSetting(db, GoogleCalendarServerClientIdSettingKey()) ?? ""; + return Results.Content(SettingsPage(googleClientId), "text/html; charset=utf-8"); +}).RequireAuthorization(); + +app.MapPost("/settings/google-calendar", async ( + [FromForm] GoogleCalendarSettingsForm form, + OtthonDbContext db) => +{ + await SetSetting( + db, + GoogleCalendarServerClientIdSettingKey(), + Clean(form.ServerClientId) ?? ""); + return Results.Redirect("/settings"); +}).RequireAuthorization().DisableAntiforgery(); + +app.MapGet("/api/app-config", async (HttpContext context, OtthonDbContext db) => +{ + if (!HasApiToken(context)) + { + return Results.Unauthorized(); + } + + var googleClientId = await GetSetting(db, GoogleCalendarServerClientIdSettingKey()); + return Results.Ok(new AppConfigResponse( + googleClientId, + !string.IsNullOrWhiteSpace(googleClientId))); +}); + app.MapPost("/api/devices/register", async ( DeviceRegistrationRequest request, HttpContext context, @@ -603,6 +633,34 @@ static async Task GetOrCreateWorkspace( return workspace; } +static async Task GetSetting(OtthonDbContext db, string key) +{ + var setting = await db.AppSettings.FindAsync(key); + return Clean(setting?.Value); +} + +static async Task SetSetting(OtthonDbContext db, string key, string value) +{ + var setting = await db.AppSettings.FindAsync(key); + if (setting is null) + { + setting = new AppSetting + { + Key = key, + Value = value, + UpdatedUtc = DateTimeOffset.UtcNow + }; + db.AppSettings.Add(setting); + } + else + { + setting.Value = value; + setting.UpdatedUtc = DateTimeOffset.UtcNow; + } + + await db.SaveChangesAsync(); +} + static void ApplyProductLookup(Product target, Product lookup, string source) { target.Name = lookup.Name; @@ -688,6 +746,7 @@ static async Task EnsureDatabaseReady(OtthonDbContext db, ILogger logger) await EnsureProductSchemaAsync(db); await EnsureFoodLearningSchemaAsync(db); await EnsureWorkspaceSchemaAsync(db); + await EnsureAppSettingsSchemaAsync(db); return; } catch (Exception ex) when (attempt < maxAttempts) @@ -705,6 +764,7 @@ static async Task EnsureDatabaseReady(OtthonDbContext db, ILogger logger) await EnsureProductSchemaAsync(db); await EnsureFoodLearningSchemaAsync(db); await EnsureWorkspaceSchemaAsync(db); + await EnsureAppSettingsSchemaAsync(db); } static async Task EnsureProductSchemaAsync(OtthonDbContext db) @@ -740,6 +800,21 @@ END await db.Database.ExecuteSqlRawAsync(sql); } +static async Task EnsureAppSettingsSchemaAsync(OtthonDbContext db) +{ + var sql = """ +IF OBJECT_ID('AppSettings', 'U') IS NULL +BEGIN + CREATE TABLE AppSettings ( + [Key] nvarchar(160) NOT NULL CONSTRAINT PK_AppSettings PRIMARY KEY, + [Value] nvarchar(max) NOT NULL, + UpdatedUtc datetimeoffset NOT NULL + ); +END +"""; + await db.Database.ExecuteSqlRawAsync(sql); +} + static async Task EnsureFoodLearningSchemaAsync(OtthonDbContext db) { var sql = """ @@ -910,7 +985,10 @@ static string DevicesPage(IReadOnlyList devices)

JARVIS API

Registered devices

-
+
+ Settings +
+

The app can submit a device request through POST /api/devices/register. New devices stay pending until approved here.

@@ -933,8 +1011,46 @@ static string DevicesPage(IReadOnlyList devices) """; } +static string SettingsPage(string googleClientId) => $$""" + + + + + + JARVIS API settings + + + +
+
+
+

JARVIS API

+

Settings

+
+
+ Devices +
+
+
+ +
+

Google Calendar OAuth

+

Set the Web application OAuth Client ID from Google Cloud Console. Tablets will fetch this automatically; family members only see the Google consent screen.

+
+ + + + +
+
+ + +"""; + static string Html(string? value) => System.Net.WebUtility.HtmlEncode(value ?? ""); +static string GoogleCalendarServerClientIdSettingKey() => "GoogleCalendarServerClientId"; + static string AdminCss() => """ :root { color-scheme: light; font-family: Inter, Segoe UI, Arial, sans-serif; } body { margin: 0; min-height: 100vh; background: #f4f1ea; color: #17342d; display: grid; place-items: center; } @@ -946,11 +1062,16 @@ h1 { margin: 0 0 18px; font-size: clamp(28px, 4vw, 44px); } p { color: #5b6d67; } label { display: block; margin-bottom: 8px; font-weight: 800; } input { width: 100%; box-sizing: border-box; border: 1px solid #d9e2de; border-radius: 16px; padding: 14px; font-size: 16px; } -button { border: 0; border-radius: 999px; padding: 12px 18px; background: #0f7b61; color: white; font-weight: 900; cursor: pointer; margin-top: 14px; } +button, .button { border: 0; border-radius: 999px; padding: 12px 18px; background: #0f7b61; color: white; font-weight: 900; cursor: pointer; margin-top: 14px; text-decoration: none; display: inline-block; } button.secondary { background: #e6f1ed; color: #17342d; margin-top: 0; } +.button.secondary { background: #e6f1ed; color: #17342d; margin-top: 0; } button.approve { background: #0f7b61; color: white; margin-top: 0; margin-right: 8px; } button:disabled { opacity: .35; cursor: not-allowed; } td form { display: inline-block; margin: 0; } +.actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.actions form { margin: 0; } +.panel { margin-top: 20px; padding: 20px; border: 1px solid #edf2ef; border-radius: 22px; } +.panel h2 { margin-top: 0; } .error { background: #ffe8e0; color: #9b341f; padding: 12px 14px; border-radius: 14px; margin-bottom: 14px; } .hint { background: #e6f1ed; padding: 14px 16px; border-radius: 18px; } table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 18px; } @@ -969,9 +1090,12 @@ public sealed class OtthonDbContext(DbContextOptions options) public DbSet Products => Set(); public DbSet FoodLearningEvents => Set(); public DbSet Workspaces => Set(); + public DbSet AppSettings => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity().HasKey(setting => setting.Key); + modelBuilder.Entity().Property(setting => setting.Key).HasMaxLength(160); modelBuilder.Entity().HasKey(workspace => workspace.Token); modelBuilder.Entity().Property(workspace => workspace.Token).HasMaxLength(160); modelBuilder.Entity().HasKey(product => product.Barcode); @@ -1046,7 +1170,15 @@ public sealed class WorkspaceState public DateTimeOffset UpdatedUtc { get; set; } } +public sealed class AppSetting +{ + public required string Key { get; set; } + public required string Value { get; set; } + public DateTimeOffset UpdatedUtc { get; set; } +} + public sealed record LoginForm(string Password); +public sealed record GoogleCalendarSettingsForm(string? ServerClientId); public sealed record DeviceRegistrationRequest(string? DeviceName, string? Platform); public sealed record ProductUpsertRequest( string Barcode, @@ -1116,6 +1248,10 @@ public sealed record WorkspaceStateResponse( string StateJson, DateTimeOffset UpdatedUtc); +public sealed record AppConfigResponse( + string? GoogleCalendarServerClientId, + bool GoogleCalendarConfigured); + public sealed record ProductResponse( bool Found, string Barcode,