From 2176d45bd2d6910ff0746c6dcd26fe4e03704c44 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 28 May 2026 18:48:01 +0300 Subject: [PATCH 01/17] feat(event-detail): render full HTML content with save/calendar toggles - Add contentHtml? field to EventItem model - Render RSS full-text via DomSanitizer.bypassSecurityTrustHtml() - Save and calendar toggle buttons on detail page (top + bottom bars) - Load initial saved/calendar state from UsersService on init - Full SCSS overhaul: article-body styles for images, headings, blockquotes, tables, code blocks, links; active-state colours for toggle buttons Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/core/models/event.model.ts | 1 + .../features/event-detail/event-detail.html | 81 +++++-- .../features/event-detail/event-detail.scss | 209 ++++++++++++++++-- .../app/features/event-detail/event-detail.ts | 78 ++++++- 4 files changed, 325 insertions(+), 44 deletions(-) diff --git a/event-aggregator-client/src/app/core/models/event.model.ts b/event-aggregator-client/src/app/core/models/event.model.ts index 46c00a8..6424d84 100644 --- a/event-aggregator-client/src/app/core/models/event.model.ts +++ b/event-aggregator-client/src/app/core/models/event.model.ts @@ -7,6 +7,7 @@ export interface EventItem { id: number; title: string; description: string; + contentHtml?: string; sourceUrl: string; imageUrl?: string; publishedDate: string; diff --git a/event-aggregator-client/src/app/features/event-detail/event-detail.html b/event-aggregator-client/src/app/features/event-detail/event-detail.html index 4e411ac..e453d38 100644 --- a/event-aggregator-client/src/app/features/event-detail/event-detail.html +++ b/event-aggregator-client/src/app/features/event-detail/event-detail.html @@ -12,51 +12,94 @@ @if (event(); as e) {
- - arrow_back Стрічка - + +
+ + arrow_back До стрічки + + + +
+ + + + open_in_new Оригінал + +
+
+ + @if (e.imageUrl) { } -

{{ e.title }}

- -
+ +
@if (e.source) { - {{ e.source.name }} + {{ e.source.name }} } @if (e.category) { - {{ e.category }} + {{ e.category }} + } + @if (e.source?.language) { + {{ e.source?.language === 'uk' ? '🇺🇦 UA' : '🇬🇧 EN' }} } - +
+ + +

{{ e.title }}

+ + +
+ calendar_today {{ e.publishedDate | date:'d MMMM yyyy, HH:mm' }} + @if (e.eventDate) { + + event + Дата події: {{ e.eventDate | date:'d MMMM yyyy' }} + + } @if (e.location) { - + place {{ e.location }} } @if (e.author) { - person {{ e.author }} + + person {{ e.author }} + }
-

{{ e.description }}

+ +
-
+ + +
+ + open_in_new Відкрити оригінал - -
+
} diff --git a/event-aggregator-client/src/app/features/event-detail/event-detail.scss b/event-aggregator-client/src/app/features/event-detail/event-detail.scss index e9173e4..195443a 100644 --- a/event-aggregator-client/src/app/features/event-detail/event-detail.scss +++ b/event-aggregator-client/src/app/features/event-detail/event-detail.scss @@ -12,25 +12,83 @@ .detail-wrap { max-width: 760px; margin: 0 auto; - padding: 24px 16px; + padding: 24px 16px 40px; - .back-btn { margin-bottom: 16px; } + // ── Top navigation ── + .top-nav { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; + .back-btn { flex-shrink: 0; } + + .action-bar { + display: flex; + flex-wrap: wrap; + gap: 8px; + + button, a { + display: flex; + align-items: center; + gap: 4px; + } + } + } + + // ── Hero image ── .hero-img { width: 100%; - max-height: 360px; + max-height: 380px; object-fit: cover; - border-radius: 8px; - margin-bottom: 24px; + border-radius: 10px; + margin-bottom: 20px; + display: block; } + // ── Tags row ── + .tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + + .tag { + padding: 3px 10px; + border-radius: 12px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.3px; + } + + .source-tag { + background: #e8f0fe; + color: #1a73e8; + } + + .cat-tag { + background: #fce8e6; + color: #d93025; + } + + .lang-tag { + background: #e6f4ea; + color: #188038; + } + } + + // ── Title ── .title { - font-size: 1.75rem; + font-size: 1.8rem; font-weight: 700; margin: 0 0 16px; line-height: 1.3; + color: #111; } + // ── Meta row ── .meta { display: flex; flex-wrap: wrap; @@ -40,30 +98,145 @@ font-size: 0.875rem; color: #555; - .source { + span { display: flex; align-items: center; gap: 4px; } + + .event-date-badge { background: #e8f0fe; color: #1a73e8; - padding: 2px 10px; + padding: 3px 10px; border-radius: 12px; - font-weight: 500; + font-weight: 600; } - mat-icon { vertical-align: middle; font-size: 16px; } + mat-icon { font-size: 16px; width: 16px; height: 16px; } } - mat-divider { margin: 16px 0; } + mat-divider { margin: 20px 0; } - .body-text { + // ── Article body (rendered HTML from RSS) ── + .article-body { font-size: 1rem; - line-height: 1.7; - color: #333; - white-space: pre-line; + line-height: 1.8; + color: #222; + + // Paragraphs + p { + margin: 0 0 1.1em; + } + + // Headings + h1, h2, h3, h4, h5, h6 { + margin: 1.4em 0 0.5em; + font-weight: 700; + line-height: 1.3; + color: #111; + } + h1 { font-size: 1.6rem; } + h2 { font-size: 1.35rem; } + h3 { font-size: 1.15rem; } + + // Images + img { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 16px 0; + display: block; + } + + // Links + a { + color: #1a73e8; + text-decoration: none; + &:hover { text-decoration: underline; } + } + + // Blockquote + blockquote { + margin: 16px 0; + padding: 12px 16px; + border-left: 4px solid #1a73e8; + background: #f8f9ff; + border-radius: 0 6px 6px 0; + color: #444; + font-style: italic; + } + + // Lists + ul, ol { + margin: 0 0 1em; + padding-left: 1.5em; + li { margin-bottom: 0.3em; } + } + + // Inline code / code blocks + code { + font-family: 'Fira Code', monospace; + font-size: 0.9em; + background: #f1f3f4; + padding: 2px 5px; + border-radius: 4px; + } + + pre { + background: #f1f3f4; + padding: 14px 16px; + border-radius: 6px; + overflow-x: auto; + code { background: none; padding: 0; } + } + + // Horizontal rule + hr { + border: none; + border-top: 1px solid #e0e0e0; + margin: 24px 0; + } + + // Tables + table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + font-size: 0.9rem; + + th, td { + border: 1px solid #e0e0e0; + padding: 8px 12px; + text-align: left; + } + + th { + background: #f5f5f5; + font-weight: 600; + } + + tr:hover td { background: #fafafa; } + } } - .actions { + // ── Bottom action row ── + .bottom-actions { display: flex; flex-wrap: wrap; - gap: 12px; - margin-top: 32px; + gap: 10px; + margin-top: 8px; + + button, a { + display: flex; + align-items: center; + gap: 4px; + } + } + + // ── Active state colours for toggle buttons ── + .active-saved { + color: #f59e0b !important; + border-color: #f59e0b !important; + } + + .active-calendar { + color: #1a73e8 !important; + border-color: #1a73e8 !important; } } diff --git a/event-aggregator-client/src/app/features/event-detail/event-detail.ts b/event-aggregator-client/src/app/features/event-detail/event-detail.ts index 250349a..6341b31 100644 --- a/event-aggregator-client/src/app/features/event-detail/event-detail.ts +++ b/event-aggregator-client/src/app/features/event-detail/event-detail.ts @@ -1,12 +1,17 @@ -import { Component, inject, OnInit, signal } from '@angular/core'; +import { Component, computed, inject, OnInit, signal } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { DatePipe } from '@angular/common'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatDividerModule } from '@angular/material/divider'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { AuthService } from '../../core/services/auth.service'; import { EventsService } from '../../core/services/events.service'; +import { UsersService } from '../../core/services/users.service'; import { EventItem } from '../../core/models/event.model'; @Component({ @@ -15,23 +20,82 @@ import { EventItem } from '../../core/models/event.model'; imports: [ DatePipe, RouterLink, MatButtonModule, MatIconModule, MatChipsModule, - MatProgressSpinnerModule, MatDividerModule, + MatProgressSpinnerModule, MatDividerModule, MatTooltipModule, ], templateUrl: './event-detail.html', styleUrl: './event-detail.scss', }) export class EventDetail implements OnInit { - private route = inject(ActivatedRoute); + private route = inject(ActivatedRoute); private eventsService = inject(EventsService); + private usersService = inject(UsersService); + private auth = inject(AuthService); + private sanitizer = inject(DomSanitizer); + private snackBar = inject(MatSnackBar); - event = signal(null); - loading = signal(true); + event = signal(null); + loading = signal(true); + saved = signal(false); + inCalendar = signal(false); + + get userId() { return this.auth.currentUser()?.id ?? 0; } + + safeContent = computed((): SafeHtml | null => { + const e = this.event(); + if (!e) return null; + const html = e.contentHtml || e.description; + return this.sanitizer.bypassSecurityTrustHtml(html); + }); ngOnInit() { const id = Number(this.route.snapshot.paramMap.get('id')); + this.eventsService.getById(id).subscribe({ - next: e => { this.event.set(e); this.loading.set(false); }, - error: () => this.loading.set(false), + next: e => { this.event.set(e); this.loading.set(false); }, + error: () => this.loading.set(false), }); + + this.usersService.getSavedEvents(this.userId) + .subscribe(evs => { if (evs.some(e => e.id === id)) this.saved.set(true); }); + + this.usersService.getCalendarEvents(this.userId) + .subscribe(evs => { if (evs.some(e => e.id === id)) this.inCalendar.set(true); }); + } + + toggleSaved() { + const ev = this.event(); + if (!ev) return; + + if (this.saved()) { + this.usersService.removeEventStatus(this.userId, ev.id).subscribe(() => { + this.saved.set(false); + this.snackBar.open('Прибрано зі збережених', '', { duration: 2000 }); + }); + } else { + this.usersService.setEventStatus(this.userId, ev.id, { status: 'Interesting', isInCalendar: false }) + .subscribe(() => { + this.saved.set(true); + this.snackBar.open('Збережено ★', '', { duration: 2000 }); + }); + } + } + + toggleCalendar() { + const ev = this.event(); + if (!ev) return; + + if (this.inCalendar()) { + this.usersService.setEventStatus(this.userId, ev.id, { isInCalendar: false }) + .subscribe(() => { + this.inCalendar.set(false); + this.snackBar.open('Прибрано з календаря', '', { duration: 2000 }); + }); + } else { + this.usersService.setEventStatus(this.userId, ev.id, { isInCalendar: true }) + .subscribe(() => { + this.inCalendar.set(true); + this.snackBar.open('Додано до календаря 📅', '', { duration: 2000 }); + }); + } } } From a2bec7a267883b12255484ac2b0d87b41a1d4aba Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 28 May 2026 19:25:48 +0300 Subject: [PATCH 02/17] feat: Telegram bot digest notifications Backend: - Add TelegramUsername / TelegramChatId / TelegramNotificationsEnabled to User - EF migration AddTelegramFields - ITelegramBotService + TelegramBotService (Telegram.Bot 22.x, HTML mode, auto-split >4000 chars) - NullTelegramBotService stub when BotToken not configured - TelegramBotBackgroundService: long polling, /start links account by username, /digest sends on-demand digest - DigestService: send Telegram message alongside email when enabled - UsersController: PATCH /{id}/telegram endpoint, telegram fields in MapUser Frontend: - User model: telegramNotificationsEnabled / telegramUsername / telegramChatId - UsersService: updateTelegram() PATCH call - Profile: mat-slide-toggle, @username input, connection status indicator, saveTelegram() method; MatSlideToggleModule added to imports Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/UsersController.cs | 31 ++ EventAggregator/EventAggregator.csproj | 1 + ...260528162150_AddTelegramFields.Designer.cs | 301 ++++++++++++++++++ .../20260528162150_AddTelegramFields.cs | 49 +++ .../Migrations/AppDbContextModelSnapshot.cs | 9 + EventAggregator/Models/User.cs | 5 + EventAggregator/Program.cs | 14 + EventAggregator/Services/DigestService.cs | 87 ++++- .../Services/ITelegramBotService.cs | 6 + .../Services/NullTelegramBotService.cs | 16 + .../Services/TelegramBotBackgroundService.cs | 175 ++++++++++ .../Services/TelegramBotService.cs | 67 ++++ EventAggregator/appsettings.example.json | 3 + .../src/app/core/models/user.model.ts | 9 + .../src/app/core/services/users.service.ts | 6 +- .../src/app/features/profile/profile.html | 52 +++ .../src/app/features/profile/profile.scss | 70 ++++ .../src/app/features/profile/profile.ts | 30 +- 18 files changed, 925 insertions(+), 6 deletions(-) create mode 100644 EventAggregator/Migrations/20260528162150_AddTelegramFields.Designer.cs create mode 100644 EventAggregator/Migrations/20260528162150_AddTelegramFields.cs create mode 100644 EventAggregator/Services/ITelegramBotService.cs create mode 100644 EventAggregator/Services/NullTelegramBotService.cs create mode 100644 EventAggregator/Services/TelegramBotBackgroundService.cs create mode 100644 EventAggregator/Services/TelegramBotService.cs diff --git a/EventAggregator/Controllers/UsersController.cs b/EventAggregator/Controllers/UsersController.cs index 72950af..5f837b8 100644 --- a/EventAggregator/Controllers/UsersController.cs +++ b/EventAggregator/Controllers/UsersController.cs @@ -259,6 +259,33 @@ public async Task RemoveEventStatus(int id, int eventId) // ── Helpers ────────────────────────────────────────────────────────────── + // ── Telegram ───────────────────────────────────────────────────────────── + + /// Зберегти Telegram-налаштування користувача + [HttpPatch("{id:int}/telegram")] + public async Task UpdateTelegram(int id, UpdateTelegramRequest req) + { + var user = await _db.Users.FindAsync(id); + if (user is null) return NotFound(); + + // Strip leading @ if user typed it + var username = req.TelegramUsername?.Trim().TrimStart('@').ToLower(); + + // If username changed — reset chat link so user needs to /start again + if (user.TelegramUsername != username) + { + user.TelegramUsername = username; + user.TelegramChatId = null; + } + + user.TelegramNotificationsEnabled = req.TelegramNotificationsEnabled; + await _db.SaveChangesAsync(); + + return Ok(new { user.TelegramChatId }); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + private static object MapUser(User u, IEnumerable filters) => new { u.Id, @@ -268,10 +295,14 @@ public async Task RemoveEventStatus(int id, int eventId) u.PhotoUrl, u.ReportFrequency, u.LastReportSent, + u.TelegramNotificationsEnabled, + u.TelegramUsername, + u.TelegramChatId, Filters = filters.Select(f => new { f.Id, f.Name, f.Type }) }; } public record CreateUserRequest(string FirstName, string LastName, string Email, string? ReportFrequency); public record UpdateUserRequest(string? FirstName, string? LastName, string? PhotoUrl, string? ReportFrequency); +public record UpdateTelegramRequest(bool TelegramNotificationsEnabled, string? TelegramUsername); public record SetEventStatusRequest(string? Status, bool IsInCalendar); diff --git a/EventAggregator/EventAggregator.csproj b/EventAggregator/EventAggregator.csproj index 64914e3..fa2193d 100644 --- a/EventAggregator/EventAggregator.csproj +++ b/EventAggregator/EventAggregator.csproj @@ -22,6 +22,7 @@ + diff --git a/EventAggregator/Migrations/20260528162150_AddTelegramFields.Designer.cs b/EventAggregator/Migrations/20260528162150_AddTelegramFields.Designer.cs new file mode 100644 index 0000000..01e3f8b --- /dev/null +++ b/EventAggregator/Migrations/20260528162150_AddTelegramFields.Designer.cs @@ -0,0 +1,301 @@ +// +using System; +using EventAggregator.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventAggregator.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260528162150_AddTelegramFields")] + partial class AddTelegramFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("ContentHtml") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("FeedSourceId") + .HasColumnType("INTEGER"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("MaxPeople") + .HasColumnType("INTEGER"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.Property("SourceUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FeedSourceId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastFetched") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FeedSources"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SearchKeyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Filters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastReportSent") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasColumnType("TEXT"); + + b.Property("ReportFrequency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelegramChatId") + .HasColumnType("INTEGER"); + + b.Property("TelegramNotificationsEnabled") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IsInCalendar") + .HasColumnType("INTEGER"); + + b.Property("MarkedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("UserId"); + + b.ToTable("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FilterId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FilterId"); + + b.HasIndex("FilterId"); + + b.ToTable("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.HasOne("EventAggregator.API.Models.FeedSource", "FeedSource") + .WithMany("Events") + .HasForeignKey("FeedSourceId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FeedSource"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.HasOne("EventAggregator.API.Models.Event", "Event") + .WithMany("UserEventStatuses") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserEventStatuses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.HasOne("EventAggregator.API.Models.Filter", "Filter") + .WithMany("UserFilters") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserFilters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Filter"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Navigation("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Navigation("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Navigation("UserEventStatuses"); + + b.Navigation("UserFilters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventAggregator/Migrations/20260528162150_AddTelegramFields.cs b/EventAggregator/Migrations/20260528162150_AddTelegramFields.cs new file mode 100644 index 0000000..1611df1 --- /dev/null +++ b/EventAggregator/Migrations/20260528162150_AddTelegramFields.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventAggregator.Migrations +{ + /// + public partial class AddTelegramFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TelegramChatId", + table: "Users", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "TelegramNotificationsEnabled", + table: "Users", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "TelegramUsername", + table: "Users", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TelegramChatId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "TelegramNotificationsEnabled", + table: "Users"); + + migrationBuilder.DropColumn( + name: "TelegramUsername", + table: "Users"); + } + } +} diff --git a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs index 260ba84..1cbf2b8 100644 --- a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs +++ b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs @@ -157,6 +157,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("TelegramChatId") + .HasColumnType("INTEGER"); + + b.Property("TelegramNotificationsEnabled") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .HasColumnType("TEXT"); + b.Property("TwoFactorEnabled") .HasColumnType("INTEGER"); diff --git a/EventAggregator/Models/User.cs b/EventAggregator/Models/User.cs index 424bfbe..58a9520 100644 --- a/EventAggregator/Models/User.cs +++ b/EventAggregator/Models/User.cs @@ -24,6 +24,11 @@ public class User public bool TwoFactorEnabled { get; set; } = false; public string? TwoFactorSecret { get; set; } + // ── Telegram ────────────────────────────────────────────────────────────── + public bool TelegramNotificationsEnabled { get; set; } = false; + public string? TelegramUsername { get; set; } // введено в профілі (без @) + public long? TelegramChatId { get; set; } // встановлюється після /start + public ICollection UserFilters { get; set; } = new List(); public ICollection UserEventStatuses { get; set; } = new List(); } \ No newline at end of file diff --git a/EventAggregator/Program.cs b/EventAggregator/Program.cs index db24546..ddfb3b2 100644 --- a/EventAggregator/Program.cs +++ b/EventAggregator/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using Telegram.Bot; var builder = WebApplication.CreateBuilder(args); @@ -29,6 +30,19 @@ builder.Services.AddHostedService(); builder.Services.AddHostedService(); +// ── Telegram Bot ────────────────────────────────────────────────────────── +var telegramToken = builder.Configuration["Telegram:BotToken"]; +if (!string.IsNullOrWhiteSpace(telegramToken)) +{ + builder.Services.AddSingleton(new TelegramBotClient(telegramToken)); + builder.Services.AddScoped(); + builder.Services.AddHostedService(); +} +else +{ + builder.Services.AddScoped(); +} + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { diff --git a/EventAggregator/Services/DigestService.cs b/EventAggregator/Services/DigestService.cs index 2dad3eb..c62817e 100644 --- a/EventAggregator/Services/DigestService.cs +++ b/EventAggregator/Services/DigestService.cs @@ -9,13 +9,16 @@ public class DigestService { private readonly AppDbContext _db; private readonly IEmailService _emailService; + private readonly ITelegramBotService _telegramService; private readonly ILogger _logger; - public DigestService(AppDbContext db, IEmailService emailService, ILogger logger) + public DigestService(AppDbContext db, IEmailService emailService, + ITelegramBotService telegramService, ILogger logger) { - _db = db; - _emailService = emailService; - _logger = logger; + _db = db; + _emailService = emailService; + _telegramService = telegramService; + _logger = logger; } public async Task SendAllDueAsync() @@ -65,12 +68,52 @@ public async Task SendDigestAsync(User user) await _emailService.SendAsync(user.Email, subject, html); + // Telegram + if (user.TelegramNotificationsEnabled && user.TelegramChatId.HasValue) + { + var tgText = BuildTelegramText(user, events); + await _telegramService.SendMessageAsync(user.TelegramChatId.Value, tgText); + } + user.LastReportSent = DateTime.UtcNow; await _db.SaveChangesAsync(); _logger.LogInformation("Digest sent to {Email}: {Count} events", user.Email, events.Count); } + /// Send digest only to Telegram (called by /digest bot command). + public async Task SendTelegramDigestAsync(User user) + { + if (!user.TelegramChatId.HasValue) return; + + var from = GetFromDate(user.ReportFrequency); + var keywords = user.UserFilters + .Select(uf => uf.Filter.SearchKeyword.ToLower()) + .ToList(); + + var events = await _db.Events + .Include(e => e.FeedSource) + .Where(e => e.PublishedDate >= from) + .Where(e => keywords.Any(k => + e.Title.ToLower().Contains(k) || + e.Description.ToLower().Contains(k))) + .OrderByDescending(e => e.PublishedDate) + .Take(10) + .ToListAsync(); + + if (events.Count == 0) + { + await _telegramService.SendMessageAsync(user.TelegramChatId.Value, + "ℹ️ За вашими підписками поки що немає нових подій."); + return; + } + + var tgText = BuildTelegramText(user, events); + await _telegramService.SendMessageAsync(user.TelegramChatId.Value, tgText); + _logger.LogInformation("Telegram digest sent to chatId={ChatId}: {Count} events", + user.TelegramChatId.Value, events.Count); + } + public async Task BuildPreviewAsync(User user) { var from = GetFromDate(user.ReportFrequency); @@ -195,6 +238,42 @@ private static string BuildHtml(User user, List events) return sb.ToString(); } + private static string BuildTelegramText(User user, List events) + { + var sb = new StringBuilder(); + + sb.AppendLine($"📅 EventAggregator — {FrequencyLabel(user.ReportFrequency)} дайджест"); + sb.AppendLine(); + sb.AppendLine($"Привіт, {TgEscape(user.FirstName)}! 👋"); + sb.AppendLine("Ось підбірка свіжих подій за вашими підписками:"); + sb.AppendLine(); + + for (int i = 0; i < events.Count; i++) + { + var ev = events[i]; + var source = TgEscape(ev.FeedSource?.Name ?? "Невідоме джерело"); + var date = ev.PublishedDate.ToString("dd.MM.yyyy"); + var cat = string.IsNullOrEmpty(ev.Category) ? "" : $" · {TgEscape(ev.Category)}"; + var desc = ev.Description.Length > 150 + ? ev.Description[..150] + "…" + : ev.Description; + + sb.AppendLine($"{i + 1}. {TgEscape(ev.Title)}"); + sb.AppendLine($"📰 {source} · {date}{cat}"); + sb.AppendLine(TgEscape(desc)); + sb.AppendLine($"🔗 Читати далі"); + sb.AppendLine(); + } + + sb.AppendLine($"Частота розсилки: {FrequencyLabel(user.ReportFrequency)}"); + + return sb.ToString().Trim(); + } + + /// Escapes characters that have special meaning in Telegram HTML mode. + private static string TgEscape(string s) => + s.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + private static string Escape(string s) => s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); } diff --git a/EventAggregator/Services/ITelegramBotService.cs b/EventAggregator/Services/ITelegramBotService.cs new file mode 100644 index 0000000..ad39c44 --- /dev/null +++ b/EventAggregator/Services/ITelegramBotService.cs @@ -0,0 +1,6 @@ +namespace EventAggregator.API.Services; + +public interface ITelegramBotService +{ + Task SendMessageAsync(long chatId, string htmlText); +} diff --git a/EventAggregator/Services/NullTelegramBotService.cs b/EventAggregator/Services/NullTelegramBotService.cs new file mode 100644 index 0000000..6ef23ee --- /dev/null +++ b/EventAggregator/Services/NullTelegramBotService.cs @@ -0,0 +1,16 @@ +namespace EventAggregator.API.Services; + +/// No-op implementation used when Telegram:BotToken is not configured. +public class NullTelegramBotService : ITelegramBotService +{ + private readonly ILogger _logger; + + public NullTelegramBotService(ILogger logger) + => _logger = logger; + + public Task SendMessageAsync(long chatId, string htmlText) + { + _logger.LogDebug("Telegram not configured — skipping message to chatId={ChatId}", chatId); + return Task.CompletedTask; + } +} diff --git a/EventAggregator/Services/TelegramBotBackgroundService.cs b/EventAggregator/Services/TelegramBotBackgroundService.cs new file mode 100644 index 0000000..d7fd65a --- /dev/null +++ b/EventAggregator/Services/TelegramBotBackgroundService.cs @@ -0,0 +1,175 @@ +using EventAggregator.API.Models; +using EventAggregator.Data; +using Microsoft.EntityFrameworkCore; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace EventAggregator.API.Services; + +public class TelegramBotBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ITelegramBotClient _botClient; + private readonly ILogger _logger; + + public TelegramBotBackgroundService( + IServiceScopeFactory scopeFactory, + ITelegramBotClient botClient, + ILogger logger) + { + _scopeFactory = scopeFactory; + _botClient = botClient; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var options = new ReceiverOptions + { + AllowedUpdates = [UpdateType.Message], + DropPendingUpdates = true, + }; + + _botClient.StartReceiving( + updateHandler: HandleUpdateAsync, + errorHandler: HandleErrorAsync, + receiverOptions: options, + cancellationToken: stoppingToken); + + _logger.LogInformation("Telegram bot started (long polling)"); + + // Keep the service alive until cancellation + await Task.Delay(Timeout.Infinite, stoppingToken).ContinueWith(_ => { }); + } + + // ── Update handler ──────────────────────────────────────────────────────── + + private async Task HandleUpdateAsync( + ITelegramBotClient client, Update update, CancellationToken ct) + { + if (update.Message is not { } msg) return; + if (msg.Text is not { } text) return; + + var chatId = msg.Chat.Id; + var username = msg.From?.Username?.ToLower(); + + if (text.StartsWith("/start")) + await HandleStartAsync(client, chatId, username, ct); + else if (text.StartsWith("/digest")) + await HandleDigestAsync(client, chatId, ct); + else + await client.SendMessage(chatId, + "Привіт! Я — EventAggregator бот 🎭\n\n" + + "Доступні команди:\n" + + "/start — підключити акаунт\n" + + "/digest — отримати дайджест прямо зараз", + cancellationToken: ct); + } + + // ── /start ──────────────────────────────────────────────────────────────── + + private async Task HandleStartAsync( + ITelegramBotClient client, long chatId, string? username, CancellationToken ct) + { + if (string.IsNullOrEmpty(username)) + { + await client.SendMessage(chatId, + "❌ У вашому Telegram-акаунті не налаштовано юзернейм.\n" + + "Додайте @username у налаштуваннях Telegram і спробуйте ще раз.", + cancellationToken: ct); + return; + } + + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var user = await db.Users.FirstOrDefaultAsync( + u => u.TelegramUsername != null && + u.TelegramUsername.ToLower() == username, ct); + + if (user is null) + { + await client.SendMessage(chatId, + $"❌ Акаунт з юзернеймом @{username} не знайдено в EventAggregator.\n\n" + + "Переконайтесь, що ви правильно вказали юзернейм у профілі на сайті та увімкнули Telegram-розсилку.", + parseMode: ParseMode.Html, + cancellationToken: ct); + return; + } + + user.TelegramChatId = chatId; + await db.SaveChangesAsync(ct); + + _logger.LogInformation("Telegram linked: userId={UserId} → chatId={ChatId}", user.Id, chatId); + + await client.SendMessage(chatId, + $"✅ Акаунт підключено!\n\n" + + $"Привіт, {Escape(user.FirstName)}! 👋\n" + + $"Тепер ти отримуватимеш персональні дайджести прямо в Telegram.\n\n" + + $"Частота розсилки: {FrequencyLabel(user.ReportFrequency)}\n\n" + + $"Відправ /digest щоб отримати добірку прямо зараз.", + parseMode: ParseMode.Html, + cancellationToken: ct); + } + + // ── /digest ─────────────────────────────────────────────────────────────── + + private async Task HandleDigestAsync( + ITelegramBotClient client, long chatId, CancellationToken ct) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var digestService = scope.ServiceProvider.GetRequiredService(); + + var user = await db.Users + .Include(u => u.UserFilters).ThenInclude(uf => uf.Filter) + .FirstOrDefaultAsync(u => u.TelegramChatId == chatId, ct); + + if (user is null) + { + await client.SendMessage(chatId, + "❌ Акаунт не підключено.\nНадішли /start для підключення.", + cancellationToken: ct); + return; + } + + await client.SendMessage(chatId, "⏳ Формую дайджест...", cancellationToken: ct); + + try + { + await digestService.SendTelegramDigestAsync(user); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending on-demand digest to chatId={ChatId}", chatId); + await client.SendMessage(chatId, + "❌ Помилка формування дайджесту. Спробуйте пізніше.", + cancellationToken: ct); + } + } + + // ── Error handler ───────────────────────────────────────────────────────── + + private Task HandleErrorAsync( + ITelegramBotClient client, Exception ex, + HandleErrorSource source, CancellationToken ct) + { + _logger.LogWarning(ex, "Telegram polling error [{Source}]", source); + return Task.CompletedTask; + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static string FrequencyLabel(string f) => f switch + { + "Daily" => "Щоденний", + "Weekly" => "Тижневий", + "Monthly" => "Місячний", + _ => "Персональний", + }; + + private static string Escape(string s) => + s.Replace("&", "&").Replace("<", "<").Replace(">", ">"); +} diff --git a/EventAggregator/Services/TelegramBotService.cs b/EventAggregator/Services/TelegramBotService.cs new file mode 100644 index 0000000..80e4c30 --- /dev/null +++ b/EventAggregator/Services/TelegramBotService.cs @@ -0,0 +1,67 @@ +using Telegram.Bot; +using Telegram.Bot.Types.Enums; + +namespace EventAggregator.API.Services; + +public class TelegramBotService : ITelegramBotService +{ + private readonly ITelegramBotClient _client; + private readonly ILogger _logger; + + public TelegramBotService(ITelegramBotClient client, ILogger logger) + { + _client = client; + _logger = logger; + } + + public async Task SendMessageAsync(long chatId, string htmlText) + { + // Telegram message limit is 4096 chars — split if needed + const int limit = 4000; + + if (htmlText.Length <= limit) + { + await SendChunkAsync(chatId, htmlText); + return; + } + + // Split on double newline boundaries + var parts = SplitText(htmlText, limit); + foreach (var part in parts) + await SendChunkAsync(chatId, part); + } + + private async Task SendChunkAsync(long chatId, string text) + { + try + { + await _client.SendMessage(chatId, text, parseMode: ParseMode.Html); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Telegram message to chatId={ChatId}", chatId); + } + } + + private static List SplitText(string text, int limit) + { + var parts = new List(); + var lines = text.Split('\n'); + var current = new System.Text.StringBuilder(); + + foreach (var line in lines) + { + if (current.Length + line.Length + 1 > limit) + { + parts.Add(current.ToString().Trim()); + current.Clear(); + } + current.AppendLine(line); + } + + if (current.Length > 0) + parts.Add(current.ToString().Trim()); + + return parts; + } +} diff --git a/EventAggregator/appsettings.example.json b/EventAggregator/appsettings.example.json index faed014..faa1ee6 100644 --- a/EventAggregator/appsettings.example.json +++ b/EventAggregator/appsettings.example.json @@ -19,5 +19,8 @@ "Password": "your-app-password", "FromName": "EventAggregator", "FromEmail": "your@gmail.com" + }, + "Telegram": { + "BotToken": "YOUR-BOT-TOKEN-FROM-BOTFATHER" } } diff --git a/event-aggregator-client/src/app/core/models/user.model.ts b/event-aggregator-client/src/app/core/models/user.model.ts index 7cf269f..7d914f2 100644 --- a/event-aggregator-client/src/app/core/models/user.model.ts +++ b/event-aggregator-client/src/app/core/models/user.model.ts @@ -8,6 +8,10 @@ export interface User { photoUrl?: string; reportFrequency: string; lastReportSent?: string; + // Telegram + telegramNotificationsEnabled: boolean; + telegramUsername?: string; + telegramChatId?: number; filters: Filter[]; } @@ -24,3 +28,8 @@ export interface UpdateUserRequest { photoUrl?: string; reportFrequency?: string; } + +export interface UpdateTelegramRequest { + telegramNotificationsEnabled: boolean; + telegramUsername?: string; +} diff --git a/event-aggregator-client/src/app/core/services/users.service.ts b/event-aggregator-client/src/app/core/services/users.service.ts index 9acb63b..327af12 100644 --- a/event-aggregator-client/src/app/core/services/users.service.ts +++ b/event-aggregator-client/src/app/core/services/users.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { CreateUserRequest, UpdateUserRequest, User } from '../models/user.model'; +import { CreateUserRequest, UpdateUserRequest, UpdateTelegramRequest, User } from '../models/user.model'; import { EventItem, EventsResponse, EventStatus } from '../models/event.model'; import { Filter } from '../models/filter.model'; import { environment } from '../../../environments/environment'; @@ -22,6 +22,10 @@ export class UsersService { return this.http.put(`${this.base}/${id}`, req); } + updateTelegram(id: number, req: UpdateTelegramRequest) { + return this.http.patch<{ telegramChatId?: number }>(`${this.base}/${id}/telegram`, req); + } + getFilters(id: number) { return this.http.get(`${this.base}/${id}/filters`); } diff --git a/event-aggregator-client/src/app/features/profile/profile.html b/event-aggregator-client/src/app/features/profile/profile.html index f4f6308..41aa889 100644 --- a/event-aggregator-client/src/app/features/profile/profile.html +++ b/event-aggregator-client/src/app/features/profile/profile.html @@ -38,6 +38,58 @@ + + + +
+ send + Telegram-сповіщення +
+ +
+ + Отримувати дайджести у Telegram + + + @if (tgEnabled()) { + + Telegram юзернейм + @  + + Введіть юзернейм без @ і натисніть «Зберегти» + + + + @if (tgChatId()) { +
+ check_circle + Бот підключено — ви будете отримувати дайджести в Telegram +
+ } @else if (tgUsername()) { +
+ schedule + + Збережіть та напишіть команду /start нашому Telegram-боту, + щоб підключити акаунт + +
+ } + } + + +
} diff --git a/event-aggregator-client/src/app/features/profile/profile.scss b/event-aggregator-client/src/app/features/profile/profile.scss index 01b3819..2e70d72 100644 --- a/event-aggregator-client/src/app/features/profile/profile.scss +++ b/event-aggregator-client/src/app/features/profile/profile.scss @@ -232,6 +232,76 @@ } } +// ── Telegram-секція ─────────────────────────────────────────────────────── + +.section-divider { margin: 28px 0 20px; } + +.section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 1rem; + font-weight: 600; + color: #333; + margin-bottom: 16px; + + .tg-icon { color: #229ed9; } +} + +.telegram-block { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 480px; + + .tg-field { + width: 100%; + } + + .tg-status { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + font-size: 0.875rem; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + flex-shrink: 0; + margin-top: 1px; + } + + code { + background: rgba(0,0,0,0.08); + padding: 1px 5px; + border-radius: 4px; + font-family: monospace; + } + + &.connected { + background: #e6f4ea; + color: #1e7e34; + mat-icon { color: #34a853; } + } + + &.pending { + background: #fff8e1; + color: #795548; + mat-icon { color: #f9a825; } + } + } + + .tg-save-btn { + align-self: flex-start; + display: flex; + align-items: center; + gap: 6px; + } +} + // Виділення дат у календарі з подіями ::ng-deep .has-event .mat-calendar-body-cell-content { background-color: #e8f0fe !important; diff --git a/event-aggregator-client/src/app/features/profile/profile.ts b/event-aggregator-client/src/app/features/profile/profile.ts index c40b954..a90ca25 100644 --- a/event-aggregator-client/src/app/features/profile/profile.ts +++ b/event-aggregator-client/src/app/features/profile/profile.ts @@ -9,6 +9,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatChipsModule } from '@angular/material/chips'; import { MatDividerModule } from '@angular/material/divider'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTabsModule } from '@angular/material/tabs'; @@ -29,7 +30,7 @@ import { User } from '../../core/models/user.model'; FormsModule, DatePipe, RouterLink, QRCodeComponent, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatChipsModule, - MatDividerModule, MatProgressSpinnerModule, MatTabsModule, + MatDividerModule, MatSlideToggleModule, MatProgressSpinnerModule, MatTabsModule, MatDatepickerModule, MatNativeDateModule, ], templateUrl: './profile.html', @@ -48,6 +49,12 @@ export class Profile implements OnInit { calendarEvents = signal([]); loading = signal(true); + // Telegram + tgEnabled = signal(false); + tgUsername = signal(''); + tgChatId = signal(null); + tgSaving = signal(false); + // 2FA twoFactorEnabled = signal(false); qrUri = signal(''); @@ -86,6 +93,9 @@ export class Profile implements OnInit { next: u => { this.user.set(u); this.userFilterIds.set(new Set(u.filters.map(f => f.id))); + this.tgEnabled.set(u.telegramNotificationsEnabled); + this.tgUsername.set(u.telegramUsername ?? ''); + this.tgChatId.set(u.telegramChatId ?? null); this.loading.set(false); }, error: () => this.loading.set(false), @@ -123,6 +133,24 @@ export class Profile implements OnInit { }).subscribe(() => this.snackBar.open('Профіль збережено', '', { duration: 2000 })); } + saveTelegram() { + this.tgSaving.set(true); + this.usersService.updateTelegram(this.userId, { + telegramNotificationsEnabled: this.tgEnabled(), + telegramUsername: this.tgUsername().trim().replace(/^@/, '') || undefined, + }).subscribe({ + next: res => { + this.tgChatId.set(res.telegramChatId ?? null); + this.tgSaving.set(false); + this.snackBar.open('Telegram-налаштування збережено', '', { duration: 2500 }); + }, + error: () => { + this.tgSaving.set(false); + this.snackBar.open('Помилка збереження', 'ОК', { duration: 3000 }); + }, + }); + } + onDateSelected(date: Date | null) { const cur = this.selectedDate(); if (cur && date && cur.toDateString() === date.toDateString()) { From ad1195c105ac0f74630b58e6cfc29376de2d80f7 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 28 May 2026 19:42:37 +0300 Subject: [PATCH 03/17] chore: add VS Code compound launch for full-stack dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F5 (or Run > Start Debugging > 🚀 Full Stack) now: - builds the .NET backend and attaches the coreclr debugger - starts ng serve in a dedicated terminal and opens Chrome at localhost:4200 Both processes stop together when debugging ends (stopAll: true) Co-Authored-By: Claude Sonnet 4.6 --- .vscode/launch.json | 45 +++++++++++++++++++++++++++++++++++++++++++ .vscode/tasks.json | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..076600b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + "version": "0.2.0", + + "compounds": [ + { + // Це те, що запускається по F5 + "name": "🚀 Full Stack", + "configurations": ["Backend (.NET)", "Frontend (Chrome)"], + "stopAll": true, + "presentation": { "order": 1 } + } + ], + + "configurations": [ + + // ── .NET бекенд з дебагером ──────────────────────────────────────────────── + { + "name": "Backend (.NET)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build: backend", + "program": "${workspaceFolder}/EventAggregator/bin/Debug/net9.0/EventAggregator.dll", + "args": [], + "cwd": "${workspaceFolder}/EventAggregator", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM": "true" + }, + "presentation": { "hidden": true } + }, + + // ── Angular у Chrome ─────────────────────────────────────────────────────── + { + "name": "Frontend (Chrome)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}/event-aggregator-client/src", + "preLaunchTask": "serve: frontend", + "presentation": { "hidden": true } + } + + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..6332fe5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,47 @@ +{ + "version": "2.0.0", + "tasks": [ + + // ── Backend ──────────────────────────────────────────────────────────────── + { + "label": "build: backend", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EventAggregator/EventAggregator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile", + "group": "build", + "presentation": { "reveal": "silent" } + }, + + // ── Frontend ─────────────────────────────────────────────────────────────── + { + "label": "serve: frontend", + "type": "shell", + "command": "npm start", + "options": { + "cwd": "${workspaceFolder}/event-aggregator-client" + }, + "isBackground": true, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "label": "Angular" + }, + "problemMatcher": { + "owner": "angular", + "pattern": [{ "regexp": "^(.*)$", "message": 1 }], + "background": { + "activeOnStart": true, + "beginsPattern": "Building\\.\\.\\.", + "endsPattern": "Local:.*http://localhost" + } + } + } + + ] +} From a095d03cd255911580ba80adee938b409f97bcd7 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 28 May 2026 20:54:53 +0300 Subject: [PATCH 04/17] fix: auto-open browser on F5, resilient RSS parsing, clean feed sources launch.json: replaced compound+chrome with single coreclr launch; serverReadyAction opens localhost:4200 when .NET starts listening; preLaunchTask now builds .NET first, then waits for Angular tasks.json: serve: frontend depends on build: backend (sequential); simplified background pattern matchers for Angular 20 Vite output RssFeedService: use GetAsync to check HTTP status before parsing; skip sources returning HTML (instead of crashing); add CheckCharacters=false to XmlReaderSettings to tolerate invalid chars like 0x02; catch XmlException gracefully with a warning log instead of propagating the error DbSeeder: remove 6 broken sources (LIGA.net 404, AIN.EN bad XML, Harvard/Afisha HTML, Kingston/Stanford 404); add Reuters, AIN.UA, Hacker News as reliable replacements Co-Authored-By: Claude Sonnet 4.6 --- .vscode/launch.json | 43 ++++++------------ .vscode/tasks.json | 9 ++-- EventAggregator/Data/DbSeeder.cs | 34 ++++++-------- EventAggregator/Services/RssFeedService.cs | 53 ++++++++++++++++++---- 4 files changed, 78 insertions(+), 61 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 076600b..67d5f31 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,45 +1,30 @@ { "version": "0.2.0", - - "compounds": [ - { - // Це те, що запускається по F5 - "name": "🚀 Full Stack", - "configurations": ["Backend (.NET)", "Frontend (Chrome)"], - "stopAll": true, - "presentation": { "order": 1 } - } - ], - "configurations": [ - - // ── .NET бекенд з дебагером ──────────────────────────────────────────────── { - "name": "Backend (.NET)", + // F5 → збирає .NET → запускає Angular → відкриває браузер на localhost:4200 + "name": "🚀 Full Stack", "type": "coreclr", "request": "launch", - "preLaunchTask": "build: backend", + + // Спочатку збирає бекенд + запускає Angular (чекає поки Angular готовий) + "preLaunchTask": "serve: frontend", + + // Запускає скомпільований .NET з дебагером "program": "${workspaceFolder}/EventAggregator/bin/Debug/net9.0/EventAggregator.dll", "args": [], "cwd": "${workspaceFolder}/EventAggregator", "stopAtEntry": false, "env": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM": "true" + "ASPNETCORE_ENVIRONMENT": "Development" }, - "presentation": { "hidden": true } - }, - // ── Angular у Chrome ─────────────────────────────────────────────────────── - { - "name": "Frontend (Chrome)", - "type": "chrome", - "request": "launch", - "url": "http://localhost:4200", - "webRoot": "${workspaceFolder}/event-aggregator-client/src", - "preLaunchTask": "serve: frontend", - "presentation": { "hidden": true } + // Коли .NET виведе "Now listening on:" — автоматично відкриє браузер на :4200 + "serverReadyAction": { + "action": "openExternally", + "pattern": "Now listening on:", + "uriFormat": "http://localhost:4200" + } } - ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6332fe5..fee0eb8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ - // ── Backend ──────────────────────────────────────────────────────────────── + // ── 1. Збірка бекенду ────────────────────────────────────────────────────── { "label": "build: backend", "command": "dotnet", @@ -18,7 +18,7 @@ "presentation": { "reveal": "silent" } }, - // ── Frontend ─────────────────────────────────────────────────────────────── + // ── 2. Angular dev-сервер (запускається після збірки бекенду) ────────────── { "label": "serve: frontend", "type": "shell", @@ -26,6 +26,7 @@ "options": { "cwd": "${workspaceFolder}/event-aggregator-client" }, + "dependsOn": ["build: backend"], "isBackground": true, "presentation": { "reveal": "always", @@ -37,8 +38,8 @@ "pattern": [{ "regexp": "^(.*)$", "message": 1 }], "background": { "activeOnStart": true, - "beginsPattern": "Building\\.\\.\\.", - "endsPattern": "Local:.*http://localhost" + "beginsPattern": "Building", + "endsPattern": "localhost:4200" } } } diff --git a/EventAggregator/Data/DbSeeder.cs b/EventAggregator/Data/DbSeeder.cs index a6ab899..78b0485 100644 --- a/EventAggregator/Data/DbSeeder.cs +++ b/EventAggregator/Data/DbSeeder.cs @@ -15,28 +15,24 @@ private static void SeedFeedSources(AppDbContext db) if (db.FeedSources.Any()) return; db.FeedSources.AddRange( - // --- Новини --- - new FeedSource { Name = "Українська правда", FeedUrl = "https://www.pravda.com.ua/rss/view_news/", Category = "news", Language = "uk" }, - new FeedSource { Name = "LIGA.net", FeedUrl = "https://www.liga.net/en/news/rss.xml", Category = "news", Language = "en" }, - new FeedSource { Name = "AIN.EN", FeedUrl = "https://en.ain.ua/feed/", Category = "tech", Language = "en" }, - new FeedSource { Name = "dev.ua", FeedUrl = "https://dev.ua/rss", Category = "tech", Language = "uk" }, - new FeedSource { Name = "BBC News", FeedUrl = "http://feeds.bbci.co.uk/news/rss.xml", Category = "news", Language = "en" }, - new FeedSource { Name = "The Guardian", FeedUrl = "https://www.theguardian.com/world/rss", Category = "news", Language = "en" }, - new FeedSource { Name = "MIT News", FeedUrl = "https://news.mit.edu/rss/feed", Category = "science", Language = "en" }, + // --- Новини (перевірено — RSS коректний) --- + new FeedSource { Name = "Українська правда", FeedUrl = "https://www.pravda.com.ua/rss/view_news/", Category = "news", Language = "uk", IsActive = true }, + new FeedSource { Name = "BBC News", FeedUrl = "https://feeds.bbci.co.uk/news/rss.xml", Category = "news", Language = "en", IsActive = true }, + new FeedSource { Name = "The Guardian", FeedUrl = "https://www.theguardian.com/world/rss", Category = "news", Language = "en", IsActive = true }, + new FeedSource { Name = "Reuters", FeedUrl = "https://feeds.reuters.com/reuters/topNews", Category = "news", Language = "en", IsActive = true }, - // --- IT-події --- - new FeedSource { Name = "DOU Calendar", FeedUrl = "https://dou.ua/calendar/feed/", Category = "events", Language = "uk" }, + // --- IT / Технології --- + new FeedSource { Name = "dev.ua", FeedUrl = "https://dev.ua/rss", Category = "tech", Language = "uk", IsActive = true }, + new FeedSource { Name = "AIN.UA", FeedUrl = "https://ain.ua/feed/", Category = "tech", Language = "uk", IsActive = true }, + new FeedSource { Name = "MIT News", FeedUrl = "https://news.mit.edu/rss/feed", Category = "science", Language = "en", IsActive = true }, + new FeedSource { Name = "Hacker News", FeedUrl = "https://hnrss.org/frontpage", Category = "tech", Language = "en", IsActive = true }, - // --- Академічні події --- - new FeedSource { Name = "Stanford Events", FeedUrl = "https://events.stanford.edu/rss", Category = "events", Language = "en" }, - new FeedSource { Name = "Harvard Gazette", FeedUrl = "https://news.harvard.edu/gazette/feed/harvard-events/", Category = "events", Language = "en" }, + // --- IT-події --- + new FeedSource { Name = "DOU Calendar", FeedUrl = "https://dou.ua/calendar/feed/", Category = "events", Language = "uk", IsActive = true }, - // --- Культурні / концертні події --- - new FeedSource { Name = "Афіша Запоріжжя", FeedUrl = "https://afisha.zp.ua/docs/feedrss/", Category = "events", Language = "uk" }, - new FeedSource { Name = "AfishaLviv Концерти", FeedUrl = "https://afishalviv.net/category/koncerti-ta-festivali/feed/", Category = "events", Language = "uk" }, - new FeedSource { Name = "LvivOnline", FeedUrl = "https://lviv-online.com/ua/feed/", Category = "events", Language = "uk" }, - new FeedSource { Name = "OBX Things Events", FeedUrl = "https://www.obxthings.com/events.rss", Category = "events", Language = "en" }, - new FeedSource { Name = "Kingston Live", FeedUrl = "https://kingstonlive.ca/calendar/rss", Category = "events", Language = "en" } + // --- Культурні / місцеві події --- + new FeedSource { Name = "AfishaLviv Концерти", FeedUrl = "https://afishalviv.net/category/koncerti-ta-festivali/feed/", Category = "events", Language = "uk", IsActive = true }, + new FeedSource { Name = "LvivOnline", FeedUrl = "https://lviv-online.com/ua/feed/", Category = "events", Language = "uk", IsActive = true } ); db.SaveChanges(); diff --git a/EventAggregator/Services/RssFeedService.cs b/EventAggregator/Services/RssFeedService.cs index f31111b..5fb713b 100644 --- a/EventAggregator/Services/RssFeedService.cs +++ b/EventAggregator/Services/RssFeedService.cs @@ -40,12 +40,47 @@ public async Task RefreshAllFeedsAsync() public async Task RefreshFeedAsync(FeedSource source) { - var stream = await _httpClient.GetStreamAsync(source.FeedUrl); + // ── 1. HTTP-запит з перевіркою статусу ─────────────────────────────── + using var response = await _httpClient.GetAsync(source.FeedUrl); - var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; - using var reader = XmlReader.Create(stream, settings); - var feed = SyndicationFeed.Load(reader); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Джерело {Name}: HTTP {Status} — пропускаємо", + source.Name, (int)response.StatusCode); + return; + } + + var contentType = response.Content.Headers.ContentType?.MediaType ?? ""; + if (contentType.Contains("text/html", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("Джерело {Name}: повернуло HTML замість RSS — пропускаємо", + source.Name); + return; + } + + // ── 2. Парсинг XML (толерантний до поганих символів та DTD) ────────── + using var stream = await response.Content.ReadAsStreamAsync(); + + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Ignore, + CheckCharacters = false, // ігноруємо неприпустимі символи (0x02 тощо) + IgnoreWhitespace = false, + }; + + SyndicationFeed feed; + try + { + using var reader = XmlReader.Create(stream, settings); + feed = SyndicationFeed.Load(reader); + } + catch (XmlException ex) + { + _logger.LogWarning("Джерело {Name}: некоректний XML — {Msg}", source.Name, ex.Message); + return; + } + // ── 3. Збереження нових подій ───────────────────────────────────────── var existingUrls = (await _db.Events .Where(e => e.FeedSourceId == source.Id) .Select(e => e.SourceUrl) @@ -62,16 +97,16 @@ public async Task RefreshFeedAsync(FeedSource source) newEvents.Add(new Event { - Title = item.Title?.Text ?? string.Empty, + Title = item.Title?.Text ?? string.Empty, Description = item.Summary?.Text ?? string.Empty, - SourceUrl = url, + SourceUrl = url, PublishedDate = item.PublishDate == default ? DateTime.UtcNow : item.PublishDate.UtcDateTime, FeedSourceId = source.Id, - Category = source.Category, - Author = item.Authors.FirstOrDefault()?.Name, - ImageUrl = item.Links + Category = source.Category, + Author = item.Authors.FirstOrDefault()?.Name, + ImageUrl = item.Links .FirstOrDefault(l => l.RelationshipType == "enclosure")?.Uri?.ToString(), }); } From 2619443bfbb8442670894cc98682a291fbe9b956 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 28 May 2026 22:22:50 +0300 Subject: [PATCH 05/17] + --- .../Controllers/AdminController.cs | 93 +++++- EventAggregator/Controllers/AuthController.cs | 3 + .../Controllers/FeedsController.cs | 2 +- .../20260528191942_AddIsBanned.Designer.cs | 304 ++++++++++++++++++ .../Migrations/20260528191942_AddIsBanned.cs | 29 ++ .../Migrations/AppDbContextModelSnapshot.cs | 3 + EventAggregator/Models/User.cs | 2 + EventAggregator/events.db | Bin 323584 -> 512000 bytes EventAggregator/events.db-shm | Bin 32768 -> 32768 bytes EventAggregator/events.db-wal | Bin 119512 -> 103032 bytes .../src/app/core/services/feeds.service.ts | 43 ++- .../src/app/features/admin/admin.html | 249 +++++++++----- .../src/app/features/admin/admin.scss | 46 ++- .../src/app/features/admin/admin.ts | 76 ++++- .../features/admin/user-detail-dialog.html | 94 ++++++ .../features/admin/user-detail-dialog.scss | 95 ++++++ .../app/features/admin/user-detail-dialog.ts | 71 ++++ .../src/environments/environment.ts | 2 +- 18 files changed, 1011 insertions(+), 101 deletions(-) create mode 100644 EventAggregator/Migrations/20260528191942_AddIsBanned.Designer.cs create mode 100644 EventAggregator/Migrations/20260528191942_AddIsBanned.cs create mode 100644 event-aggregator-client/src/app/features/admin/user-detail-dialog.html create mode 100644 event-aggregator-client/src/app/features/admin/user-detail-dialog.scss create mode 100644 event-aggregator-client/src/app/features/admin/user-detail-dialog.ts diff --git a/EventAggregator/Controllers/AdminController.cs b/EventAggregator/Controllers/AdminController.cs index 95ff4da..14394de 100644 --- a/EventAggregator/Controllers/AdminController.cs +++ b/EventAggregator/Controllers/AdminController.cs @@ -13,23 +13,30 @@ public class AdminController : ControllerBase private readonly AppDbContext _db; public AdminController(AppDbContext db) => _db = db; + // ── RSS ─────────────────────────────────────────────────────────────────── + + [HttpPatch("feeds/{id}/toggle")] + public async Task ToggleFeed(int id) + { + var source = await _db.FeedSources.FindAsync(id); + if (source is null) return NotFound(); + source.IsActive = !source.IsActive; + await _db.SaveChangesAsync(); + return Ok(new { source.Id, source.IsActive }); + } + /// Загальна статистика + дані по кожному джерелу [HttpGet("stats")] public async Task GetStats() { - var totalEvents = await _db.Events.CountAsync(); - var totalUsers = await _db.Users.CountAsync(); + var totalEvents = await _db.Events.CountAsync(); + var totalUsers = await _db.Users.CountAsync(); var activeSources = await _db.FeedSources.CountAsync(s => s.IsActive); var sources = await _db.FeedSources .Select(s => new { - s.Id, - s.Name, - s.Category, - s.Language, - s.IsActive, - s.LastFetched, + s.Id, s.Name, s.Category, s.Language, s.IsActive, s.LastFetched, EventCount = s.Events.Count(), }) .OrderByDescending(s => s.EventCount) @@ -37,4 +44,74 @@ public async Task GetStats() return Ok(new { totalEvents, totalUsers, activeSources, sources }); } + + // ── Users ───────────────────────────────────────────────────────────────── + + [HttpGet("users")] + public async Task GetUsers() + { + var users = await _db.Users + .Select(u => new + { + u.Id, u.FirstName, u.LastName, u.Email, + u.IsBanned, u.ReportFrequency, + u.TelegramNotificationsEnabled, u.TelegramChatId, + FilterCount = u.UserFilters.Count(), + SavedCount = u.UserEventStatuses.Count(s => s.Status == "Interesting"), + CalendarCount = u.UserEventStatuses.Count(s => s.IsInCalendar), + }) + .OrderBy(u => u.Id) + .ToListAsync(); + + return Ok(users); + } + + [HttpGet("users/{id}")] + public async Task GetUser(int id) + { + var user = await _db.Users + .Include(u => u.UserFilters).ThenInclude(uf => uf.Filter) + .FirstOrDefaultAsync(u => u.Id == id); + + if (user is null) return NotFound(); + + return Ok(new + { + user.Id, user.FirstName, user.LastName, user.Email, + user.IsBanned, user.ReportFrequency, user.LastReportSent, + user.TwoFactorEnabled, + user.TelegramNotificationsEnabled, user.TelegramUsername, user.TelegramChatId, + Filters = user.UserFilters.Select(uf => new { uf.Filter.Id, uf.Filter.Name, uf.Filter.Type }), + SavedCount = await _db.UserEventStatuses.CountAsync(s => s.UserId == id && s.Status == "Interesting"), + CalendarCount = await _db.UserEventStatuses.CountAsync(s => s.UserId == id && s.IsInCalendar), + }); + } + + [HttpPost("users/{id}/toggle-ban")] + public async Task ToggleBan(int id) + { + var user = await _db.Users.FindAsync(id); + if (user is null) return NotFound(); + user.IsBanned = !user.IsBanned; + await _db.SaveChangesAsync(); + return Ok(new { user.Id, user.IsBanned }); + } + + [HttpDelete("users/{id}")] + public async Task DeleteUser(int id) + { + var user = await _db.Users + .Include(u => u.UserFilters) + .Include(u => u.UserEventStatuses) + .FirstOrDefaultAsync(u => u.Id == id); + + if (user is null) return NotFound(); + + _db.UserFilters.RemoveRange(user.UserFilters); + _db.UserEventStatuses.RemoveRange(user.UserEventStatuses); + _db.Users.Remove(user); + await _db.SaveChangesAsync(); + + return NoContent(); + } } diff --git a/EventAggregator/Controllers/AuthController.cs b/EventAggregator/Controllers/AuthController.cs index 9fd0304..0a0b304 100644 --- a/EventAggregator/Controllers/AuthController.cs +++ b/EventAggregator/Controllers/AuthController.cs @@ -54,6 +54,9 @@ public async Task Login(LoginRequest req) if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) return Unauthorized(new { message = "Невірний email або пароль" }); + if (user.IsBanned) + return Unauthorized(new { message = "Ваш акаунт заблоковано. Зв'яжіться з адміністратором: adamyocardium@gmail.com" }); + if (user.TwoFactorEnabled) return Ok(new { requiresTwoFactor = true, userId = user.Id }); diff --git a/EventAggregator/Controllers/FeedsController.cs b/EventAggregator/Controllers/FeedsController.cs index f94b91d..8bce2ca 100644 --- a/EventAggregator/Controllers/FeedsController.cs +++ b/EventAggregator/Controllers/FeedsController.cs @@ -39,6 +39,6 @@ public async Task RefreshOne(int id) if (source is null) return NotFound(); await _feedService.RefreshFeedAsync(source); - return Ok(new { message = $"Стрічку «{source.Name}» оновлено" }); + return Ok(new { message = $"Стрічку «{source.Name}» оновлено", lastFetched = source.LastFetched }); } } diff --git a/EventAggregator/Migrations/20260528191942_AddIsBanned.Designer.cs b/EventAggregator/Migrations/20260528191942_AddIsBanned.Designer.cs new file mode 100644 index 0000000..5e08a65 --- /dev/null +++ b/EventAggregator/Migrations/20260528191942_AddIsBanned.Designer.cs @@ -0,0 +1,304 @@ +// +using System; +using EventAggregator.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventAggregator.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260528191942_AddIsBanned")] + partial class AddIsBanned + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("ContentHtml") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("FeedSourceId") + .HasColumnType("INTEGER"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("MaxPeople") + .HasColumnType("INTEGER"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.Property("SourceUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FeedSourceId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastFetched") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FeedSources"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SearchKeyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Filters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastReportSent") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasColumnType("TEXT"); + + b.Property("ReportFrequency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelegramChatId") + .HasColumnType("INTEGER"); + + b.Property("TelegramNotificationsEnabled") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IsInCalendar") + .HasColumnType("INTEGER"); + + b.Property("MarkedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("UserId"); + + b.ToTable("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FilterId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FilterId"); + + b.HasIndex("FilterId"); + + b.ToTable("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.HasOne("EventAggregator.API.Models.FeedSource", "FeedSource") + .WithMany("Events") + .HasForeignKey("FeedSourceId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FeedSource"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.HasOne("EventAggregator.API.Models.Event", "Event") + .WithMany("UserEventStatuses") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserEventStatuses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.HasOne("EventAggregator.API.Models.Filter", "Filter") + .WithMany("UserFilters") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserFilters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Filter"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Navigation("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Navigation("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Navigation("UserEventStatuses"); + + b.Navigation("UserFilters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventAggregator/Migrations/20260528191942_AddIsBanned.cs b/EventAggregator/Migrations/20260528191942_AddIsBanned.cs new file mode 100644 index 0000000..6ca4764 --- /dev/null +++ b/EventAggregator/Migrations/20260528191942_AddIsBanned.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventAggregator.Migrations +{ + /// + public partial class AddIsBanned : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsBanned", + table: "Users", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsBanned", + table: "Users"); + } + } +} diff --git a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs index 1cbf2b8..5833477 100644 --- a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs +++ b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs @@ -139,6 +139,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("IsBanned") + .HasColumnType("INTEGER"); + b.Property("LastName") .IsRequired() .HasColumnType("TEXT"); diff --git a/EventAggregator/Models/User.cs b/EventAggregator/Models/User.cs index 58a9520..3080718 100644 --- a/EventAggregator/Models/User.cs +++ b/EventAggregator/Models/User.cs @@ -24,6 +24,8 @@ public class User public bool TwoFactorEnabled { get; set; } = false; public string? TwoFactorSecret { get; set; } + public bool IsBanned { get; set; } = false; + // ── Telegram ────────────────────────────────────────────────────────────── public bool TelegramNotificationsEnabled { get; set; } = false; public string? TelegramUsername { get; set; } // введено в профілі (без @) diff --git a/EventAggregator/events.db b/EventAggregator/events.db index 9111846ea85f5799179a0c5ad6987a3592d4ce32..e86550b26579cf93bd2db16e66d3debe6e4c7460 100644 GIT binary patch delta 138033 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 z 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 !rY?;*Z+{;pl5S(TU3Y=ml5iK&`GGM3L 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

