Skip to content

Commit 3f63e7e

Browse files
committed
Fully Implement Notification Page
1 parent 926dcde commit 3f63e7e

File tree

20 files changed

+474
-151
lines changed

20 files changed

+474
-151
lines changed

backend/NXTBackend.API.Core/Services/Implementation/NotificationService.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// See README in the root project for more information.
44
// ============================================================================
55

6-
using System.Text.Json;
76
using Microsoft.AspNetCore.Http;
87
using Microsoft.EntityFrameworkCore;
98
using NXTBackend.API.Core.Services.Interface;
@@ -12,7 +11,6 @@
1211
using NXTBackend.API.Domain.Entities;
1312
using NXTBackend.API.Domain.Enums;
1413
using NXTBackend.API.Infrastructure.Database;
15-
using NXTBackend.API.Models;
1614

1715
namespace NXTBackend.API.Core.Services.Implementation;
1816

@@ -24,27 +22,36 @@ public NotificationService(DatabaseContext ctx) : base(ctx)
2422
{
2523
DefineFilter<bool>("read", (q, read) => q.Where(n => read ? n.ReadAt != null : n.ReadAt == null));
2624
DefineFilter<Guid>("user_id", (q, userId) => q.Where(n => n.NotifiableId == userId));
27-
DefineFilter<NotificationState>("state", (q, state) => q.Where(n => n.State == state));
28-
DefineFilter<NotificationKind>("kind", (q, kind) => q.Where(n => (n.Descriptor & kind) != 0));
29-
DefineFilter<NotificationKind>("not[kind]", (q, kind) => q.Where(n => (n.Descriptor & kind) == 0));
25+
DefineFilter<NotificationKind>("kind", (q, kind) =>
26+
kind == NotificationKind.None
27+
? q // Skip the filter
28+
: q.Where(n => (n.Descriptor & kind) != 0));
29+
30+
DefineFilter<NotificationKind>("not[kind]", (q, kind) =>
31+
kind == NotificationKind.None
32+
? q // Skip the filter
33+
: q.Where(n => (n.Descriptor & kind) == 0));
3034
}
3135

32-
public override async Task<PaginatedList<Notification>> GetAllAsync(PaginationParams pagination, SortingParams sorting, FilterDictionary? filters = null)
36+
public override async Task<PaginatedList<Notification>> GetAllAsync(PaginationParams pagination,
37+
SortingParams sorting, FilterDictionary? filters = null)
3338
{
3439
var query = ApplyFilters(_dbSet.AsQueryable(), filters);
3540
query = SortedList<Notification>.Apply(query, sorting);
3641
return await PaginatedList<Notification>.CreateAsync(query, pagination.Page, pagination.Size);
3742
}
3843

