diff --git a/packages/api/src/controllers/foundEventsController.ts b/packages/api/src/controllers/foundEventsController.ts index 857f5c4..99bb4e1 100644 --- a/packages/api/src/controllers/foundEventsController.ts +++ b/packages/api/src/controllers/foundEventsController.ts @@ -2,7 +2,6 @@ import express from "express"; import { Controller, Get, - Post, Request, Route, Security, @@ -17,13 +16,6 @@ import { KC_SECURITY_NAME } from "../services/keycloak/user/keycloak-user.js"; export class FoundEventsController extends Controller { private readonly foundEventsService = inject(FoundEventsService); - @Post("crawl") - @Security(KC_SECURITY_NAME) - @SuccessResponse("200", "OK") - public async crawl(): Promise { - await this.foundEventsService.crawl(); - } - @Get("future") @Security(KC_SECURITY_NAME) @SuccessResponse("200", "OK") diff --git a/packages/api/src/controllers/pushController.ts b/packages/api/src/controllers/pushController.ts index 923ed2f..2f9f699 100644 --- a/packages/api/src/controllers/pushController.ts +++ b/packages/api/src/controllers/pushController.ts @@ -32,39 +32,13 @@ export class PushContoller extends Controller { ); } - @Post("reset") - @Security(KC_SECURITY_NAME) - @SuccessResponse("200", "OK") - public async resetSubscriptions( - @Request() request: express.Request, - ): Promise { - return await this.pushService.reset(request); - } - @Post("clearSingle") @Security(KC_SECURITY_NAME) @SuccessResponse("200", "OK") public async clearSubscription( @Request() request: express.Request, - @Query() clientId: string, + @Query() clientId?: string, ): Promise { - await this.pushService.clearSubscription(clientId, request); - } - - //TODO remove - @Post("test-publish") - @Security(KC_SECURITY_NAME) - @SuccessResponse("200", "OK") - public async publishTestMessage( - @Request() request: express.Request, - ): Promise { - this.pushService.sendtestNotification(request); - } - - //TODO remove - @Post("notify") - @SuccessResponse("200", "OK") - public async notifyEvents(): Promise { - await this.pushService.sendEventNotifications(); + await this.pushService.clearSubscription(clientId ?? null, request); } } diff --git a/packages/api/src/controllers/testController.ts b/packages/api/src/controllers/testController.ts new file mode 100644 index 0000000..0edfb98 --- /dev/null +++ b/packages/api/src/controllers/testController.ts @@ -0,0 +1,26 @@ +import { Controller, Post, Route, Security, SuccessResponse } from "tsoa"; +import { Claim } from "../dtos/claim.js"; +import { inject } from "../infrastructure/di/index.js"; +import { FoundEventsService } from "../services/foundEvents/foundEventsService.js"; +import { KC_SECURITY_NAME } from "../services/keycloak/user/keycloak-user.js"; +import { PushService } from "../services/push/pushService.js"; + +@Route("test") +export class TestContoller extends Controller { + private readonly pushService = inject(PushService); + private readonly foundEventsService = inject(FoundEventsService); + + @Post("notify") + @Security(KC_SECURITY_NAME, [Claim.UserAdmin]) + @SuccessResponse("200", "OK") + public async notifyEvents(): Promise { + await this.pushService.sendEventNotifications(); + } + + @Post("crawl") + @Security(KC_SECURITY_NAME) + @SuccessResponse("200", "OK") + public async crawl(): Promise { + await this.foundEventsService.crawl(); + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index ad4a4cd..d78b2f9 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,5 +1,6 @@ import constants from "constants"; import cors from "cors"; +import { CronJob } from "cron"; import express, { type NextFunction, type Request, @@ -11,7 +12,9 @@ import { RegisterRoutes } from "./generated/routes.js"; import { inject } from "./infrastructure/di/injector.js"; import { sessionHandler } from "./infrastructure/sessionHandler.js"; import { EnvironmentService } from "./services/environmentService.js"; +import { FoundEventsService } from "./services/foundEvents/foundEventsService.js"; import { KeycloakUser } from "./services/keycloak/user/keycloak-user.js"; +import { PushService } from "./services/push/pushService.js"; // import helmet from "helmet"; // TODO import http2 from 'http2'; @@ -67,12 +70,13 @@ async function run() { // key = fs.readFileSync("src/key.pem"); port = 3000; - app.listen(port, error => { + const server = app.listen(port, error => { if (error) { console.error("Startup crashed:", error); } console.log(`Server is running at http://localhost:${port}`); }); + server.on("close", () => stopCron()); } else { port = 5443; const SSL_CERT_PATH = process.env["SSL_CERT_PATH"]; @@ -100,17 +104,56 @@ async function run() { constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3, }; - https.createServer(sslOptions, app).listen(port, () => { + const server = https.createServer(sslOptions, app).listen(port, () => { console.log(`Server is running on port ${port}`); }); + server.on("close", () => stopCron()); } // const h2SslOptions={}; // http2.createServer(h2SslOptions, app).listen(port, ()=>{ // console.log(`HTTP/2 server is running on port ${port}`); // }) + initJobs(); + startCron(); console.log("setup done"); } +const jobs: CronJob[] = []; + +function initJobs() { + const eventsService = inject(FoundEventsService); + const crawlJob = CronJob.from({ + cronTime: "35 4 * * *", + onTick: async () => { + await eventsService.crawl(); + }, + }); + jobs.push(crawlJob); + + const pushService = inject(PushService); + const notifyJob = CronJob.from({ + cronTime: "10 12 * * *", + onTick: async () => { + await pushService.sendEventNotifications(); + }, + }); + jobs.push(notifyJob); +} + +function startCron() { + console.log("starting jobs"); + for (const job of jobs) { + job.start(); + } +} + +async function stopCron() { + console.log("stopping jobs"); + for (const job of jobs) { + job.stop(); + } +} + run(); diff --git a/packages/api/src/services/foundEvents/foundEventsService.ts b/packages/api/src/services/foundEvents/foundEventsService.ts index 6045469..b3ccc93 100644 --- a/packages/api/src/services/foundEvents/foundEventsService.ts +++ b/packages/api/src/services/foundEvents/foundEventsService.ts @@ -22,7 +22,7 @@ export class FoundEventsService { maxRequestsPerMinute: 100, maxConcurrency: 2, requestHandler: async ({ request, page, log }) => { - if (request.retryCount > 0) return; + // if (request.retryCount > 0) return; const label = request.label; diff --git a/packages/api/src/services/foundEvents/parseTUS.ts b/packages/api/src/services/foundEvents/parseTUS.ts index 23950b3..1f1cc67 100644 --- a/packages/api/src/services/foundEvents/parseTUS.ts +++ b/packages/api/src/services/foundEvents/parseTUS.ts @@ -67,8 +67,8 @@ export async function parseTUSDetail( const aboluteUrl = url != null ? isAbsolute - ? new URL(url) - : new URL(url, pageUrl) + ? new URL(url).toString() + : new URL(url, pageUrl).toString() : ""; return { url: aboluteUrl, diff --git a/packages/api/src/services/push/pushService.ts b/packages/api/src/services/push/pushService.ts index d0e1b4d..4e97138 100644 --- a/packages/api/src/services/push/pushService.ts +++ b/packages/api/src/services/push/pushService.ts @@ -134,164 +134,85 @@ export class PushService { lang: "de-DE", // TODO renotify: true, re-enable when tag requireInteraction: true, - timestamp: date?.valueOf(), + // timestamp: date?.valueOf(), vibrate: [100], // TODO tag: Topic.EVENTS, implement grouping in Angular SW }, }; - for (const subscription of subscriptions) { - if ( - subscription.lastNotified && - subscription.lastNotified >= event.foundDate - ) { + for (const s of subscriptions) { + if (s.lastNotified && s.lastNotified >= event.foundDate) { // this subscription has already seen this event continue; } - if ( - !this.eventMatchesFilter( - event, - subscription.topicConfiguration, - ) - ) { + if (!this.eventMatchesFilter(event, s.topicConfiguration)) { // this subscription does not care for this event continue; } - console.log(`sending ${subscriptions.length} test messages`); - for (const s of subscriptions) { - try { - let backoff = 500; - let retry: boolean; - do { - console.log("SENDING"); - console.dir("payload"); - retry = await this.sendNotification( - s, - payload, - backoff, - ); - backoff *= 2; - } while (retry); - } catch (error) { - console.error("Cannot send notification: ", error); - throw error; - } + try { + let backoff = 500; + let retry: boolean; + let maxRetry = 5; + do { + retry = await this.sendNotification( + s, + payload, + backoff, + ); + if (maxRetry <= 0) { + throw new Error("Exceeded max retry"); + } + backoff *= 2; + maxRetry--; + } while (retry); + await this.db.doRequest( + async prisma => + prisma.pushSubscription.update({ + where: { + id: s.id, + }, + data: { + lastNotified: new Date(Date.now()), + }, + }), + null, + ); + } catch (error) { + console.error("Cannot send notification: ", error); + // throw error; } } } - - await this.db.doRequest( - async prisma => - prisma.pushSubscription.updateMany({ - where: { - id: { - in: subscriptions.map(s => s.id), - }, - }, - data: { - lastNotified: new Date(Date.now()), - }, - }), - null, - ); - } - - public async sendtestNotification(request: Request) { - const subscriptions = await this.db.doRequest( - async prisma => - prisma.pushSubscription.findMany({ where: { topic: "TEST" } }), - request, - ); - - const payload: AngularPushPayload = { - notification: { - title: "ATitle", - actions: [ - { - action: "explode", - title: "Make Boom", - }, - ], - body: "DatBod", - data: { - onActionClick: { - default: { - operation: NotificationActionOperation.OPEN_WINDOW, - }, - explode: { - operation: NotificationActionOperation.OPEN_WINDOW, - url: "explode.html", - }, - }, - }, - icon: "https://taekwondo-chemnitz.toni714.de/favicon.ico", - lang: "de-DE", - renotify: true, - requireInteraction: true, - tag: "group-tag", - timestamp: Date.now(), - vibrate: [100, 50, 100], - }, - }; - - console.log(`sending ${subscriptions.length} test messages`); - for (const s of subscriptions) { - const subscription: webpush.PushSubscription = { - endpoint: s.endpoint, - keys: { - auth: s.auth, - p256dh: s.p256dh, - }, - expirationTime: s.expirationTime, - }; - try { - const result = await webpush.sendNotification( - subscription, - JSON.stringify(payload), - { - topic: Topic.TEST, - }, - ); - console.log("success"); - console.dir(result); - } catch (error) { - console.error("Cannot send notification: ", error); - throw error; - } - } - } - - public async reset(request: Request): Promise { - const userId = await this.userService.getUid(request, request.res); - const subscriptions = await this.db.doRequest( - async prisma => - await prisma.pushSubscription.deleteMany({ - where: { userId: userId }, - }), - request, - ); - - console.log(`deleted ${subscriptions.count} subs for ${userId}`); } public async clearSubscription( - clientId: string, + clientId: string | null, request: Request, ): Promise { const userId = await this.userService.getUid(request, request.res); - const batchResult = await this.db.doRequest( - async prisma => - await prisma.pushSubscription.deleteMany({ - where: { - userId: userId, - AND: { - clientId: clientId, - }, - }, - }), - request, - ); + const batchResult = clientId + ? await this.db.doRequest( + async prisma => + await prisma.pushSubscription.deleteMany({ + where: { + userId: userId, + AND: { + clientId: clientId, + }, + }, + }), + request, + ) + : await this.db.doRequest( + async prisma => + await prisma.pushSubscription.deleteMany({ + where: { + userId: userId, + }, + }), + request, + ); return batchResult.count; } diff --git a/packages/frontend/public/icons/icon-128x128.png b/packages/frontend/public/icons/icon-128x128.png index 5a9a2cc..f5261fc 100644 Binary files a/packages/frontend/public/icons/icon-128x128.png and b/packages/frontend/public/icons/icon-128x128.png differ diff --git a/packages/frontend/public/icons/icon-144x144.png b/packages/frontend/public/icons/icon-144x144.png index 11702cd..024c296 100644 Binary files a/packages/frontend/public/icons/icon-144x144.png and b/packages/frontend/public/icons/icon-144x144.png differ diff --git a/packages/frontend/public/icons/icon-152x152.png b/packages/frontend/public/icons/icon-152x152.png index ff4e06b..b12b688 100644 Binary files a/packages/frontend/public/icons/icon-152x152.png and b/packages/frontend/public/icons/icon-152x152.png differ diff --git a/packages/frontend/public/icons/icon-192x192.png b/packages/frontend/public/icons/icon-192x192.png index afd36a4..af5833d 100644 Binary files a/packages/frontend/public/icons/icon-192x192.png and b/packages/frontend/public/icons/icon-192x192.png differ diff --git a/packages/frontend/public/icons/icon-384x384.png b/packages/frontend/public/icons/icon-384x384.png deleted file mode 100644 index 613ac79..0000000 Binary files a/packages/frontend/public/icons/icon-384x384.png and /dev/null differ diff --git a/packages/frontend/public/icons/icon-512x512.png b/packages/frontend/public/icons/icon-512x512.png deleted file mode 100644 index 7574990..0000000 Binary files a/packages/frontend/public/icons/icon-512x512.png and /dev/null differ diff --git a/packages/frontend/public/icons/icon-72x72.png b/packages/frontend/public/icons/icon-72x72.png index 033724e..e4f93de 100644 Binary files a/packages/frontend/public/icons/icon-72x72.png and b/packages/frontend/public/icons/icon-72x72.png differ diff --git a/packages/frontend/public/icons/icon-96x96.png b/packages/frontend/public/icons/icon-96x96.png index 3090dc2..3e89257 100644 Binary files a/packages/frontend/public/icons/icon-96x96.png and b/packages/frontend/public/icons/icon-96x96.png differ diff --git a/packages/frontend/public/manifest.webmanifest b/packages/frontend/public/manifest.webmanifest index 198d9dd..a5100e8 100644 --- a/packages/frontend/public/manifest.webmanifest +++ b/packages/frontend/public/manifest.webmanifest @@ -1,57 +1,45 @@ { - "name": "frontend", - "short_name": "frontend", - "display": "standalone", - "scope": "./", - "start_url": "./", - "icons": [ - { - "src": "icons/icon-72x72.png", - "sizes": "72x72", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-96x96.png", - "sizes": "96x96", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-128x128.png", - "sizes": "128x128", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-144x144.png", - "sizes": "144x144", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-152x152.png", - "sizes": "152x152", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-384x384.png", - "sizes": "384x384", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "icons/icon-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable any" - } - ] + "name": "TKD Club Chemnitz Datenbank", + "short_name": "TCC DB", + "display": "standalone", + "scope": "./", + "start_url": "./", + "icons": [ + { + "src": "icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + } + ] } diff --git a/packages/frontend/src/app/components/events/events.ts b/packages/frontend/src/app/components/events/events.ts index b6b4cd9..ed65189 100644 --- a/packages/frontend/src/app/components/events/events.ts +++ b/packages/frontend/src/app/components/events/events.ts @@ -74,6 +74,6 @@ export class Events implements OnInit { await this.pushService.resetSubscriptions(); } protected async subscribe() { - this.pushService.subscribeToEvents(); + await this.pushService.subscribeToEvents(); } } diff --git a/packages/frontend/src/app/components/test/test.html b/packages/frontend/src/app/components/test/test.html index 13c0714..2a612f0 100644 --- a/packages/frontend/src/app/components/test/test.html +++ b/packages/frontend/src/app/components/test/test.html @@ -1,20 +1,28 @@

Testeite für interne Berechtigung

API: {{apiStatus()}}

Auth: {{authTest()}}

- - - +

Capabilities: {{pushCaps|json}}

+
More Caps: {{asyncCaps()|json}}
+ + Claim:
    - @let claims = this.auth.claims(); - @if(claims) { -
    +    @let claims = this.auth.claims(); @if(claims) {
    +    
     {{claims | json}}
    -
    +
    }
-
+    
 {{this.auth.userInfo() | json}}
-
+
diff --git a/packages/frontend/src/app/components/test/test.ts b/packages/frontend/src/app/components/test/test.ts index c48e0d0..ae87503 100644 --- a/packages/frontend/src/app/components/test/test.ts +++ b/packages/frontend/src/app/components/test/test.ts @@ -6,25 +6,35 @@ import { inject, signal, } from '@angular/core'; +import { ClaimGuardDirective } from '../../directives/claim-guard.directive'; import * as api from '../../generated-api/api'; import { Authentication } from '../../services/authentication'; import { PushService } from '../../services/push/pushService'; @Component({ selector: 'app-test', - imports: [JsonPipe], + imports: [JsonPipe, ClaimGuardDirective], templateUrl: './test.html', styleUrl: './test.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class Test { protected readonly pushService = inject(PushService); - protected auth = inject(Authentication); + protected readonly auth = inject(Authentication); - protected apiStatus = signal('Waiting'); - protected authTest = signal('Waiting'); + protected readonly apiStatus = signal('Waiting'); + protected readonly authTest = signal('Waiting'); + protected readonly Claim = api.Claim; + protected readonly pushCaps = { + serviceWorker: 'serviceWorker' in navigator, + pushManager: 'PushManager' in window, + notification: 'Notification' in window, + notifyPermission: Notification.permission, + }; + protected asyncCaps = signal(null); public constructor() { + this.loadCaps(); effect(() => { const loggedIn = this.auth.loggedIn(); if (!loggedIn) { @@ -51,4 +61,22 @@ export class Test { }); }); } + + private async loadCaps() { + const caps = { + registraion: await navigator.serviceWorker.getRegistrations(), + subscription: await ( + await navigator.serviceWorker.ready + ).pushManager.getSubscription(), + }; + + this.asyncCaps.set(caps); + } + + protected async notifyAll() { + await api.notifyEvents(); + } + protected async crawl() { + await api.crawl(); + } } diff --git a/packages/frontend/src/app/services/push/pushService.ts b/packages/frontend/src/app/services/push/pushService.ts index e7ed36d..6172398 100644 --- a/packages/frontend/src/app/services/push/pushService.ts +++ b/packages/frontend/src/app/services/push/pushService.ts @@ -25,16 +25,21 @@ export class PushService { public async subscribeToEvents() { try { + console.log('trying to subscribe'); const subscription = await this.swPush.requestSubscription({ serverPublicKey: VAPID.publicKey, }); + console.log('returned'); + console.dir(subscription); + console.log('end'); + const previousCliendId = localStorage.getItem( SUBSCRIPTION_CLIENT_ID, ); if (previousCliendId) { try { - await api.clearSubscription(previousCliendId); + await api.clearSubscription({ clientId: previousCliendId }); console.log('previous subscription cleared'); } catch {} } @@ -62,11 +67,10 @@ export class PushService { } public async resetSubscriptions() { + const clientId = localStorage.getItem(SUBSCRIPTION_CLIENT_ID); await this.swPush.unsubscribe(); - await api.resetSubscriptions(); - } - - public async publishTestMessage() { - await api.publishTestMessage(); + await api.clearSubscription({ + clientId: clientId ?? undefined, + }); } }