M74|E~ouTiZ*z_KWEYmGf)rmZ7N=f zWwvbW*&zzOZrhc3zU#lOC*1P!EVQh-~2y)w}{ux+!ecHxl3G74d~?+im^s5VXHD{&~(k+iCKyCo%Z&xVD4VX z+xyUf7;DsWs?JTuO|lBZ0X6IzDT09XAPmLd_$(v$nqPST>ZD2Er;A?tc2>|MX5I2d zQUn#sb{ky{s)8!m+Zsj8E^XIvp{w;}2?xItwdt%U_q8cHee0{EW0M7$cgh|Z*Cy@6FZV!o_Z-Fg4sJTQr%=OZA+lT?>~??}`%Zi()N3 z60(OJQeJydePbkdTfpyP`1Y6gc|j#X;XlUed(ibEBg`k5VGbHyNvI0X^q9woM*&Sh zt}Z_z**>GOGAu8uaZYA(DLCg{k+6;rTkJk&i zd5RFF{jq9@@+*9?Ywt1aEyl_LtCBDZSm^U6FNI_is(gNmgYdKa*h(Te2>P3p!v^E~ z7|3qedH4JAlMg^0u5MU9%VOO|l1m9tZ;m3JmuBVQO`svJY83t1;A`Nima)ZxrlzDW zcrQys=Z^Dox2}`YZm!HCV3Lp$!?l1Hb(RuTM-(M>CrZ-i@04s~8#Y2wZumOVN`(AA zsWNH}RUM1BWr|rL`iWi8pokaAQ?ENCr&?KEjFaFgNDC9u_EXjA9e3|d9% zNB9s<9f>dp%=oSgUMS;{lmjCkGrWX9=O&3|(1B@-#RxW3_};7(s2OkFVD6K?Uz1kq za)?fuQRBESMO8}XKVG1qURth)Q#6W>5q8a-abJ$flsQ9iE=4f-W&C=rB%bzc(Zev} zm-xj4{WmJPN@h#RRq86Q>->LQuhsNammxVva0y`*=Wo-TNGt&h%QR8l7GZJJ13jDd zCq0~;xdjIY-rRD@>8(2k-o)(u)-VTdFOtbH`IN> zM|NWYM?-=nR}UC+YUE_YC*qEvHiqcj z2$zBn1P6-;NFIYU(*@emJqft`9Jqdg$3MP0_5&a>mm*dqtI;E2Q9XP~CmNhDhd{Ghaf?I%FYK;@{%VH|`)QP&Rbk+w34MZPut`{zGrFTADnqct}NV;3b8h$s7vvgKkv`abA&jY z>Q-o~X+iA@gsz*Yk_Yjx4!OUW1?C-~V=`B5y9z8K$t4VPV!=$>T-q3N3@A|tnyFVt{;Lp;>GS{at zhiGgHt0f;8Yr<&Q!*mjrTP`f=LS=TziAc@5ReXfR9At9@(Na}Ti-3}N&1L?Bsgay0 zP*DcSnp!l+xM^{YeNy;r&0j}!%!5d^TDaHZTDTgEL>Gfr{O+6kuVD%|l1CCIBh|BZIJ~{0lRu>IQ9M)(D&K zdjzM-5?!uY#h<4XNrYb$1>I#qJWjs3&~$zAj&GDKRUqtzut-ja5SYV>$|z{@V<5VX zHf5%%C2)Lh0 zU8vySD`sg+!Ue_XqZ|Yxly0duadiD9vrCg7$$c^syR2;+vnVlbq0V`S0`N~i#$AyExsJ&47ys)r?05Djc%W2Y#F zMbT>;P$&n`0(ypWmXE)RNUkA6Q)nEZMKp`XZFUQh@ko;LdwAlG#z{8XM1Dq(lChRE zq^C7m!8~H_Gh+;4ZZOxF0p=oejyX-}Sd+CUmV#E0e^O@gd%wF_Cz^%m75a$g(X8$I z6jgbgR3+?8r~1F9+c55+(l&W}n50wmLO~A5jD5_3Q71}qjBAAL~sJc!fUzRF|K~-IReZDQx8H{!Hq>2-tg2yI7BmxUqHL1g#f_)1CK!^b z==O)3zfP1AU{%m`6)TcHwdUkEg}IsajuRs+%-E$Jgw~sHYixdogK_9{j{wxfaojTV zgYmP++#{2hUP60W$$Vou#MEda1QCgrxkrm3SZh5jlb!j60PJ%(K;`w08feR5i^_M@ IIG?}$4fgog)&Kwi diff --git a/EventAggregator/events.db-shm b/EventAggregator/events.db-shm index 9ec1e95e468ef438143987d980fe8ffbec1cd069..0e08165850a260d3f3b5716b37341237662e5919 100644 GIT binary patch delta 326 zcmZo@U}|V!s+V}A%K!obK+FiFB!P4-5Kli6x;b3KSb;%#!4$sZK?fV}Okp)a`{^rNTyqSsdm6RYS12+Q? h&{9F5ql6hm7({^zUUD$9PG)4(WaeTJ0CK-`0sv;2JXQbz delta 345 zcmZo@U}|V!s+V}A%K!p=K+MR%ARr5*1A+MT$=nS8sUB{Yd2L-?mdXz&EI6b->sUND zsp^47fdR0@M5!?8Y)rpa90YP z+4%7{6SD+^?8c8jSp}IHSb)}Z0<9Ee5MmH!5ZU-Kmx+-ZY!nZJz{HQWj9eQ(D)IsV DDR@cG diff --git a/EventAggregator/events.db-wal b/EventAggregator/events.db-wal index 42333512481bf99347eca6aa15d348a906b0f868..7464f3a28f72644adbde1b126a27ef93750df5c6 100644 GIT binary patch literal 103032 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 S0b+m{AO?s5#=zmt3;Z8Qp0Moz literal 119512 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 CVqvNP diff --git a/event-aggregator-client/src/app/core/services/feeds.service.ts b/event-aggregator-client/src/app/core/services/feeds.service.ts index ff1e100..fdf5540 100644 --- a/event-aggregator-client/src/app/core/services/feeds.service.ts +++ b/event-aggregator-client/src/app/core/services/feeds.service.ts @@ -19,6 +19,27 @@ export interface AdminStats { sources: SourceStats[]; } +export interface AdminUser { + id: number; + firstName: string; + lastName: string; + email: string; + isBanned: boolean; + reportFrequency: string; + telegramNotificationsEnabled: boolean; + telegramChatId: number | null; + filterCount: number; + savedCount: number; + calendarCount: number; +} + +export interface AdminUserDetail extends AdminUser { + lastReportSent: string | null; + twoFactorEnabled: boolean; + telegramUsername: string | null; + filters: { id: number; name: string; type: string }[]; +} + @Injectable({ providedIn: 'root' }) export class FeedsService { private http = inject(HttpClient); @@ -33,6 +54,26 @@ export class FeedsService { } refreshOne(id: number) { - return this.http.post<{ message: string }>(`${this.base}/feeds/${id}/refresh`, {}); + return this.http.post<{ message: string; lastFetched: string }>(`${this.base}/feeds/${id}/refresh`, {}); + } + + toggleActive(id: number) { + return this.http.patch<{ id: number; isActive: boolean }>(`${this.base}/admin/feeds/${id}/toggle`, {}); + } + + getUsers() { + return this.http.get(`${this.base}/admin/users`); + } + + getUserDetail(id: number) { + return this.http.get(`${this.base}/admin/users/${id}`); + } + + toggleBan(id: number) { + return this.http.post<{ id: number; isBanned: boolean }>(`${this.base}/admin/users/${id}/toggle-ban`, {}); + } + + deleteUser(id: number) { + return this.http.delete(`${this.base}/admin/users/${id}`); } } diff --git a/event-aggregator-client/src/app/features/admin/admin.html b/event-aggregator-client/src/app/features/admin/admin.html index e7850ea..fec046a 100644 --- a/event-aggregator-client/src/app/features/admin/admin.html +++ b/event-aggregator-client/src/app/features/admin/admin.html @@ -52,90 +52,173 @@