39-
/// <inheritdoc/>
44+
/// <inheritdoc />
4045
public async Task MarkAsReadAsync(Guid userId, IEnumerable<Guid>? notificationIds = null)
4146
{
4247
if (notificationIds is not null && notificationIds.Any())
4348
{
4449
var idList = notificationIds.ToList();
45-
bool hasUnauthorizedNotifications = await _context.Notifications.AnyAsync(n => idList.Contains(n.Id) && n.NotifiableId != userId);
50+
var hasUnauthorizedNotifications =
51+
await _context.Notifications.AnyAsync(n => idList.Contains(n.Id) && n.NotifiableId != userId);
4652
if (hasUnauthorizedNotifications)
47-
throw new ServiceException(StatusCodes.Status403Forbidden, "One or more notification IDs do not belong to the current user");
53+
throw new ServiceException(StatusCodes.Status403Forbidden,
54+
"One or more notification IDs do not belong to the current user");
4855
}
4956

5057
var updateQuery = _context.Notifications.Where(un => un.NotifiableId == userId);
@@ -53,17 +60,18 @@ public async Task MarkAsReadAsync(Guid userId, IEnumerable<Guid>? notificationId
5360
await updateQuery.ExecuteUpdateAsync(s => s.SetProperty(un => un.ReadAt, DateTimeOffset.UtcNow));
5461
}
5562

56-
/// <inheritdoc/>
63+
/// <inheritdoc />
5764
public async Task MarkAsUnreadAsync(Guid userId, IEnumerable<Guid>? notificationIds = null)
5865
{
5966
if (notificationIds is not null && notificationIds.Any())
6067
{
6168
var idList = notificationIds.ToList();
62-
bool hasUnauthorizedNotifications = await _context.Notifications
69+
var hasUnauthorizedNotifications = await _context.Notifications
6370
.AnyAsync(n => idList.Contains(n.Id) && n.NotifiableId != userId);
6471

6572
if (hasUnauthorizedNotifications)
66-
throw new ServiceException(StatusCodes.Status403Forbidden, "One or more notification IDs do not belong to the current user");
73+
throw new ServiceException(StatusCodes.Status403Forbidden,
74+
"One or more notification IDs do not belong to the current user");
6775
}
6876

6977
var updateQuery = _context.Notifications.Where(un => un.NotifiableId == userId);

backend/NXTBackend.API.Domain/Enums/NotificationKind.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ namespace NXTBackend.API.Domain.Enums;
1515
[Flags]
1616
public enum NotificationKind
1717
{
18+
/// <summary>
19+
/// No specific notification kind, acts as a wildcard/ignore
20+
/// </summary>
21+
[JsonPropertyName(nameof(None))] None = 0,
22+
1823
/// <summary>
1924
/// Notification requires user to accept or decline.
2025
/// </summary>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// ============================================================================
2+
// Copyright (c) 2025 - W2Wizard.
3+
// See README.md in the project root for license information.
4+
// ============================================================================
5+
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Mvc;
8+
using NXTBackend.API.Models.Responses.Objects;
9+
10+
namespace NXTBackend.API.Controllers;
11+
12+
[ApiController]
13+
[Tags("Hooks")]
14+
[Route("webhooks/git")]
15+
[AllowAnonymous]
16+
#if DEBUG
17+
[ApiExplorerSettings(IgnoreApi = false)]
18+
#else
19+
[ApiExplorerSettings(IgnoreApi = true)]
20+
#endif
21+
public class GitHookController(ILogger<NotificationController> logger) : Controller
22+
{
23+
[HttpPost("/webhooks/git")]
24+
[EndpointSummary("Create a notification")]
25+
[ProducesResponseType(StatusCodes.Status200OK)]
26+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
27+
public async Task<ActionResult<NotificationDO>> OnRepoCreate()
28+
{
29+
Request.Headers.TryGetValue("X-Gitea-Event", out var eventType);
30+
Request.Headers.TryGetValue("X-Gitea-Signature", out var signature);
31+
32+
if (string.IsNullOrEmpty(eventType))
33+
return BadRequest(new ProblemDetails
34+
{
35+
Title = "Missing Event Type",
36+
Detail = "X-Gitea-Event header is required."
37+
});
38+
39+
// Read the request body to get the webhook payload
40+
using var reader = new StreamReader(Request.Body);
41+
var payload = await reader.ReadToEndAsync();
42+
43+
if (string.IsNullOrEmpty(payload))
44+
return BadRequest(new ProblemDetails
45+
{
46+
Title = "Empty Payload",
47+
Detail = "Webhook payload cannot be empty."
48+
});
49+
50+
// Log the received event
51+
logger.LogInformation("Received Gitea webhook event: {EventType}", eventType);
52+
53+
// TODO: Verify webhook signature if webhook secret is configured
54+
// TODO: Process the webhook payload based on event type
55+
56+
return Ok();
57+
}
58+
}

backend/NXTBackend.API/Controllers/UserController.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Microsoft.AspNetCore.Authorization;
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.AspNetCore.OutputCaching;
10-
using Microsoft.EntityFrameworkCore;
1110
using NXTBackend.API.Core.Services.Interface;
1211
using NXTBackend.API.Core.Utils;
1312
using NXTBackend.API.Core.Utils.Query;
@@ -94,12 +93,17 @@ public async Task<ActionResult<IEnumerable<NotificationDO>>> GetNotifications(
9493
INotificationService notificationService,
9594
[FromQuery] PaginationParams paging,
9695
[FromQuery] SortingParams sorting,
97-
[FromQuery(Name = "filter[state]")] NotificationState? state
96+
[FromQuery(Name = "filter[read]")] bool? read,
97+
[FromQuery(Name = "filter[not[kind]]")]
98+
NotificationKind? notKind,
99+
[FromQuery(Name = "filter[kind]")] NotificationKind? kind
98100
)
99101
{
100102
var filters = new FilterDictionary()
101103
.AddFilter("user_id", User.GetSID())
102-
.AddFilter("state", state);
104+
.AddFilter("kind", kind)
105+
.AddFilter("not[kind]", notKind)
106+
.AddFilter("read", read);
103107

104108
var page = await notificationService.GetAllAsync(paging, sorting, filters);
105109
page.AppendHeaders(Response.Headers);

backend/NXTBackend.API/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public static void RegisterServices(WebApplicationBuilder builder)
190190
PermitLimit = 1680, // More requests
191191
Window = TimeSpan.FromMinutes(20),
192192
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
193-
QueueLimit = 5
193+
QueueLimit = 10
194194
});
195195
});
196196
});

