From 66dc53f12f0e63829041f867342b48184328ebfa Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 25 May 2026 12:07:40 +0300 Subject: [PATCH] fix(web): address PR review critical issues for Discord OAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add V019 migration: rename session_audit_log.actor_telegram_id → actor_external_user_id - Add CSRF protection to Discord OAuth flow (state cookie with HttpOnly/Secure/Strict) - Add Discord OAuth env vars to compose.yaml, deploy.yml, and .env.example - Fix SQL COALESCE for nullable telegram_id in GetGroupManagersAsync and GetSessionParticipantsAsync Co-Authored-By: Claude Opus 4.7 --- .env.example | 7 ++++++ .gitea/workflows/deploy.yml | 3 +++ compose.yaml | 3 +++ .../V019__audit_log_platform_identity.sql | 18 +++++++++++++++ src/GmRelay.Web/Program.cs | 23 +++++++++++++++++-- src/GmRelay.Web/Services/SessionService.cs | 4 ++-- 6 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/GmRelay.Bot/Migrations/V019__audit_log_platform_identity.sql diff --git a/.env.example b/.env.example index 6f92bc9..47db44c 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,13 @@ TELEGRAM_MINI_APP_URL= # Можно получить в Discord Developer Portal (https://discord.com/developers/applications) DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN_HERE +# Discord OAuth (для Web Dashboard) +# Client ID и Secret из OAuth2 раздела Discord Developer Portal +# Redirect URI должен указывать на /auth/discord/callback вашего домена +DISCORD_CLIENT_ID=YOUR_DISCORD_CLIENT_ID_HERE +DISCORD_CLIENT_SECRET=YOUR_DISCORD_CLIENT_SECRET_HERE +DISCORD_REDIRECT_URI=https://your-domain.example/auth/discord/callback + # Пароль для базы данных PostgreSQL POSTGRES_PASSWORD=StrongPasswordForDatabase diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index a2124f4..5440737 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -113,6 +113,9 @@ jobs: echo "DISCORD_BOT_TOKEN=${{ secrets.DISCORD_BOT_TOKEN }}" >> .env echo "TELEGRAM_BOT_USERNAME=${{ secrets.TELEGRAM_BOT_USERNAME }}" >> .env echo "TELEGRAM_MINI_APP_URL=${{ secrets.TELEGRAM_MINI_APP_URL }}" >> .env + echo "DISCORD_CLIENT_ID=${{ secrets.DISCORD_CLIENT_ID }}" >> .env + echo "DISCORD_CLIENT_SECRET=${{ secrets.DISCORD_CLIENT_SECRET }}" >> .env + echo "DISCORD_REDIRECT_URI=${{ secrets.DISCORD_REDIRECT_URI }}" >> .env - name: Deploy Containers run: | diff --git a/compose.yaml b/compose.yaml index f14a35f..2bdbd1a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -94,6 +94,9 @@ services: - "Telegram__BotToken=${TELEGRAM_BOT_TOKEN:?Set TELEGRAM_BOT_TOKEN in .env}" - "Telegram__BotUsername=${TELEGRAM_BOT_USERNAME:?Set TELEGRAM_BOT_USERNAME in .env}" - "Telegram__MiniAppUrl=${TELEGRAM_MINI_APP_URL:-}" + - "Discord__ClientId=${DISCORD_CLIENT_ID:-}" + - "Discord__ClientSecret=${DISCORD_CLIENT_SECRET:-}" + - "Discord__RedirectUri=${DISCORD_REDIRECT_URI:-}" ports: - "${GMRELAY_WEB_PORT:-8080}:8080" volumes: diff --git a/src/GmRelay.Bot/Migrations/V019__audit_log_platform_identity.sql b/src/GmRelay.Bot/Migrations/V019__audit_log_platform_identity.sql new file mode 100644 index 0000000..e191e07 --- /dev/null +++ b/src/GmRelay.Bot/Migrations/V019__audit_log_platform_identity.sql @@ -0,0 +1,18 @@ +-- ============================================================= +-- V019: Rename session_audit_log.actor_telegram_id to actor_external_user_id +-- ============================================================= +-- Scope: Support platform-agnostic audit log identity. +-- ============================================================= + +ALTER TABLE session_audit_log + ADD COLUMN actor_external_user_id VARCHAR(255); + +UPDATE session_audit_log + SET actor_external_user_id = actor_telegram_id::TEXT + WHERE actor_external_user_id IS NULL; + +ALTER TABLE session_audit_log + ALTER COLUMN actor_external_user_id SET NOT NULL; + +ALTER TABLE session_audit_log + DROP COLUMN actor_telegram_id; diff --git a/src/GmRelay.Web/Program.cs b/src/GmRelay.Web/Program.cs index b3ce775..ad67c2d 100644 --- a/src/GmRelay.Web/Program.cs +++ b/src/GmRelay.Web/Program.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Security.Claims; +using System.Security.Cryptography; using System.Text.Json; using Telegram.Bot; using Npgsql; @@ -184,9 +185,16 @@ app.MapPost("/auth/logout", async (HttpContext context) => }); // Discord OAuth endpoints -app.MapGet("/auth/discord", (DiscordAuthService discordAuth) => +app.MapGet("/auth/discord", (HttpContext context, DiscordAuthService discordAuth) => { var state = Guid.NewGuid().ToString("N"); + context.Response.Cookies.Append("__DiscordOAuthState", state, new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + MaxAge = TimeSpan.FromMinutes(5) + }); var url = discordAuth.BuildAuthorizeUrl(state); return Results.Redirect(url); }); @@ -197,8 +205,19 @@ app.MapGet("/auth/discord/callback", async ( ISessionStore sessionStore) => { var code = context.Request.Query["code"].ToString(); - if (string.IsNullOrWhiteSpace(code)) + var state = context.Request.Query["state"].ToString(); + var storedState = context.Request.Cookies["__DiscordOAuthState"]; + + context.Response.Cookies.Delete("__DiscordOAuthState"); + + if (string.IsNullOrWhiteSpace(code) || + string.IsNullOrWhiteSpace(state) || + !CryptographicOperations.FixedTimeEquals( + System.Text.Encoding.UTF8.GetBytes(state), + System.Text.Encoding.UTF8.GetBytes(storedState ?? string.Empty))) + { return Results.Redirect("/login?error=auth_failed"); + } var user = await discordAuth.ExchangeCodeAsync(code); if (user is null) diff --git a/src/GmRelay.Web/Services/SessionService.cs b/src/GmRelay.Web/Services/SessionService.cs index 119fdc6..3318d3c 100644 --- a/src/GmRelay.Web/Services/SessionService.cs +++ b/src/GmRelay.Web/Services/SessionService.cs @@ -179,7 +179,7 @@ public sealed class SessionService( await using var conn = await dataSource.OpenConnectionAsync(); return (await conn.QueryAsync( """ - SELECT p.telegram_id AS TelegramId, + SELECT COALESCE(p.telegram_id, 0) AS TelegramId, COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId, p.display_name AS DisplayName, p.telegram_username AS TelegramUsername, @@ -641,7 +641,7 @@ public sealed class SessionService( return (await conn.QueryAsync( """ SELECT sp.id AS Id, - p.telegram_id AS TelegramId, + COALESCE(p.telegram_id, 0) AS TelegramId, COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId, p.display_name AS DisplayName, p.telegram_username AS TelegramUsername,