admin_panel_settings Адміністрування

- + -

list RSS-джерела

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Джерело - {{ row.name }} - Категорія - {{ row.category }} - Мова - {{ row.language === 'uk' ? '🇺🇦' : '🇬🇧' }} - Подій -
- {{ row.eventCount }} - -
-
Оновлено - - {{ timeAgo(row.lastFetched) }} - - Статус - - {{ row.isActive ? 'Активне' : 'Вимкнено' }} - - - -
+ + + + + + rss_feed RSS-джерела + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Джерело + {{ row.name }} + Категорія + {{ row.category }} + Мова + {{ row.language === 'uk' ? '🇺🇦' : '🇬🇧' }} + Подій +
+ {{ row.eventCount }} + +
+
Оновлено + + {{ timeAgo(row.lastFetched) }} + + Активне + + + +
+
+
+ + + + + people Користувачі + + +
+ @if (usersLoading()) { +
+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ {{ initials(row) }} +
+
Ім'я + {{ row.firstName }} {{ row.lastName }} + Email + {{ row.email }} + Фільтри{{ row.filterCount }}Telegram + @if (row.telegramChatId) { + check_circle + } @else { + cancel + } + Статус + + {{ row.isBanned ? 'Заблокований' : 'Активний' }} + + + +
+ } +
+
+ +
} diff --git a/event-aggregator-client/src/app/features/admin/admin.scss b/event-aggregator-client/src/app/features/admin/admin.scss index 55ab0ac..7dae78e 100644 --- a/event-aggregator-client/src/app/features/admin/admin.scss +++ b/event-aggregator-client/src/app/features/admin/admin.scss @@ -136,5 +136,47 @@ &.cat-science { background: #fce8e6 !important; color: #d93025 !important; } } -.chip-active { background: #e6f4ea !important; color: #34a853 !important; font-size: 0.75rem !important; } -.chip-inactive { background: #f5f5f5 !important; color: #9aa0a6 !important; font-size: 0.75rem !important; } +.chip-active { background: #e6f4ea !important; color: #34a853 !important; font-size: 0.75rem !important; } +.chip-inactive { background: #f5f5f5 !important; color: #9aa0a6 !important; font-size: 0.75rem !important; } +.chip-active-user { background: #e6f4ea !important; color: #34a853 !important; font-size: 0.75rem !important; } +.chip-banned { background: #fce8e6 !important; color: #d93025 !important; font-size: 0.75rem !important; } + +// ── Вкладки ─────────────────────────────────────────────────────────────── + +.tab-icon { font-size: 18px; margin-right: 6px; vertical-align: middle; } + +.tab-content { padding-top: 16px; } + +// ── Таблиця користувачів ────────────────────────────────────────────────── + +.users-table { + width: 100%; + + th { font-weight: 600; color: #555; font-size: 0.8rem; } + + .user-email-cell { font-size: 0.85rem; color: #555; } + + .user-row { cursor: pointer; } +} + +.user-avatar-sm { + width: 34px; + height: 34px; + border-radius: 50%; + background: #1a73e8; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.8rem; + + &.banned { background: #d93025; } +} + +.tg-icon { + font-size: 18px; + color: #9aa0a6; + + &.connected { color: #34a853; } +} diff --git a/event-aggregator-client/src/app/features/admin/admin.ts b/event-aggregator-client/src/app/features/admin/admin.ts index 72b7d7c..0fc2deb 100644 --- a/event-aggregator-client/src/app/features/admin/admin.ts +++ b/event-aggregator-client/src/app/features/admin/admin.ts @@ -10,7 +10,11 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDividerModule } from '@angular/material/divider'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { FeedsService, AdminStats, SourceStats } from '../../core/services/feeds.service'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatDialog } from '@angular/material/dialog'; +import { FeedsService, AdminStats, SourceStats, AdminUser } from '../../core/services/feeds.service'; +import { UserDetailDialog } from './user-detail-dialog'; @Component({ selector: 'app-admin', @@ -19,7 +23,7 @@ import { FeedsService, AdminStats, SourceStats } from '../../core/services/feeds DatePipe, DecimalPipe, MatCardModule, MatTableModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatProgressBarModule, MatChipsModule, - MatTooltipModule, MatDividerModule, + MatTooltipModule, MatDividerModule, MatSlideToggleModule, MatTabsModule, ], templateUrl: './admin.html', styleUrl: './admin.scss', @@ -27,15 +31,24 @@ import { FeedsService, AdminStats, SourceStats } from '../../core/services/feeds export class Admin implements OnInit { private feedsService = inject(FeedsService); private snackBar = inject(MatSnackBar); + private dialog = inject(MatDialog); stats = signal(null); loading = signal(true); refreshingAll = signal(false); refreshingId = signal(null); + togglingId = signal(null); - readonly columns = ['name', 'category', 'language', 'events', 'lastFetched', 'status', 'actions']; + users = signal([]); + usersLoading = signal(true); - ngOnInit() { this.loadStats(); } + readonly sourceColumns = ['name', 'category', 'language', 'events', 'lastFetched', 'status', 'actions']; + readonly userColumns = ['avatar', 'name', 'email', 'filters', 'telegram', 'status', 'actions']; + + ngOnInit() { + this.loadStats(); + this.loadUsers(); + } loadStats() { this.loading.set(true); @@ -45,6 +58,14 @@ export class Admin implements OnInit { }); } + loadUsers() { + this.usersLoading.set(true); + this.feedsService.getUsers().subscribe({ + next: u => { this.users.set(u); this.usersLoading.set(false); }, + error: () => this.usersLoading.set(false), + }); + } + refreshAll() { this.refreshingAll.set(true); this.feedsService.refreshAll().subscribe({ @@ -66,7 +87,9 @@ export class Admin implements OnInit { next: r => { this.refreshingId.set(null); this.snackBar.open(r.message, '', { duration: 3000 }); - this.loadStats(); + source.lastFetched = r.lastFetched; + const s = this.stats(); + if (s) this.stats.set({ ...s }); }, error: () => { this.refreshingId.set(null); @@ -75,6 +98,49 @@ export class Admin implements OnInit { }); } + toggleActive(source: SourceStats) { + this.togglingId.set(source.id); + this.feedsService.toggleActive(source.id).subscribe({ + next: r => { + source.isActive = r.isActive; + this.togglingId.set(null); + const s = this.stats(); + if (s) { + s.activeSources += r.isActive ? 1 : -1; + this.stats.set({ ...s }); + } + }, + error: () => { + this.togglingId.set(null); + this.snackBar.open('Помилка зміни статусу', 'ОК', { duration: 3000 }); + }, + }); + } + + openUser(user: AdminUser) { + const ref = this.dialog.open(UserDetailDialog, { + data: { ...user }, + width: '480px', + }); + + ref.afterClosed().subscribe(result => { + if (!result) return; + if (result.action === 'delete') { + this.users.update(list => list.filter(u => u.id !== user.id)); + const s = this.stats(); + if (s) this.stats.set({ ...s, totalUsers: s.totalUsers - 1 }); + } else if (result.action === 'ban') { + this.users.update(list => + list.map(u => u.id === user.id ? { ...u, isBanned: result.isBanned } : u) + ); + } + }); + } + + initials(u: AdminUser) { + return (u.firstName[0] + u.lastName[0]).toUpperCase(); + } + timeAgo(date: string | null): string { if (!date) return 'Ніколи'; const diff = Date.now() - new Date(date).getTime(); diff --git a/event-aggregator-client/src/app/features/admin/user-detail-dialog.html b/event-aggregator-client/src/app/features/admin/user-detail-dialog.html new file mode 100644 index 0000000..7e27639 --- /dev/null +++ b/event-aggregator-client/src/app/features/admin/user-detail-dialog.html @@ -0,0 +1,94 @@ +
+
+
{{ initials }}
+
+

{{ data.firstName }} {{ data.lastName }}

+ {{ data.email }} +
+ + {{ data.isBanned ? 'Заблоковано' : 'Активний' }} + +
+ + + @if (loading()) { +
+ } + + @if (!loading() && detail(); as d) { +
+
+ filter_list + {{ d.filters.length }} + фільтрів +
+
+ star + {{ d.savedCount }} + збережено +
+
+ calendar_today + {{ d.calendarCount }} + у календарі +
+
+ + + +
+
+ Розсилка + {{ d.reportFrequency }} +
+
+ Остання розсилка + {{ d.lastReportSent ? (d.lastReportSent | date:'dd.MM.yyyy HH:mm') : 'Ніколи' }} +
+
+ 2FA + {{ d.twoFactorEnabled ? '✅ Увімкнено' : '❌ Вимкнено' }} +
+
+ Telegram + + @if (d.telegramChatId) { + ✅ @{{ d.telegramUsername }} + } @else if (d.telegramUsername) { + ⏳ @{{ d.telegramUsername }} (очікує /start) + } @else { + — не підключено + } + +
+
+ + @if (d.filters.length > 0) { + +
+

Підписки

+
+ @for (f of d.filters; track f.id) { + {{ f.name }} + } +
+
+ } + } +
+ + + + + + +
diff --git a/event-aggregator-client/src/app/features/admin/user-detail-dialog.scss b/event-aggregator-client/src/app/features/admin/user-detail-dialog.scss new file mode 100644 index 0000000..aebab8b --- /dev/null +++ b/event-aggregator-client/src/app/features/admin/user-detail-dialog.scss @@ -0,0 +1,95 @@ +.user-dialog { + min-width: 420px; +} + +.dialog-header { + display: flex; + align-items: center; + gap: 14px; + padding-bottom: 4px; + + h2 { margin: 0; font-size: 1.1rem; font-weight: 600; } +} + +.user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: #1a73e8; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.1rem; + flex-shrink: 0; +} + +.user-email { font-size: 0.82rem; color: #888; } + +.user-title { flex: 1; } + +.chip-active-user { background: #e6f4ea !important; color: #34a853 !important; font-size: 0.75rem !important; } +.chip-banned { background: #fce8e6 !important; color: #d93025 !important; font-size: 0.75rem !important; } + +.dialog-spinner { display: flex; justify-content: center; padding: 32px 0; } + +.detail-stats { + display: flex; + gap: 24px; + padding: 16px 0; +} + +.dstat { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + flex: 1; + + mat-icon { color: #1a73e8; font-size: 20px; } + .dstat-val { font-size: 1.3rem; font-weight: 700; } + .dstat-lbl { font-size: 0.75rem; color: #888; } +} + +.detail-rows { + padding: 12px 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.detail-row { + display: flex; + gap: 8px; + font-size: 0.88rem; +} + +.detail-label { + color: #888; + min-width: 130px; +} + +.filters-section { + padding: 12px 0 4px; +} + +.filters-title { + margin: 0 0 8px; + font-size: 0.82rem; + color: #888; + font-weight: 500; +} + +.filters-wrap { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.filter-chip { + font-size: 0.75rem !important; + height: 24px !important; + background: #e8f0fe !important; + color: #1a73e8 !important; +} diff --git a/event-aggregator-client/src/app/features/admin/user-detail-dialog.ts b/event-aggregator-client/src/app/features/admin/user-detail-dialog.ts new file mode 100644 index 0000000..e981100 --- /dev/null +++ b/event-aggregator-client/src/app/features/admin/user-detail-dialog.ts @@ -0,0 +1,71 @@ +import { Component, inject, signal, OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { FeedsService, AdminUser, AdminUserDetail } from '../../core/services/feeds.service'; + +@Component({ + selector: 'app-user-detail-dialog', + standalone: true, + imports: [ + DatePipe, + MatDialogModule, MatButtonModule, MatChipsModule, + MatIconModule, MatProgressSpinnerModule, MatDividerModule, + ], + templateUrl: './user-detail-dialog.html', + styleUrl: './user-detail-dialog.scss', +}) +export class UserDetailDialog implements OnInit { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + private svc = inject(FeedsService); + private sb = inject(MatSnackBar); + + detail = signal(null); + loading = signal(true); + acting = signal(false); + + get initials() { + return (this.data.firstName[0] + this.data.lastName[0]).toUpperCase(); + } + + ngOnInit() { + this.svc.getUserDetail(this.data.id).subscribe({ + next: d => { this.detail.set(d); this.loading.set(false); }, + error: () => this.loading.set(false), + }); + } + + toggleBan() { + this.acting.set(true); + this.svc.toggleBan(this.data.id).subscribe({ + next: r => { + this.data.isBanned = r.isBanned; + const d = this.detail(); + if (d) this.detail.set({ ...d, isBanned: r.isBanned }); + this.acting.set(false); + this.sb.open(r.isBanned ? 'Користувача заблоковано' : 'Користувача розблоковано', '', { duration: 3000 }); + this.dialogRef.close({ action: 'ban', isBanned: r.isBanned }); + }, + error: () => { this.acting.set(false); this.sb.open('Помилка', 'ОК', { duration: 3000 }); }, + }); + } + + confirmDelete() { + if (!confirm(`Видалити користувача ${this.data.firstName} ${this.data.lastName}? Цю дію не можна скасувати.`)) return; + this.acting.set(true); + this.svc.deleteUser(this.data.id).subscribe({ + next: () => { + this.acting.set(false); + this.sb.open('Користувача видалено', '', { duration: 3000 }); + this.dialogRef.close({ action: 'delete' }); + }, + error: () => { this.acting.set(false); this.sb.open('Помилка видалення', 'ОК', { duration: 3000 }); }, + }); + } +} diff --git a/event-aggregator-client/src/environments/environment.ts b/event-aggregator-client/src/environments/environment.ts index 38f7b38..c19b176 100644 --- a/event-aggregator-client/src/environments/environment.ts +++ b/event-aggregator-client/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - apiUrl: 'http://localhost:5269/api', + apiUrl: 'https://localhost:7238/api', }; From 703df0c72eeb522f1a9b395f5cc59a862844dbe6 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Sun, 31 May 2026 20:56:18 +0300 Subject: [PATCH 06/17] Add user management, IsActive toggle, Railway deployment --- .dockerignore | 9 ++++++ Dockerfile | 28 +++++++++++++++++++ EventAggregator/Program.cs | 15 +++++++++- EventAggregator/appsettings.Production.json | 22 +++++++++++++++ event-aggregator-client/angular.json | 6 ++++ .../environments/environment.production.ts | 4 +++ start.bat | 21 ++++++++++++++ 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 EventAggregator/appsettings.Production.json create mode 100644 event-aggregator-client/src/environments/environment.production.ts create mode 100644 start.bat diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a88bc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +**/bin/ +**/obj/ +**/node_modules/ +**/.angular/ +**/dist/ +EventAggregator/events.db +EventAggregator/appsettings.json +.git/ +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5c9f23d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# ── Stage 1: Build Angular ──────────────────────────────────────────────── +FROM node:20-alpine AS angular-build +WORKDIR /client +COPY event-aggregator-client/package*.json ./ +RUN npm ci +COPY event-aggregator-client/ ./ +RUN npm run build -- --configuration production + +# ── Stage 2: Build .NET ─────────────────────────────────────────────────── +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS dotnet-build +WORKDIR /src +COPY EventAggregator/ ./ +# Copy Angular build output into wwwroot +COPY --from=angular-build /client/dist/event-aggregator-client/browser ./wwwroot +RUN dotnet publish -c Release -o /app/publish + +# ── Stage 3: Runtime ────────────────────────────────────────────────────── +FROM mcr.microsoft.com/dotnet/aspnet:9.0 +WORKDIR /app +COPY --from=dotnet-build /app/publish ./ + +# SQLite db lives in a persistent volume mounted at /data +ENV DB_PATH=/data/events.db +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_ENVIRONMENT=Production + +ENTRYPOINT ["dotnet", "EventAggregator.dll"] diff --git a/EventAggregator/Program.cs b/EventAggregator/Program.cs index ddfb3b2..55ad8f3 100644 --- a/EventAggregator/Program.cs +++ b/EventAggregator/Program.cs @@ -15,8 +15,9 @@ builder.Services.AddOpenApi(); +var dbPath = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=events.db"; builder.Services.AddDbContext(options => - options.UseSqlite("Data Source=events.db")); + options.UseSqlite(dbPath)); builder.Services.AddHttpClient(client => { @@ -84,9 +85,21 @@ } app.UseCors(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseDefaultFiles(); + app.UseStaticFiles(); +} + app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); +if (!app.Environment.IsDevelopment()) +{ + app.MapFallbackToFile("index.html"); +} + app.Run(); diff --git a/EventAggregator/appsettings.Production.json b/EventAggregator/appsettings.Production.json new file mode 100644 index 0000000..154b75b --- /dev/null +++ b/EventAggregator/appsettings.Production.json @@ -0,0 +1,22 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=/data/events.db" + }, + "Jwt": { + "Issuer": "EventAggregator", + "Audience": "EventAggregator", + "ExpiresInHours": "24" + }, + "RssPoll": { + "IntervalHours": "3" + }, + "Telegram": { + "BotToken": "" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/event-aggregator-client/angular.json b/event-aggregator-client/angular.json index 2f1a28e..28b833e 100644 --- a/event-aggregator-client/angular.json +++ b/event-aggregator-client/angular.json @@ -62,6 +62,12 @@ }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.production.ts" + } + ], "budgets": [ { "type": "initial", diff --git a/event-aggregator-client/src/environments/environment.production.ts b/event-aggregator-client/src/environments/environment.production.ts new file mode 100644 index 0000000..afab2e1 --- /dev/null +++ b/event-aggregator-client/src/environments/environment.production.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiUrl: '/api', +}; diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..8e912b4 --- /dev/null +++ b/start.bat @@ -0,0 +1,21 @@ +@echo off +title EventAggregator +echo. +echo ============================== +echo EventAggregator - Starting... +echo ============================== +echo. + +start "Backend (.NET)" cmd /k "title Backend && cd /d %~dp0EventAggregator && dotnet run" + +echo Waiting for backend to initialize... +timeout /t 5 /nobreak > nul + +start "Frontend (Angular)" cmd /k "title Angular && cd /d %~dp0event-aggregator-client && npm start" + +echo. +echo Backend: https://localhost:7238 +echo Frontend: http://localhost:4200 ^<-- open this in browser +echo. +echo Close both terminal windows to stop. +pause From f97b269160d9e0de7f41644d8d00c9512a879b9a Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Sun, 31 May 2026 21:00:10 +0300 Subject: [PATCH 07/17] Use DB_PATH env var for SQLite in production --- EventAggregator/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EventAggregator/Program.cs b/EventAggregator/Program.cs index 55ad8f3..815a86a 100644 --- a/EventAggregator/Program.cs +++ b/EventAggregator/Program.cs @@ -15,7 +15,9 @@ builder.Services.AddOpenApi(); -var dbPath = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=events.db"; +var dbPath = Environment.GetEnvironmentVariable("DB_PATH") is string envDbPath + ? $"Data Source={envDbPath}" + : builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=events.db"; builder.Services.AddDbContext(options => options.UseSqlite(dbPath)); From 33af3f8ed2e4d8f50746090d870e81bc75e8b8e2 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Sun, 31 May 2026 21:16:31 +0300 Subject: [PATCH 08/17] fix: npm ci --legacy-peer-deps for Docker build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5c9f23d..c9165ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-alpine AS angular-build WORKDIR /client COPY event-aggregator-client/package*.json ./ -RUN npm ci +RUN npm ci --legacy-peer-deps COPY event-aggregator-client/ ./ RUN npm run build -- --configuration production From e35e0644e3136918dd1b33cef69aa24898681426 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Sun, 31 May 2026 21:24:39 +0300 Subject: [PATCH 09/17] fix: create SQLite directory if not exists --- EventAggregator/Program.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EventAggregator/Program.cs b/EventAggregator/Program.cs index 815a86a..188ee3b 100644 --- a/EventAggregator/Program.cs +++ b/EventAggregator/Program.cs @@ -18,6 +18,12 @@ var dbPath = Environment.GetEnvironmentVariable("DB_PATH") is string envDbPath ? $"Data Source={envDbPath}" : builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=events.db"; + +// Ensure the SQLite database directory exists (needed for Railway volumes) +var dbFile = dbPath.Replace("Data Source=", "").Split(';')[0].Trim(); +var dbDir = Path.GetDirectoryName(dbFile); +if (!string.IsNullOrEmpty(dbDir)) + Directory.CreateDirectory(dbDir); builder.Services.AddDbContext(options => options.UseSqlite(dbPath)); From f4904e11e2af3140ef445006b8e3399635d2976d Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Mon, 1 Jun 2026 02:44:42 +0300 Subject: [PATCH 10/17] feat: admin guard, keywords, send-now buttons, calendar fix - AdminOnly policy on AdminController (adamyocardium@gmail.com only) - Angular adminGuard blocks /admin for non-admin users - UserKeywords model, migration, API endpoints and frontend UI - DigestService includes personal keywords in filtering - Send now buttons (email + telegram) in profile - Calendar tab label changed to Vidmicheni materialy Co-Authored-By: Claude Sonnet 4.5 --- .../Controllers/AdminController.cs | 2 +- .../Controllers/DigestController.cs | 24 ++ .../Controllers/KeywordsController.cs | 58 +++ EventAggregator/Data/AppDbContext.cs | 1 + ...20260531233959_AddUserKeywords.Designer.cs | 338 ++++++++++++++++++ .../20260531233959_AddUserKeywords.cs | 46 +++ .../Migrations/AppDbContextModelSnapshot.cs | 34 ++ EventAggregator/Models/User.cs | 1 + EventAggregator/Models/UserKeyword.cs | 14 + EventAggregator/Program.cs | 7 +- EventAggregator/Services/DigestService.cs | 17 +- EventAggregator/events.db | Bin 512000 -> 786432 bytes EventAggregator/events.db-shm | Bin 32768 -> 0 bytes EventAggregator/events.db-wal | Bin 103032 -> 0 bytes event-aggregator-client/src/app/app.routes.ts | 3 +- .../src/app/core/guards/admin.guard.ts | 15 + .../src/app/core/services/digest.service.ts | 17 + .../src/app/core/services/keywords.service.ts | 26 ++ .../src/app/features/profile/profile.html | 66 +++- .../src/app/features/profile/profile.ts | 74 +++- 20 files changed, 735 insertions(+), 8 deletions(-) create mode 100644 EventAggregator/Controllers/KeywordsController.cs create mode 100644 EventAggregator/Migrations/20260531233959_AddUserKeywords.Designer.cs create mode 100644 EventAggregator/Migrations/20260531233959_AddUserKeywords.cs create mode 100644 EventAggregator/Models/UserKeyword.cs delete mode 100644 EventAggregator/events.db-shm delete mode 100644 EventAggregator/events.db-wal create mode 100644 event-aggregator-client/src/app/core/guards/admin.guard.ts create mode 100644 event-aggregator-client/src/app/core/services/digest.service.ts create mode 100644 event-aggregator-client/src/app/core/services/keywords.service.ts diff --git a/EventAggregator/Controllers/AdminController.cs b/EventAggregator/Controllers/AdminController.cs index 14394de..5d309e2 100644 --- a/EventAggregator/Controllers/AdminController.cs +++ b/EventAggregator/Controllers/AdminController.cs @@ -7,7 +7,7 @@ namespace EventAggregator.API.Controllers; [ApiController] [Route("api/[controller]")] -[Authorize] +[Authorize(Policy = "AdminOnly")] public class AdminController : ControllerBase { private readonly AppDbContext _db; diff --git a/EventAggregator/Controllers/DigestController.cs b/EventAggregator/Controllers/DigestController.cs index 572b0b8..d51c1e1 100644 --- a/EventAggregator/Controllers/DigestController.cs +++ b/EventAggregator/Controllers/DigestController.cs @@ -50,6 +50,30 @@ public async Task SendToUser(int userId) } } + /// Надіслати дайджест тільки в Telegram (для поточного користувача) + [HttpPost("send-telegram/{userId:int}")] + public async Task SendTelegramToUser(int userId) + { + var user = await _db.Users + .Include(u => u.UserFilters) + .ThenInclude(uf => uf.Filter) + .FirstOrDefaultAsync(u => u.Id == userId); + + if (user is null) return NotFound(); + if (!user.TelegramChatId.HasValue) + return BadRequest(new { error = "Telegram не підключено. Спочатку надішліть /start боту." }); + + try + { + await _digestService.SendTelegramDigestAsync(user); + return Ok(new { message = "Дайджест надіслано в Telegram" }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + } + } + /// Переглянути HTML-дайджест у браузері без надсилання [HttpGet("preview/{userId:int}")] public async Task Preview(int userId) diff --git a/EventAggregator/Controllers/KeywordsController.cs b/EventAggregator/Controllers/KeywordsController.cs new file mode 100644 index 0000000..05899b5 --- /dev/null +++ b/EventAggregator/Controllers/KeywordsController.cs @@ -0,0 +1,58 @@ +using EventAggregator.API.Models; +using EventAggregator.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace EventAggregator.API.Controllers; + +[ApiController] +[Route("api/users/{userId:int}/keywords")] +[Authorize] +public class KeywordsController : ControllerBase +{ + private readonly AppDbContext _db; + public KeywordsController(AppDbContext db) => _db = db; + + [HttpGet] + public async Task GetKeywords(int userId) + { + var keywords = await _db.UserKeywords + .Where(k => k.UserId == userId) + .OrderBy(k => k.Keyword) + .Select(k => new { k.Id, k.Keyword }) + .ToListAsync(); + return Ok(keywords); + } + + [HttpPost] + public async Task AddKeyword(int userId, [FromBody] AddKeywordRequest req) + { + var keyword = req.Keyword.Trim().ToLower(); + if (string.IsNullOrWhiteSpace(keyword)) + return BadRequest(new { error = "Ключове слово не може бути порожнім" }); + + var exists = await _db.UserKeywords + .AnyAsync(k => k.UserId == userId && k.Keyword == keyword); + if (exists) + return Conflict(new { error = "Таке ключове слово вже є" }); + + var entry = new UserKeyword { UserId = userId, Keyword = keyword }; + _db.UserKeywords.Add(entry); + await _db.SaveChangesAsync(); + return Ok(new { entry.Id, entry.Keyword }); + } + + [HttpDelete("{id:int}")] + public async Task DeleteKeyword(int userId, int id) + { + var kw = await _db.UserKeywords.FirstOrDefaultAsync(k => k.Id == id && k.UserId == userId); + if (kw is null) return NotFound(); + _db.UserKeywords.Remove(kw); + await _db.SaveChangesAsync(); + return NoContent(); + } +} + +public record AddKeywordRequest(string Keyword); diff --git a/EventAggregator/Data/AppDbContext.cs b/EventAggregator/Data/AppDbContext.cs index e2814c5..6a93372 100644 --- a/EventAggregator/Data/AppDbContext.cs +++ b/EventAggregator/Data/AppDbContext.cs @@ -13,6 +13,7 @@ public AppDbContext(DbContextOptions options) : base(options) { } public DbSet UserFilters => Set(); public DbSet UserEventStatuses => Set(); public DbSet FeedSources => Set(); + public DbSet UserKeywords => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/EventAggregator/Migrations/20260531233959_AddUserKeywords.Designer.cs b/EventAggregator/Migrations/20260531233959_AddUserKeywords.Designer.cs new file mode 100644 index 0000000..b63f597 --- /dev/null +++ b/EventAggregator/Migrations/20260531233959_AddUserKeywords.Designer.cs @@ -0,0 +1,338 @@ +// +using System; +using EventAggregator.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventAggregator.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260531233959_AddUserKeywords")] + partial class AddUserKeywords + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("ContentHtml") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("FeedSourceId") + .HasColumnType("INTEGER"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("MaxPeople") + .HasColumnType("INTEGER"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.Property("SourceUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FeedSourceId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastFetched") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FeedSources"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SearchKeyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Filters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastReportSent") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasColumnType("TEXT"); + + b.Property("ReportFrequency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelegramChatId") + .HasColumnType("INTEGER"); + + b.Property("TelegramNotificationsEnabled") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IsInCalendar") + .HasColumnType("INTEGER"); + + b.Property("MarkedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("UserId"); + + b.ToTable("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FilterId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FilterId"); + + b.HasIndex("FilterId"); + + b.ToTable("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserKeywords"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.HasOne("EventAggregator.API.Models.FeedSource", "FeedSource") + .WithMany("Events") + .HasForeignKey("FeedSourceId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FeedSource"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.HasOne("EventAggregator.API.Models.Event", "Event") + .WithMany("UserEventStatuses") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserEventStatuses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.HasOne("EventAggregator.API.Models.Filter", "Filter") + .WithMany("UserFilters") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserFilters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Filter"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserKeywords") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Navigation("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Navigation("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Navigation("UserEventStatuses"); + + b.Navigation("UserFilters"); + + b.Navigation("UserKeywords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventAggregator/Migrations/20260531233959_AddUserKeywords.cs b/EventAggregator/Migrations/20260531233959_AddUserKeywords.cs new file mode 100644 index 0000000..1d7e227 --- /dev/null +++ b/EventAggregator/Migrations/20260531233959_AddUserKeywords.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventAggregator.Migrations +{ + /// + public partial class AddUserKeywords : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserKeywords", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + Keyword = table.Column(type: "TEXT", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserKeywords", x => x.Id); + table.ForeignKey( + name: "FK_UserKeywords_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserKeywords_UserId", + table: "UserKeywords", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserKeywords"); + } + } +} diff --git a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs index 5833477..9ced74f 100644 --- a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs +++ b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs @@ -226,6 +226,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UserFilters"); }); + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserKeywords"); + }); + modelBuilder.Entity("EventAggregator.API.Models.Event", b => { b.HasOne("EventAggregator.API.Models.FeedSource", "FeedSource") @@ -274,6 +295,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserKeywords") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("EventAggregator.API.Models.Event", b => { b.Navigation("UserEventStatuses"); @@ -294,6 +326,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UserEventStatuses"); b.Navigation("UserFilters"); + + b.Navigation("UserKeywords"); }); #pragma warning restore 612, 618 } diff --git a/EventAggregator/Models/User.cs b/EventAggregator/Models/User.cs index 3080718..bcdbd3a 100644 --- a/EventAggregator/Models/User.cs +++ b/EventAggregator/Models/User.cs @@ -33,4 +33,5 @@ public class User public ICollection UserFilters { get; set; } = new List(); public ICollection UserEventStatuses { get; set; } = new List(); + public ICollection UserKeywords { get; set; } = new List(); } \ No newline at end of file diff --git a/EventAggregator/Models/UserKeyword.cs b/EventAggregator/Models/UserKeyword.cs new file mode 100644 index 0000000..ea5aace --- /dev/null +++ b/EventAggregator/Models/UserKeyword.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventAggregator.API.Models; + +public class UserKeyword +{ + public int Id { get; set; } + + public int UserId { get; set; } + public User User { get; set; } = null!; + + [Required, MaxLength(100)] + public string Keyword { get; set; } = string.Empty; +} diff --git a/EventAggregator/Program.cs b/EventAggregator/Program.cs index 188ee3b..9fb21a6 100644 --- a/EventAggregator/Program.cs +++ b/EventAggregator/Program.cs @@ -68,7 +68,12 @@ }; }); -builder.Services.AddAuthorization(); +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => + policy.RequireClaim(System.Security.Claims.ClaimTypes.Email, + "adamyocardium@gmail.com")); +}); builder.Services.AddCors(options => options.AddDefaultPolicy(policy => diff --git a/EventAggregator/Services/DigestService.cs b/EventAggregator/Services/DigestService.cs index c62817e..8ce39e3 100644 --- a/EventAggregator/Services/DigestService.cs +++ b/EventAggregator/Services/DigestService.cs @@ -47,10 +47,17 @@ public async Task SendDigestAsync(User user) .Select(uf => uf.Filter.SearchKeyword.ToLower()) .ToList(); + // Додаємо персональні ключові слова користувача + var personalKeywords = await _db.UserKeywords + .Where(k => k.UserId == user.Id) + .Select(k => k.Keyword) + .ToListAsync(); + keywords.AddRange(personalKeywords); + var events = await _db.Events .Include(e => e.FeedSource) .Where(e => e.PublishedDate >= from) - .Where(e => keywords.Any(k => + .Where(e => !keywords.Any() || keywords.Any(k => e.Title.ToLower().Contains(k) || e.Description.ToLower().Contains(k))) .OrderByDescending(e => e.PublishedDate) @@ -91,10 +98,16 @@ public async Task SendTelegramDigestAsync(User user) .Select(uf => uf.Filter.SearchKeyword.ToLower()) .ToList(); + var personalKeywords = await _db.UserKeywords + .Where(k => k.UserId == user.Id) + .Select(k => k.Keyword) + .ToListAsync(); + keywords.AddRange(personalKeywords); + var events = await _db.Events .Include(e => e.FeedSource) .Where(e => e.PublishedDate >= from) - .Where(e => keywords.Any(k => + .Where(e => !keywords.Any() || keywords.Any(k => e.Title.ToLower().Contains(k) || e.Description.ToLower().Contains(k))) .OrderByDescending(e => e.PublishedDate) diff --git a/EventAggregator/events.db b/EventAggregator/events.db index e86550b26579cf93bd2db16e66d3debe6e4c7460..ce6823c158fc49109706713c288da63d0b7cb1ef 100644 GIT binary patch delta 197209 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�BN5f6hFkz@326Q{?>`ujl?Agc-i>4=& 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 WP)G587$#yy5N=AGV@5Vj7XDv^f@lB$ delta 647 zcmYjOO=uHA7@f)PHr;Hx(;wnerF2t?|J`n8b~d}QZBh;X9IExuTx;nMLQIhse?u_` zML~qhq$r5cgNO(P+bx6&B0(rUc@RYCDYw!@g9;*u2Pa6V!#oDwd*8=(YpITZ zb)p951C~QC_&NWi*58Q1V@C~QVK)|wdfT-o*bsPbVo=8=7NM)(#Mb7Cmd*Ny&o2hy z#^avX(hFwlY45}L-%4?_1rGXJvjFw0s=A~6R6|3x+fG%@BNA3Uj-4c ANB{r; diff --git a/EventAggregator/events.db-shm b/EventAggregator/events.db-shm deleted file mode 100644 index 0e08165850a260d3f3b5716b37341237662e5919..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*J4ysW5C-5{-_Lb@kJ*BWiIHIH9fZZx7?>K1rg{cXAPC;Vz>8?rqOys&W)AZe z{Pa#w&+PPffa>P$II0GjUJeI2US{r|M~@drC&M_rzv;i6KV7}vKAa6tubV%v&sUAB z-upgLpVQyNO#aIMkJ-#zW-C*3^O=RrMy9^2#>!9gU-xUQ=O>ewiU0uu1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZ;9m(;cL+tGn*zP;tg;vT(L{A;Q3SdqP@RSpfo=*^k1a)@n*xim-p!5M zHGx=)J_P~<2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0Rk-*SdP_L%QM3^^Gvbr*oobi p?!#vjAV7cs0RjYm7pOB*r>m9m`LENhwTb`%0t5&UAV8qa0v|Z)CmsL* diff --git a/EventAggregator/events.db-wal b/EventAggregator/events.db-wal deleted file mode 100644 index 7464f3a28f72644adbde1b126a27ef93750df5c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103032 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 S0b+m{AO?s5#=zmt3;Z8Qp0Moz diff --git a/event-aggregator-client/src/app/app.routes.ts b/event-aggregator-client/src/app/app.routes.ts index 5fccce2..7e88db0 100644 --- a/event-aggregator-client/src/app/app.routes.ts +++ b/event-aggregator-client/src/app/app.routes.ts @@ -1,5 +1,6 @@ import { Routes } from '@angular/router'; import { authGuard } from './core/guards/auth.guard'; +import { adminGuard } from './core/guards/admin.guard'; export const routes: Routes = [ { @@ -32,7 +33,7 @@ export const routes: Routes = [ { path: 'admin', loadComponent: () => import('./features/admin/admin').then(m => m.Admin), - canActivate: [authGuard], + canActivate: [authGuard, adminGuard], }, { path: '**', redirectTo: '' }, ]; diff --git a/event-aggregator-client/src/app/core/guards/admin.guard.ts b/event-aggregator-client/src/app/core/guards/admin.guard.ts new file mode 100644 index 0000000..12d44c2 --- /dev/null +++ b/event-aggregator-client/src/app/core/guards/admin.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +const ADMIN_EMAIL = 'adamyocardium@gmail.com'; + +export const adminGuard: CanActivateFn = () => { + const auth = inject(AuthService); + const router = inject(Router); + + if (auth.currentUser()?.email === ADMIN_EMAIL) return true; + + router.navigate(['/']); + return false; +}; diff --git a/event-aggregator-client/src/app/core/services/digest.service.ts b/event-aggregator-client/src/app/core/services/digest.service.ts new file mode 100644 index 0000000..f2a49cf --- /dev/null +++ b/event-aggregator-client/src/app/core/services/digest.service.ts @@ -0,0 +1,17 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; + +@Injectable({ providedIn: 'root' }) +export class DigestService { + private http = inject(HttpClient); + private base = `${environment.apiUrl}/digest`; + + sendNow(userId: number) { + return this.http.post<{ message: string }>(`${this.base}/send/${userId}`, {}); + } + + sendTelegramNow(userId: number) { + return this.http.post<{ message: string }>(`${this.base}/send-telegram/${userId}`, {}); + } +} diff --git a/event-aggregator-client/src/app/core/services/keywords.service.ts b/event-aggregator-client/src/app/core/services/keywords.service.ts new file mode 100644 index 0000000..3698120 --- /dev/null +++ b/event-aggregator-client/src/app/core/services/keywords.service.ts @@ -0,0 +1,26 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; + +export interface UserKeyword { + id: number; + keyword: string; +} + +@Injectable({ providedIn: 'root' }) +export class KeywordsService { + private http = inject(HttpClient); + private base = environment.apiUrl; + + getAll(userId: number) { + return this.http.get(`${this.base}/users/${userId}/keywords`); + } + + add(userId: number, keyword: string) { + return this.http.post(`${this.base}/users/${userId}/keywords`, { keyword }); + } + + delete(userId: number, id: number) { + return this.http.delete(`${this.base}/users/${userId}/keywords/${id}`); + } +} diff --git a/event-aggregator-client/src/app/features/profile/profile.html b/event-aggregator-client/src/app/features/profile/profile.html index 41aa889..9acb181 100644 --- a/event-aggregator-client/src/app/features/profile/profile.html +++ b/event-aggregator-client/src/app/features/profile/profile.html @@ -90,6 +90,32 @@ Зберегти Telegram + + + +

+ send + Надіслати дайджест зараз +
+
+ + + +
+ } @@ -143,7 +169,9 @@ tune Підписки
-

