diff --git a/packages/api-messages/src/requests.ts b/packages/api-messages/src/requests.ts index 9b7f9659..0014736a 100644 --- a/packages/api-messages/src/requests.ts +++ b/packages/api-messages/src/requests.ts @@ -30,6 +30,10 @@ abstract class MessageBase { export class HelloRequest extends MessageBase { public type = RequestMessageTypes.HELLO_REQUEST; + + constructor(public subscription?: PushSubscription) { + super(); + } } export class ApplicationsRequest extends MessageBase { diff --git a/packages/service/config/controllers/push-subscription.json b/packages/service/config/controllers/push-subscription.json deleted file mode 100644 index 507efa3a..00000000 --- a/packages/service/config/controllers/push-subscription.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "@context": [ - "https://linkedsoftwaredependencies.org/bundles/npm/@janeirodigital/sai-server/^0.0.0/components/context.jsonld", - "https://linkedsoftwaredependencies.org/bundles/npm/@digita-ai/handlersjs-core/^0.0.0/components/context.jsonld", - "https://linkedsoftwaredependencies.org/bundles/npm/@digita-ai/handlersjs-http/^0.0.0/components/context.jsonld" - ], - "@graph": [ - { - "@id": "urn:solid:authorization-agent:controller:PushSubscription", - "@type": "HttpHandlerController", - "label": "Push Subscription Controller", - "preResponseHandler": { - "@type": "HttpSequenceContextHandler", - "contextHandlers": [ - { - "@type": "AuthnContextHandler" - } - ] - }, - "routes": [ - { - "@type": "HttpHandlerRoute", - "path": "/push-subscribe", - "operations": [ - { - "@type": "HttpHandlerOperation", - "method": "POST", - "publish": false - } - ], - "handler": { - "@type": "PushSubscriptionHandler", - "sessionManager": { - "@id": "urn:ssv:SessionManager" - } - } - } - ] - } - ] -} diff --git a/packages/service/config/service.json b/packages/service/config/service.json index ee6e66d0..044e85b6 100644 --- a/packages/service/config/service.json +++ b/packages/service/config/service.json @@ -8,7 +8,6 @@ "./controllers/agents.json", "./controllers/login-redirect.json", "./controllers/api.json", - "./controllers/push-subscription.json", "./controllers/webhooks.json", "./controllers/invitations.json" ], @@ -33,7 +32,6 @@ { "@id": "urn:solid:authorization-agent:controller:Agents" }, { "@id": "urn:solid:authorization-agent:controller:LoginRedirect" }, { "@id": "urn:solid:authorization-agent:controller:API" }, - { "@id": "urn:solid:authorization-agent:controller:PushSubscription" }, { "@id": "urn:solid:authorization-agent:controller:Webhooks" }, { "@id": "urn:solid:authorization-agent:controller:Invitations" } ] diff --git a/packages/service/src/handlers/api-handler.ts b/packages/service/src/handlers/api-handler.ts index 62a9e660..af042540 100644 --- a/packages/service/src/handlers/api-handler.ts +++ b/packages/service/src/handlers/api-handler.ts @@ -42,6 +42,10 @@ export class ApiHandler extends HttpHandler { throw new BadRequestHttpError('body is required'); } if (body.type === RequestMessageTypes.HELLO_REQUEST) { + if (body.subscription) { + await this.sessionManager.addPushSubscription(context.authn.webId, body.subscription); + } + const oidcSession = await this.sessionManager.getOidcSession(context.authn.webId); let loginStatus: LoginStatus; diff --git a/packages/service/src/handlers/push-subscription-handler.ts b/packages/service/src/handlers/push-subscription-handler.ts deleted file mode 100644 index 94796015..00000000 --- a/packages/service/src/handlers/push-subscription-handler.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { from, Observable } from 'rxjs'; -import { BadRequestHttpError, HttpHandler, HttpHandlerResponse } from '@digita-ai/handlersjs-http'; -import { getLogger } from '@digita-ai/handlersjs-logging'; -import type { ISessionManager } from '@janeirodigital/sai-server-interfaces'; -import type { PushSubscription } from 'web-push'; -import type { AuthenticatedAuthnContext } from '../models/http-solid-context'; -import { validateContentType } from '../utils/http-validators'; - -export class PushSubscriptionHandler extends HttpHandler { - private logger = getLogger(); - - constructor(private sessionManager: ISessionManager) { - super(); - this.logger.info('PushSubscriptionHandler::constructor'); - } - - handle(context: AuthenticatedAuthnContext): Observable { - this.logger.info('PushSubscriptionHandler::handle'); - return from(this.handleAsync(context)); - } - - private async handleAsync(context: AuthenticatedAuthnContext): Promise { - validateContentType(context, 'application/json'); - // TODO: validate subscription - if (!context.request.body) { - throw new BadRequestHttpError(); - } - const subscription = context.request.body as PushSubscription; - await this.sessionManager.addPushSubscription(context.authn.webId, subscription); - - return { body: {}, status: 204, headers: {} }; - } -} diff --git a/packages/service/src/index.ts b/packages/service/src/index.ts index ad7856fe..b13a980f 100644 --- a/packages/service/src/index.ts +++ b/packages/service/src/index.ts @@ -8,7 +8,6 @@ export * from './handlers/agents-handler'; export * from './handlers/middleware-http-handler'; export * from './handlers/authn-context-handler'; export * from './handlers/api-handler'; -export * from './handlers/push-subscription-handler'; export * from './handlers/webhooks-handler'; export * from './handlers/invitations-handler'; diff --git a/packages/service/test/unit/handlers/api-handler-test.ts b/packages/service/test/unit/handlers/api-handler-test.ts index 1ea85849..86d54b4a 100644 --- a/packages/service/test/unit/handlers/api-handler-test.ts +++ b/packages/service/test/unit/handlers/api-handler-test.ts @@ -47,7 +47,8 @@ const logger = getLogger(); const saiSession: AuthorizationAgent = {} as AuthorizationAgent; const manager = { getSaiSession: jest.fn(() => saiSession), - getOidcSession: jest.fn() + getOidcSession: jest.fn(), + addPushSubscription: jest.fn() } as unknown as jest.Mocked; let apiHandler: ApiHandler; @@ -67,6 +68,7 @@ const headers = { 'content-type': 'application/json' }; beforeEach(() => { manager.getSaiSession.mockClear(); manager.getOidcSession.mockReset(); + manager.addPushSubscription.mockReset(); queue = new MockedQueue('grants'); apiHandler = new ApiHandler(manager, queue); }); @@ -121,6 +123,25 @@ describe('hello', () => { } }); }); + test('should add the subscription', (done) => { + const oidcSession = { + info: { isLoggedIn: false } + } as unknown as Session; + manager.getOidcSession.mockResolvedValueOnce(oidcSession); + const request = { + headers: { 'content-type': 'application/json' }, + body: { + type: RequestMessageTypes.HELLO_REQUEST, + subscription: { endpoint: 'https://endpoint.example' } as PushSubscription + } + } as unknown as HttpHandlerRequest; + const ctx = { request, authn } as AuthenticatedAuthnContext; + + apiHandler.handle(ctx).subscribe(() => { + expect(manager.addPushSubscription).toBeCalledWith(webId, request.body.subscription); + done(); + }); + }); }); describe('incorrect request', () => { diff --git a/packages/service/test/unit/handlers/push-subscription-handler-test.ts b/packages/service/test/unit/handlers/push-subscription-handler-test.ts deleted file mode 100644 index 96e0c1c7..00000000 --- a/packages/service/test/unit/handlers/push-subscription-handler-test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { jest } from '@jest/globals'; -import type { PushSubscription } from 'web-push'; -import { InMemoryStorage } from '@inrupt/solid-client-authn-node'; -import { BadRequestHttpError, HttpError, HttpHandlerRequest } from '@digita-ai/handlersjs-http'; - -import { PushSubscriptionHandler, AuthenticatedAuthnContext } from '../../../src'; - -import { SessionManager } from '../../../src/session-manager'; -jest.mock('../../../src/session-manager', () => { - return { - SessionManager: jest.fn(() => { - return { - addPushSubscription: jest.fn() - }; - }) - }; -}); - -let pushSubscriptionHandler: PushSubscriptionHandler; -const sessionManager = jest.mocked(new SessionManager(new InMemoryStorage())); - -const aliceWebId = 'https://alice.example'; - -const authn = { - webId: aliceWebId, - clientId: 'https://projectron.example' -}; - -beforeEach(() => { - pushSubscriptionHandler = new PushSubscriptionHandler(sessionManager); - sessionManager.addPushSubscription.mockReset(); -}); - -test('should respond with 400 if not application/json', (done) => { - const request = { - headers: {}, - body: { beep: 'boop' } - } as unknown as HttpHandlerRequest; - const ctx = { request, authn } as AuthenticatedAuthnContext; - - pushSubscriptionHandler.handle(ctx).subscribe({ - error: (e: HttpError) => { - expect(e).toBeInstanceOf(BadRequestHttpError); - done(); - } - }); -}); - -test('should respond with 400 if no body', (done) => { - const request = { - headers: { 'content-type': 'application/json' } - } as unknown as HttpHandlerRequest; - const ctx = { request, authn } as AuthenticatedAuthnContext; - - pushSubscriptionHandler.handle(ctx).subscribe({ - error: (e: HttpError) => { - expect(e).toBeInstanceOf(BadRequestHttpError); - done(); - } - }); -}); - -test('should add the subscription', (done) => { - const request = { - headers: { 'content-type': 'application/json' }, - body: { endpoint: 'https://endpoint.example' } as PushSubscription - } as unknown as HttpHandlerRequest; - const ctx = { request, authn } as AuthenticatedAuthnContext; - - pushSubscriptionHandler.handle(ctx).subscribe((response) => { - expect(sessionManager.addPushSubscription).toBeCalledWith(aliceWebId, request.body); - expect(response.status).toBe(204); - done(); - }); -}); diff --git a/ui/authorization/src/backend.ts b/ui/authorization/src/backend.ts index 858bc2d8..6571c722 100644 --- a/ui/authorization/src/backend.ts +++ b/ui/authorization/src/backend.ts @@ -75,8 +75,8 @@ async function getDataFromApi(request: Request): Prom return (await response.json()) as T; } -async function checkServerSession(): Promise { - const request = new HelloRequest(); +async function checkServerSession(subscription?: PushSubscription): Promise { + const request = new HelloRequest(subscription); const data = await getDataFromApi(request); const response = new HelloResponse(data); return response.payload; @@ -171,18 +171,6 @@ async function acceptInvitation(capabilityUrl: string, label: string, note?: str return response.payload; } -// TODO: use api messages -async function subscribeToPushNotifications(subscription: PushSubscription) { - const options = { - method: 'POST', - body: JSON.stringify(subscription), - headers: { - 'Content-Type': 'application/json' - } - }; - await authnFetch(`${backendBaseUrl}/push-subscribe`, options); -} - export function useBackend() { return { checkServerSession, @@ -198,7 +186,6 @@ export function useBackend() { listDataRegistires, createInvitation, listSocialAgentInvitations, - acceptInvitation, - subscribeToPushNotifications + acceptInvitation }; } diff --git a/ui/authorization/src/store/core.ts b/ui/authorization/src/store/core.ts index 61e3860c..0eef7399 100644 --- a/ui/authorization/src/store/core.ts +++ b/ui/authorization/src/store/core.ts @@ -34,6 +34,15 @@ export const useCoreStore = defineStore('core', () => { await oidcLogin(options); } + async function getPushSubscription() { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + if (subscription) { + pushSubscription.value = subscription; + } + return backend.checkServerSession(subscription ?? undefined); + } + async function handleRedirect(url: string) { const oidcInfo = await handleIncomingRedirect(url); if (!oidcInfo?.webId) { @@ -42,7 +51,7 @@ export const useCoreStore = defineStore('core', () => { userId.value = oidcInfo.webId; // TODO check if backend authenticated - const loginStatus = await backend.checkServerSession(); + const loginStatus = await getPushSubscription(); isBackendLoggedIn.value = loginStatus.isLoggedIn; redirectUrlForBackend.value = loginStatus.completeRedirectUrl ?? ''; } @@ -60,17 +69,6 @@ export const useCoreStore = defineStore('core', () => { } } - async function getPushSubscription() { - const registration = await navigator.serviceWorker.ready; - const subscription = await registration.pushManager.getSubscription(); - if (subscription) { - pushSubscription.value = subscription; - await backend.subscribeToPushNotifications(subscription); - } - } - - /* TODO: DRY ⬆️⬇️ */ - async function enableNotifications() { const result = await Notification.requestPermission(); if (result === 'granted') { @@ -83,8 +81,9 @@ export const useCoreStore = defineStore('core', () => { }); } pushSubscription.value = subscription; - await backend.subscribeToPushNotifications(subscription); + await backend.checkServerSession(subscription); } + return result; } return { diff --git a/ui/authorization/src/views/Dashboard.vue b/ui/authorization/src/views/Dashboard.vue index 8fc6e768..f2d45603 100644 --- a/ui/authorization/src/views/Dashboard.vue +++ b/ui/authorization/src/views/Dashboard.vue @@ -49,12 +49,13 @@ import { useAppStore } from '@/store/app'; const coreStore = useCoreStore() const appStore = useAppStore() -coreStore.getPushSubscription() const enableNotificationsLoading = ref(false) -function enableNotifications() { +async function enableNotifications() { enableNotificationsLoading.value = true - coreStore.enableNotifications() + await coreStore.enableNotifications() + enableNotificationsLoading.value = false + } // TODO: act differently depending on message.data