frontend/bun.lockb

1.06 KB
Binary file not shown.

frontend/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,35 @@
1414
"keycloak:types": "openapi-typescript https://www.keycloak.org/docs-api/26.0.0/rest-api/openapi.json -o ./src/lib/api/keycloak.d.ts"
1515
},
1616
"devDependencies": {
17-
"@shikijs/rehype": "^3.5.0",
17+
"@shikijs/rehype": "^3.6.0",
1818
"@sveltejs/adapter-node": "^5.2.12",
1919
"@sveltejs/enhanced-img": "^0.6.0",
20-
"@sveltejs/kit": "^2.21.2",
20+
"@sveltejs/kit": "^2.21.3",
2121
"@sveltejs/vite-plugin-svelte": "^5.1.0",
2222
"@tailwindcss/vite": "^4.1.8",
2323
"@types/bun": "^1.2.15",
2424
"@types/eslint": "^9.6.1",
2525
"autoprefixer": "^10.4.21",
26-
"bits-ui": "^2.4.1",
26+
"bits-ui": "^2.5.0",
2727
"clsx": "^2.1.1",
2828
"eslint": "^9.28.0",
2929
"eslint-config-prettier": "^10.1.5",
30-
"eslint-plugin-svelte": "^3.9.1",
30+
"eslint-plugin-svelte": "^3.9.2",
3131
"globals": "^16.2.0",
3232
"mode-watcher": "^1.0.7",
3333
"paneforge": "^0.0.6",
3434
"prettier": "^3.5.3",
3535
"prettier-plugin-svelte": "^3.4.0",
3636
"prettier-plugin-tailwindcss": "^0.6.12",
37-
"shiki": "^3.5.0",
37+
"shiki": "^3.6.0",
3838
"sonda": "^0.7.1",
39-
"svelte": "^5.33.14",
39+
"svelte": "^5.33.18",
4040
"svelte-check": "^4.2.1",
4141
"tailwind-merge": "^3.3.0",
4242
"tailwind-variants": "^1.0.0",
4343
"tailwindcss": "^4.1.8",
4444
"tw-animate-css": "^1.3.4",
45-
"typescript-eslint": "^8.33.1",
45+
"typescript-eslint": "^8.34.0",
4646
"vite": "^6.3.5",
4747
"vite-plugin-mkcert": "^1.17.8"
4848
},
@@ -58,14 +58,14 @@
5858
"lucide-svelte": "^0.513.0",
5959
"openapi-fetch": "^0.14.0",
6060
"openapi-typescript": "^7.8.0",
61-
"postprocessing": "^6.37.3",
61+
"postprocessing": "^6.37.4",
6262
"rehype-katex": "^7.0.1",
6363
"remark-collapse": "^0.1.2",
6464
"remark-math": "^6.0.0",
6565
"svelte-exmarkdown": "^5.0.1",
6666
"three": "^0.177.0",
6767
"winston": "^3.17.0",
68-
"zod": "^3.25.51",
68+
"zod": "^3.25.57",
6969
"svelte-sonner": "^1.0.4",
7070
"winston-console-format": "^1.0.8"
7171
}