Обрані фільтри формують стрічку та email-дайджест.

+

Обрані фільтри та ключові слова формують стрічку та email-дайджест.

+ + @for (group of allGroups(); track group.type) {

{{ group.label }}

@@ -157,6 +185,40 @@
} + + + +

Власні ключові слова

+

Додайте будь-які слова — система шукатиме їх у заголовках і описах подій.

+ +
+ + Нове ключове слово + + Натисніть Enter або кнопку «+» + + +
+ + @if (userKeywords().length > 0) { + + @for (kw of userKeywords(); track kw.id) { + + {{ kw.keyword }} + + + } + + } @else { +

Ще немає ключових слів

+ }
@@ -227,7 +289,7 @@

} @else { -

event_note Усі заплановані події

+

event_note Усі відмічені матеріали

} @if (eventsForDate().length === 0) { diff --git a/event-aggregator-client/src/app/features/profile/profile.ts b/event-aggregator-client/src/app/features/profile/profile.ts index a90ca25..726276f 100644 --- a/event-aggregator-client/src/app/features/profile/profile.ts +++ b/event-aggregator-client/src/app/features/profile/profile.ts @@ -13,12 +13,15 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTabsModule } from '@angular/material/tabs'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDatepickerModule, MatCalendarCellClassFunction } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; import { QRCodeComponent } from 'angularx-qrcode'; import { AuthService } from '../../core/services/auth.service'; import { UsersService } from '../../core/services/users.service'; import { FiltersService } from '../../core/services/filters.service'; +import { DigestService } from '../../core/services/digest.service'; +import { KeywordsService, UserKeyword } from '../../core/services/keywords.service'; import { Filter, FilterGroup } from '../../core/models/filter.model'; import { EventItem } from '../../core/models/event.model'; import { User } from '../../core/models/user.model'; @@ -31,7 +34,7 @@ import { User } from '../../core/models/user.model'; MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatChipsModule, MatDividerModule, MatSlideToggleModule, MatProgressSpinnerModule, MatTabsModule, - MatDatepickerModule, MatNativeDateModule, + MatDatepickerModule, MatNativeDateModule, MatTooltipModule, ], templateUrl: './profile.html', styleUrl: './profile.scss', @@ -40,6 +43,8 @@ export class Profile implements OnInit { private auth = inject(AuthService); private usersService = inject(UsersService); private filtersService = inject(FiltersService); + private digestService = inject(DigestService); + private keywordsService = inject(KeywordsService); private snackBar = inject(MatSnackBar); user = signal(null); @@ -49,6 +54,14 @@ export class Profile implements OnInit { calendarEvents = signal([]); loading = signal(true); + // Ключові слова + userKeywords = signal([]); + newKeyword = ''; + + // Send now + sendingEmail = signal(false); + sendingTelegram = signal(false); + // Telegram tgEnabled = signal(false); tgUsername = signal(''); @@ -104,6 +117,7 @@ export class Profile implements OnInit { this.twoFactorEnabled.set(this.auth.currentUser()?.twoFactorEnabled ?? false); this.usersService.getSavedEvents(this.userId).subscribe(e => this.savedEvents.set(e)); this.usersService.getCalendarEvents(this.userId).subscribe(e => this.calendarEvents.set(e)); + this.keywordsService.getAll(this.userId).subscribe(k => this.userKeywords.set(k)); } isSubscribed(filterId: number) { return this.userFilterIds().has(filterId); } @@ -160,6 +174,64 @@ export class Profile implements OnInit { } } + // ── Ключові слова ──────────────────────────────────────────────────────── + + addKeyword() { + const kw = this.newKeyword.trim(); + if (!kw) return; + this.keywordsService.add(this.userId, kw).subscribe({ + next: k => { + this.userKeywords.update(list => [...list, k]); + this.newKeyword = ''; + this.snackBar.open(`«${k.keyword}» додано`, '', { duration: 2000 }); + }, + error: err => this.snackBar.open(err.error?.error ?? 'Помилка', 'ОК', { duration: 3000 }), + }); + } + + removeKeyword(kw: UserKeyword) { + this.keywordsService.delete(this.userId, kw.id).subscribe({ + next: () => { + this.userKeywords.update(list => list.filter(k => k.id !== kw.id)); + this.snackBar.open(`«${kw.keyword}» видалено`, '', { duration: 2000 }); + }, + }); + } + + onKeywordEnter(event: KeyboardEvent) { + if (event.key === 'Enter') this.addKeyword(); + } + + // ── Send now ───────────────────────────────────────────────────────────── + + sendEmailNow() { + this.sendingEmail.set(true); + this.digestService.sendNow(this.userId).subscribe({ + next: res => { + this.sendingEmail.set(false); + this.snackBar.open(res.message, '', { duration: 3000 }); + }, + error: err => { + this.sendingEmail.set(false); + this.snackBar.open(err.error?.error ?? 'Помилка відправки', 'ОК', { duration: 4000 }); + }, + }); + } + + sendTelegramNow() { + this.sendingTelegram.set(true); + this.digestService.sendTelegramNow(this.userId).subscribe({ + next: res => { + this.sendingTelegram.set(false); + this.snackBar.open(res.message, '', { duration: 3000 }); + }, + error: err => { + this.sendingTelegram.set(false); + this.snackBar.open(err.error?.error ?? 'Помилка відправки', 'ОК', { duration: 4000 }); + }, + }); + } + // ── 2FA ────────────────────────────────────────────────────────────────── setupTwoFactor() { From 8cc324f18d1160660b4d5cb353f9dd9169b23402 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Mon, 1 Jun 2026 03:16:04 +0300 Subject: [PATCH 11/17] fix: send-now uses 30-day window and returns proper error if no events - Added SendNowAsync method with fixed 30-day range - Controller returns 400 with clear message if no matching events found - Telegram send-now uses same logic via telegramOnly flag Co-Authored-By: Claude Sonnet 4.5 --- .../Controllers/DigestController.cs | 20 +++++--- EventAggregator/Services/DigestService.cs | 50 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/EventAggregator/Controllers/DigestController.cs b/EventAggregator/Controllers/DigestController.cs index d51c1e1..607cf95 100644 --- a/EventAggregator/Controllers/DigestController.cs +++ b/EventAggregator/Controllers/DigestController.cs @@ -28,7 +28,7 @@ public async Task SendAll() return Ok(new { message = "Дайджести оброблено" }); } - /// Примусово надіслати дайджест конкретному користувачу (для тестування) + /// Надіслати дайджест на пошту (кнопка «Надіслати зараз») [HttpPost("send/{userId:int}")] public async Task SendToUser(int userId) { @@ -41,8 +41,12 @@ public async Task SendToUser(int userId) try { - await _digestService.SendDigestAsync(user); - return Ok(new { message = $"Дайджест надіслано на {user.Email}" }); + var count = await _digestService.SendNowAsync(user, telegramOnly: false); + return Ok(new { message = $"Дайджест надіслано на {user.Email} ({count} подій)" }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); } catch (Exception ex) { @@ -50,7 +54,7 @@ public async Task SendToUser(int userId) } } - /// Надіслати дайджест тільки в Telegram (для поточного користувача) + /// Надіслати дайджест тільки в Telegram (кнопка «У Telegram») [HttpPost("send-telegram/{userId:int}")] public async Task SendTelegramToUser(int userId) { @@ -65,8 +69,12 @@ public async Task SendTelegramToUser(int userId) try { - await _digestService.SendTelegramDigestAsync(user); - return Ok(new { message = "Дайджест надіслано в Telegram" }); + var count = await _digestService.SendNowAsync(user, telegramOnly: true); + return Ok(new { message = $"Дайджест надіслано в Telegram ({count} подій)" }); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); } catch (Exception ex) { diff --git a/EventAggregator/Services/DigestService.cs b/EventAggregator/Services/DigestService.cs index 8ce39e3..265e3c0 100644 --- a/EventAggregator/Services/DigestService.cs +++ b/EventAggregator/Services/DigestService.cs @@ -21,6 +21,56 @@ public DigestService(AppDbContext db, IEmailService emailService, _logger = logger; } + /// + /// Відправка на вимогу (кнопка «Надіслати зараз»). + /// Завжди бере останні 30 днів, повертає кількість надісланих подій. + /// Кидає InvalidOperationException якщо нічого не знайдено. + /// + public async Task SendNowAsync(User user, bool telegramOnly = false) + { + var from = DateTime.UtcNow.AddDays(-30); + + var keywords = user.UserFilters + .Select(uf => uf.Filter.SearchKeyword.ToLower()) + .ToList(); + + var personalKeywords = await _db.UserKeywords + .Where(k => k.UserId == user.Id) + .Select(k => k.Keyword) + .ToListAsync(); + keywords.AddRange(personalKeywords); + + var events = await _db.Events + .Include(e => e.FeedSource) + .Where(e => e.PublishedDate >= from) + .Where(e => !keywords.Any() || keywords.Any(k => + e.Title.ToLower().Contains(k) || + e.Description.ToLower().Contains(k))) + .OrderByDescending(e => e.PublishedDate) + .Take(15) + .ToListAsync(); + + if (events.Count == 0) + throw new InvalidOperationException( + "Не знайдено жодної події за останні 30 днів за вашими підписками. " + + "Спробуйте додати більше ключових слів або категорій."); + + if (!telegramOnly) + { + var subject = "EventAggregator — Дайджест на ваш запит"; + var html = BuildHtml(user, events); + await _emailService.SendAsync(user.Email, subject, html); + } + + if (user.TelegramNotificationsEnabled && user.TelegramChatId.HasValue) + { + var tgText = BuildTelegramText(user, events); + await _telegramService.SendMessageAsync(user.TelegramChatId.Value, tgText); + } + + return events.Count; + } + public async Task SendAllDueAsync() { var now = DateTime.UtcNow; From dabfa8f0c3271d12bba4ccd3ad458917bd8d4848 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Mon, 1 Jun 2026 03:37:44 +0300 Subject: [PATCH 12/17] fix: remove event types checkboxes from feed sidebar Co-Authored-By: Claude Sonnet 4.5 --- .../src/app/features/feed/feed.html | 10 ---------- event-aggregator-client/src/app/features/feed/feed.ts | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/event-aggregator-client/src/app/features/feed/feed.html b/event-aggregator-client/src/app/features/feed/feed.html index e42f7f7..016df4d 100644 --- a/event-aggregator-client/src/app/features/feed/feed.html +++ b/event-aggregator-client/src/app/features/feed/feed.html @@ -31,17 +31,7 @@ (click)="filterByCategory('science')">🔬 Наука - - @for (group of filterGroups(); track group.type) { -
-

{{ group.label }}

- @for (filter of group.filters; track filter.id) { - {{ filter.name }} - } -
- - } diff --git a/event-aggregator-client/src/app/features/feed/feed.ts b/event-aggregator-client/src/app/features/feed/feed.ts index 8f3bfc9..0127e13 100644 --- a/event-aggregator-client/src/app/features/feed/feed.ts +++ b/event-aggregator-client/src/app/features/feed/feed.ts @@ -2,7 +2,6 @@ import { Component, inject, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatListModule } from '@angular/material/list'; -import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -24,7 +23,7 @@ import { EventCard } from '../../shared/components/event-card/event-card'; standalone: true, imports: [ FormsModule, - MatSidenavModule, MatListModule, MatCheckboxModule, + MatSidenavModule, MatListModule, MatButtonModule, MatIconModule, MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, MatPaginatorModule, MatDividerModule, From 131c596d5d45abb5d6839a09a7d89cb9205de26c Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Mon, 1 Jun 2026 03:46:37 +0300 Subject: [PATCH 13/17] fix: save session token after successful login --- .../src/app/core/services/auth.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/event-aggregator-client/src/app/core/services/auth.service.ts b/event-aggregator-client/src/app/core/services/auth.service.ts index 2b79db4..030ec1a 100644 --- a/event-aggregator-client/src/app/core/services/auth.service.ts +++ b/event-aggregator-client/src/app/core/services/auth.service.ts @@ -26,7 +26,13 @@ export class AuthService { } login(req: LoginRequest) { - return this.http.post(`${this.base}/login`, req); + return this.http.post(`${this.base}/login`, req).pipe( + tap(res => { + if (!('requiresTwoFactor' in res)) { + this.saveSession(res as AuthResponse); + } + }) + ); } verifyTwoFactor(userId: number, code: string) { From 99cd29e713de082c2b730cd5a219f8ab84c5b6e8 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Wed, 3 Jun 2026 17:13:33 +0300 Subject: [PATCH 14/17] fix: admin guard fetches user from API if not in memory; show 2FA setup errors --- .../src/app/core/guards/admin.guard.ts | 23 ++++++++++++++++--- .../src/app/features/profile/profile.ts | 8 ++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/event-aggregator-client/src/app/core/guards/admin.guard.ts b/event-aggregator-client/src/app/core/guards/admin.guard.ts index 12d44c2..bd885a0 100644 --- a/event-aggregator-client/src/app/core/guards/admin.guard.ts +++ b/event-aggregator-client/src/app/core/guards/admin.guard.ts @@ -1,5 +1,6 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; +import { map, catchError, of } from 'rxjs'; import { AuthService } from '../services/auth.service'; const ADMIN_EMAIL = 'adamyocardium@gmail.com'; @@ -8,8 +9,24 @@ export const adminGuard: CanActivateFn = () => { const auth = inject(AuthService); const router = inject(Router); - if (auth.currentUser()?.email === ADMIN_EMAIL) return true; + const allow = (email: string | undefined) => { + if (email === ADMIN_EMAIL) return true; + router.navigate(['/']); + return false; + }; - router.navigate(['/']); - return false; + // Якщо юзер вже є в пам'яті — перевіряємо одразу + if (auth.currentUser()) { + return allow(auth.currentUser()!.email); + } + + // Токен є, але currentUser порожній (наприклад, localStorage частково очищено) + // → підтягуємо юзера з API і тоді перевіряємо + return auth.me().pipe( + map(user => allow(user.email)), + catchError(() => { + router.navigate(['/']); + return of(false); + }) + ); }; diff --git a/event-aggregator-client/src/app/features/profile/profile.ts b/event-aggregator-client/src/app/features/profile/profile.ts index 726276f..3b5e23c 100644 --- a/event-aggregator-client/src/app/features/profile/profile.ts +++ b/event-aggregator-client/src/app/features/profile/profile.ts @@ -238,7 +238,13 @@ export class Profile implements OnInit { this.twoFaLoading.set(true); this.auth.setupTwoFactor().subscribe({ next: res => { this.qrUri.set(res.otpauthUri); this.twoFaLoading.set(false); }, - error: () => this.twoFaLoading.set(false), + error: err => { + this.twoFaLoading.set(false); + const msg = err.status === 401 + ? 'Сесія закінчилась — увійдіть знову' + : (err.error?.message ?? 'Помилка налаштування 2FA'); + this.snackBar.open(msg, 'ОК', { duration: 4000 }); + }, }); } From 5d3b0857f36a3d01e7125c85bfa9a2b165bfba1f Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Wed, 3 Jun 2026 19:25:10 +0300 Subject: [PATCH 15/17] feat: add language/category preferences and send-now to subscriptions tab --- .../Controllers/UsersController.cs | 14 +- ...60603162212_AddUserPreferences.Designer.cs | 344 ++++++++++++++++++ .../20260603162212_AddUserPreferences.cs | 38 ++ .../Migrations/AppDbContextModelSnapshot.cs | 6 + EventAggregator/Models/User.cs | 6 + EventAggregator/Services/DigestService.cs | 7 + .../src/app/core/models/user.model.ts | 4 + .../src/app/features/profile/profile.html | 55 +++ .../src/app/features/profile/profile.scss | 40 ++ .../src/app/features/profile/profile.ts | 37 ++ 10 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 EventAggregator/Migrations/20260603162212_AddUserPreferences.Designer.cs create mode 100644 EventAggregator/Migrations/20260603162212_AddUserPreferences.cs diff --git a/EventAggregator/Controllers/UsersController.cs b/EventAggregator/Controllers/UsersController.cs index 5f837b8..ba8d015 100644 --- a/EventAggregator/Controllers/UsersController.cs +++ b/EventAggregator/Controllers/UsersController.cs @@ -57,10 +57,12 @@ public async Task Update(int id, UpdateUserRequest req) var user = await _db.Users.FindAsync(id); if (user is null) return NotFound(); - user.FirstName = req.FirstName ?? user.FirstName; - user.LastName = req.LastName ?? user.LastName; - user.PhotoUrl = req.PhotoUrl ?? user.PhotoUrl; - user.ReportFrequency = req.ReportFrequency ?? user.ReportFrequency; + user.FirstName = req.FirstName ?? user.FirstName; + user.LastName = req.LastName ?? user.LastName; + user.PhotoUrl = req.PhotoUrl ?? user.PhotoUrl; + user.ReportFrequency = req.ReportFrequency ?? user.ReportFrequency; + user.PreferredLanguage = req.PreferredLanguage ?? user.PreferredLanguage; + user.PreferredCategories = req.PreferredCategories ?? user.PreferredCategories; await _db.SaveChangesAsync(); return NoContent(); @@ -298,11 +300,13 @@ public async Task UpdateTelegram(int id, UpdateTelegramRequest re u.TelegramNotificationsEnabled, u.TelegramUsername, u.TelegramChatId, + u.PreferredLanguage, + u.PreferredCategories, Filters = filters.Select(f => new { f.Id, f.Name, f.Type }) }; } public record CreateUserRequest(string FirstName, string LastName, string Email, string? ReportFrequency); -public record UpdateUserRequest(string? FirstName, string? LastName, string? PhotoUrl, string? ReportFrequency); +public record UpdateUserRequest(string? FirstName, string? LastName, string? PhotoUrl, string? ReportFrequency, string? PreferredLanguage, string? PreferredCategories); public record UpdateTelegramRequest(bool TelegramNotificationsEnabled, string? TelegramUsername); public record SetEventStatusRequest(string? Status, bool IsInCalendar); diff --git a/EventAggregator/Migrations/20260603162212_AddUserPreferences.Designer.cs b/EventAggregator/Migrations/20260603162212_AddUserPreferences.Designer.cs new file mode 100644 index 0000000..60e2b52 --- /dev/null +++ b/EventAggregator/Migrations/20260603162212_AddUserPreferences.Designer.cs @@ -0,0 +1,344 @@ +// +using System; +using EventAggregator.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventAggregator.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260603162212_AddUserPreferences")] + partial class AddUserPreferences + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("ContentHtml") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventDate") + .HasColumnType("TEXT"); + + b.Property("FeedSourceId") + .HasColumnType("INTEGER"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("MaxPeople") + .HasColumnType("INTEGER"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.Property("SourceUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FeedSourceId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastFetched") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FeedSources"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SearchKeyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Filters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastReportSent") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasColumnType("TEXT"); + + b.Property("PreferredCategories") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguage") + .HasColumnType("TEXT"); + + b.Property("ReportFrequency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelegramChatId") + .HasColumnType("INTEGER"); + + b.Property("TelegramNotificationsEnabled") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IsInCalendar") + .HasColumnType("INTEGER"); + + b.Property("MarkedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("UserId"); + + b.ToTable("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FilterId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FilterId"); + + b.HasIndex("FilterId"); + + b.ToTable("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserKeywords"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.HasOne("EventAggregator.API.Models.FeedSource", "FeedSource") + .WithMany("Events") + .HasForeignKey("FeedSourceId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FeedSource"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserEventStatus", b => + { + b.HasOne("EventAggregator.API.Models.Event", "Event") + .WithMany("UserEventStatuses") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserEventStatuses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserFilter", b => + { + b.HasOne("EventAggregator.API.Models.Filter", "Filter") + .WithMany("UserFilters") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserFilters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Filter"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.UserKeyword", b => + { + b.HasOne("EventAggregator.API.Models.User", "User") + .WithMany("UserKeywords") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Event", b => + { + b.Navigation("UserEventStatuses"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.FeedSource", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.Filter", b => + { + b.Navigation("UserFilters"); + }); + + modelBuilder.Entity("EventAggregator.API.Models.User", b => + { + b.Navigation("UserEventStatuses"); + + b.Navigation("UserFilters"); + + b.Navigation("UserKeywords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventAggregator/Migrations/20260603162212_AddUserPreferences.cs b/EventAggregator/Migrations/20260603162212_AddUserPreferences.cs new file mode 100644 index 0000000..f3960b3 --- /dev/null +++ b/EventAggregator/Migrations/20260603162212_AddUserPreferences.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventAggregator.Migrations +{ + /// + public partial class AddUserPreferences : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PreferredCategories", + table: "Users", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "PreferredLanguage", + table: "Users", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PreferredCategories", + table: "Users"); + + migrationBuilder.DropColumn( + name: "PreferredLanguage", + table: "Users"); + } + } +} diff --git a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs index 9ced74f..67a8f9c 100644 --- a/EventAggregator/Migrations/AppDbContextModelSnapshot.cs +++ b/EventAggregator/Migrations/AppDbContextModelSnapshot.cs @@ -156,6 +156,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PhotoUrl") .HasColumnType("TEXT"); + b.Property("PreferredCategories") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguage") + .HasColumnType("TEXT"); + b.Property("ReportFrequency") .IsRequired() .HasColumnType("TEXT"); diff --git a/EventAggregator/Models/User.cs b/EventAggregator/Models/User.cs index bcdbd3a..cfc63ef 100644 --- a/EventAggregator/Models/User.cs +++ b/EventAggregator/Models/User.cs @@ -31,6 +31,12 @@ public class User public string? TelegramUsername { get; set; } // введено в профілі (без @) public long? TelegramChatId { get; set; } // встановлюється після /start + // ── Preferences ────────────────────────────────────────────────────────── + /// "uk", "en" or null/empty = both languages + public string? PreferredLanguage { get; set; } + /// Comma-separated: "events,news,tech,science" or null/empty = all + public string? PreferredCategories { get; set; } + public ICollection UserFilters { get; set; } = new List(); public ICollection UserEventStatuses { get; set; } = new List(); public ICollection UserKeywords { get; set; } = new List(); diff --git a/EventAggregator/Services/DigestService.cs b/EventAggregator/Services/DigestService.cs index 265e3c0..237e535 100644 --- a/EventAggregator/Services/DigestService.cs +++ b/EventAggregator/Services/DigestService.cs @@ -40,12 +40,19 @@ public async Task SendNowAsync(User user, bool telegramOnly = false) .ToListAsync(); keywords.AddRange(personalKeywords); + var prefLang = string.IsNullOrWhiteSpace(user.PreferredLanguage) ? null : user.PreferredLanguage; + var prefCats = string.IsNullOrWhiteSpace(user.PreferredCategories) + ? null + : user.PreferredCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); + var events = await _db.Events .Include(e => e.FeedSource) .Where(e => e.PublishedDate >= from) .Where(e => !keywords.Any() || keywords.Any(k => e.Title.ToLower().Contains(k) || e.Description.ToLower().Contains(k))) + .Where(e => prefLang == null || e.FeedSource!.Language == prefLang) + .Where(e => prefCats == null || prefCats.Contains(e.Category ?? "")) .OrderByDescending(e => e.PublishedDate) .Take(15) .ToListAsync(); diff --git a/event-aggregator-client/src/app/core/models/user.model.ts b/event-aggregator-client/src/app/core/models/user.model.ts index 7d914f2..38ca82b 100644 --- a/event-aggregator-client/src/app/core/models/user.model.ts +++ b/event-aggregator-client/src/app/core/models/user.model.ts @@ -12,6 +12,8 @@ export interface User { telegramNotificationsEnabled: boolean; telegramUsername?: string; telegramChatId?: number; + preferredLanguage?: string; + preferredCategories?: string; filters: Filter[]; } @@ -27,6 +29,8 @@ export interface UpdateUserRequest { lastName?: string; photoUrl?: string; reportFrequency?: string; + preferredLanguage?: string; + preferredCategories?: string; } export interface UpdateTelegramRequest { diff --git a/event-aggregator-client/src/app/features/profile/profile.html b/event-aggregator-client/src/app/features/profile/profile.html index 9acb181..29db121 100644 --- a/event-aggregator-client/src/app/features/profile/profile.html +++ b/event-aggregator-client/src/app/features/profile/profile.html @@ -171,7 +171,62 @@

Обрані фільтри та ключові слова формують стрічку та email-дайджест.

+ +

Мова

+
+ + +
+

Якщо нічого не обрано — показуються всі мови.

+ + + +

Категорія

+
+ @for (cat of CATEGORIES; track cat.value) { + + } +
+

Якщо нічого не обрано — показуються всі категорії.

+ + + + +

Надіслати дайджест зараз

+
+ + +
+ + + + @for (group of allGroups(); track group.type) {

{{ group.label }}

diff --git a/event-aggregator-client/src/app/features/profile/profile.scss b/event-aggregator-client/src/app/features/profile/profile.scss index 2e70d72..bd5572e 100644 --- a/event-aggregator-client/src/app/features/profile/profile.scss +++ b/event-aggregator-client/src/app/features/profile/profile.scss @@ -74,6 +74,46 @@ input { letter-spacing: 0.25em; font-size: 1.1rem; text-align: center; } } +// ── Мова/Категорія кнопки ───────────────────────────────────────────────── + +.group-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #888; + margin: 0 0 10px; +} + +.lang-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; + + button { + border-radius: 20px; + min-width: 100px; + transition: background 0.15s, color 0.15s; + + &.active { + background-color: #e8f0fe; + color: #1a73e8; + border-color: #1a73e8; + font-weight: 500; + } + } +} + +.send-now-block { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 4px; +} + +.hint.muted { color: #bbb; } + // ── Підписки ────────────────────────────────────────────────────────────── .filter-group { diff --git a/event-aggregator-client/src/app/features/profile/profile.ts b/event-aggregator-client/src/app/features/profile/profile.ts index 3b5e23c..d3a6cac 100644 --- a/event-aggregator-client/src/app/features/profile/profile.ts +++ b/event-aggregator-client/src/app/features/profile/profile.ts @@ -54,6 +54,17 @@ export class Profile implements OnInit { calendarEvents = signal([]); loading = signal(true); + // Мова / категорії + preferredLanguage = signal(''); + preferredCategories = signal>(new Set()); + + readonly CATEGORIES = [ + { value: 'events', label: '📅 Події' }, + { value: 'news', label: '📰 Новини' }, + { value: 'tech', label: '💻 IT/Tech' }, + { value: 'science', label: '🔬 Наука' }, + ]; + // Ключові слова userKeywords = signal([]); newKeyword = ''; @@ -109,6 +120,10 @@ export class Profile implements OnInit { this.tgEnabled.set(u.telegramNotificationsEnabled); this.tgUsername.set(u.telegramUsername ?? ''); this.tgChatId.set(u.telegramChatId ?? null); + this.preferredLanguage.set(u.preferredLanguage ?? ''); + this.preferredCategories.set( + new Set((u.preferredCategories ?? '').split(',').filter(Boolean)) + ); this.loading.set(false); }, error: () => this.loading.set(false), @@ -137,6 +152,28 @@ export class Profile implements OnInit { } } + toggleCategory(cat: string) { + this.preferredCategories.update(s => { + const n = new Set(s); + n.has(cat) ? n.delete(cat) : n.add(cat); + return n; + }); + this.savePreferences(); + } + + setLanguage(lang: string) { + this.preferredLanguage.set(this.preferredLanguage() === lang ? '' : lang); + this.savePreferences(); + } + + savePreferences() { + const cats = [...this.preferredCategories()].join(','); + this.usersService.update(this.userId, { + preferredLanguage: this.preferredLanguage() || undefined, + preferredCategories: cats || undefined, + }).subscribe(); + } + saveProfile() { const u = this.user(); if (!u) return; From 6de0a959a545d50fdeab5dc457f35832552c1a4a Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Wed, 3 Jun 2026 19:52:47 +0300 Subject: [PATCH 16/17] fix: remove event types from subscriptions tab; fix digest category over-filtering --- EventAggregator/Services/DigestService.cs | 4 ---- .../src/app/features/profile/profile.html | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/EventAggregator/Services/DigestService.cs b/EventAggregator/Services/DigestService.cs index 237e535..76da603 100644 --- a/EventAggregator/Services/DigestService.cs +++ b/EventAggregator/Services/DigestService.cs @@ -41,9 +41,6 @@ public async Task SendNowAsync(User user, bool telegramOnly = false) keywords.AddRange(personalKeywords); var prefLang = string.IsNullOrWhiteSpace(user.PreferredLanguage) ? null : user.PreferredLanguage; - var prefCats = string.IsNullOrWhiteSpace(user.PreferredCategories) - ? null - : user.PreferredCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); var events = await _db.Events .Include(e => e.FeedSource) @@ -52,7 +49,6 @@ public async Task SendNowAsync(User user, bool telegramOnly = false) e.Title.ToLower().Contains(k) || e.Description.ToLower().Contains(k))) .Where(e => prefLang == null || e.FeedSource!.Language == prefLang) - .Where(e => prefCats == null || prefCats.Contains(e.Category ?? "")) .OrderByDescending(e => e.PublishedDate) .Take(15) .ToListAsync(); diff --git a/event-aggregator-client/src/app/features/profile/profile.html b/event-aggregator-client/src/app/features/profile/profile.html index 29db121..2f21475 100644 --- a/event-aggregator-client/src/app/features/profile/profile.html +++ b/event-aggregator-client/src/app/features/profile/profile.html @@ -226,21 +226,6 @@ - - @for (group of allGroups(); track group.type) { -
-

{{ group.label }}

- - @for (filter of group.filters; track filter.id) { - - @if (isSubscribed(filter.id)) { check } - {{ filter.name }} - - } - -
- } -

Власні ключові слова

From c9a7533f46fb8c1a769dd90bac2773ed5456fac2 Mon Sep 17 00:00:00 2001 From: Yevginiia Date: Thu, 4 Jun 2026 00:46:22 +0300 Subject: [PATCH 17/17] chore: trigger redeploy