frontend/src/lib/api/types.d.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,83 @@ export interface paths {
13221322
put?: never;
13231323
post?: never;
13241324
delete?: never;
1325+
options?: never;
1326+
head?: never;
1327+
patch?: never;
1328+
trace?: never;
1329+
};
1330+
"/webhooks/git": {
1331+
parameters: {
1332+
query?: never;
1333+
header?: never;
1334+
path?: never;
1335+
cookie?: never;
1336+
};
1337+
get?: never;
1338+
put?: never;
1339+
/** Create a notification */
1340+
post: {
1341+
parameters: {
1342+
query?: never;
1343+
header?: never;
1344+
path?: never;
1345+
cookie?: never;
1346+
};
1347+
requestBody?: never;
1348+
responses: {
1349+
/** @description OK */
1350+
200: {
1351+
headers: {
1352+
[name: string]: unknown;
1353+
};
1354+
content: {
1355+
"text/plain": components["schemas"]["NotificationDO"];
1356+
"application/json": components["schemas"]["NotificationDO"];
1357+
"text/json": components["schemas"]["NotificationDO"];
1358+
};
1359+
};
1360+
/** @description Bad Request */
1361+
400: {
1362+
headers: {
1363+
[name: string]: unknown;
1364+
};
1365+
content: {
1366+
"text/plain": components["schemas"]["ProblemDetails"];
1367+
"application/json": components["schemas"]["ProblemDetails"];
1368+
"text/json": components["schemas"]["ProblemDetails"];
1369+
};
1370+
};
1371+
/** @description Unauthorized */
1372+
401: {
1373+
headers: {
1374+
[name: string]: unknown;
1375+
};
1376+
content?: never;
1377+
};
1378+
/** @description Forbidden */
1379+
403: {
1380+
headers: {
1381+
[name: string]: unknown;
1382+
};
1383+
content?: never;
1384+
};
1385+
/** @description Not Found */
1386+
404: {
1387+
headers: {
1388+
[name: string]: unknown;
1389+
};
1390+
content?: never;
1391+
};
1392+
/** @description Too Many Requests */
1393+
429: {
1394+
headers: {
1395+
[name: string]: unknown;
1396+
};
1397+
content?: never;
1398+
};
1399+
};
1400+
};
1401+
delete?: never;
13251402
options?: never;
13261403
head?: never;
13271404
patch?: never;
@@ -3782,7 +3859,9 @@ export interface paths {
37823859
"page[size]"?: number;
37833860
sort_by?: string;
37843861
sort?: components["schemas"]["Order"];
3785-
"filter[state]"?: string;
3862+
"filter[read]"?: boolean;
3863+
"filter[not[kind]]"?: number;
3864+
"filter[kind]"?: number;
37863865
};
37873866
header?: never;
37883867
path?: never;

frontend/src/lib/components/base.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script lang="ts">
2-
import type { Snippet } from "svelte";
3-
import * as Resizable from "$lib/components/ui/resizable";
2+
import type {Snippet} from "svelte";
43
54
interface Props {
65
left: Snippet;
@@ -16,7 +15,7 @@
1615
<div
1716
class="grid grid-rows-[auto_1fr] lg:grid-cols-[minmax(200px,300px)_1fr] lg:grid-rows-1"
1817
>
19-
<aside class="dark:bg-card sticky top-0 flex flex-col gap-2 p-4 lg:h-dvh lg:border-r">
18+
<aside class="dark:bg-card sticky top-0 flex flex-col gap-2 p-4 lg:h-dvh lg:border-r z-[49]">
2019
{@render left()}
2120
</aside>
2221

frontend/src/lib/components/cards/task-card.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<a
1616
{href}
17-
class="flex-1 bg-muted h-48 max-w-96 transform rounded p-3 no-underline shadow motion-safe:transition-transform hover:scale-[1.025]"
17+
class="flex-1 bg-muted h-48 min-w-96 transform rounded p-3 no-underline shadow motion-safe:transition-transform hover:scale-[1.025]"
1818
>
1919
<div class="grid h-full place-content-center justify-items-center gap-2">
2020
{#if type === "project"}

0 commit comments

Comments
 (0)