Skip to content

Commit

Permalink
initial web push
Browse files Browse the repository at this point in the history
  • Loading branch information
samurex committed Apr 15, 2024
1 parent b04f0b2 commit 35c46f3
Show file tree
Hide file tree
Showing 15 changed files with 245 additions and 299 deletions.
1 change: 1 addition & 0 deletions examples/vuejectron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"core-js": "^3.33.0",
"pinia": "^2.1.7",
"roboto-fontface": "*",
"vite-plugin-pwa": "^0.19.0",
"vue": "^3.3.4",
"vue-router": "^4.2.5",
"vuetify": "^3.5.6",
Expand Down
39 changes: 39 additions & 0 deletions examples/vuejectron/src/store/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,46 @@ export const useAppStore = defineStore('app', () => {
const currentAgent = ref<Agent>();
const currentProject = ref<Project>();
const saiError = ref<string | undefined>();
const pushSubscription = ref<PushSubscription | null>(null);

let solidLdoDataset: SolidLdoDataset
const ldoProjects = ref<Record<RegistrationId, Project[]>>({});
const ldoTasks = ref<Record<ProjectId, Task[]>>({});


async function getPushSubscription() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
pushSubscription.value = subscription;
}
}

async function enableNotifications() {
const session = await ensureSaiSession();
if (!session.webPushService) {
return null
}
const result = await Notification.requestPermission();
if (result === 'granted') {
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: session.webPushService?.vapidPublicKey
});
}
pushSubscription.value = subscription;
}
return result;
}

async function subscribeViaPush(): Promise<void> {
const session = await ensureSaiSession();
await session.subscribeViaPush(pushSubscription.value!, session.registrationIri);
}

async function loadAgents(force = false): Promise<void> {
if (force || !agents.value.length) {
agents.value = await getAgents();
Expand Down Expand Up @@ -457,6 +492,10 @@ export const useAppStore = defineStore('app', () => {


return {
pushSubscription,
getPushSubscription,
enableNotifications,
subscribeViaPush,
agents,
currentAgent,
currentProject,
Expand Down
4 changes: 4 additions & 0 deletions examples/vuejectron/src/store/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class OidcError extends Error {

export const useCoreStore = defineStore('core', () => {
const userId = ref<string | null>(null);
const isAuthorized = ref(false);
const saiError = ref<string | undefined>();
const authorizationRedirectUri = ref<string | null>(null);


async function login(oidcIssuer: string) {
const options = {
Expand Down
19 changes: 19 additions & 0 deletions examples/vuejectron/src/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
self.addEventListener('push', (event) => {

// show notification
self.registration.showNotification('SAI Auth', {
body: event.data.json().notification.body,
});

// send to all clients
self.clients.matchAll().then((clientList) => {
for (const client of clientList) {
client.postMessage(event.data.json().notification)
}
});

});

// TODO: handle messages from clients
self.addEventListener('message', () => {
})
33 changes: 32 additions & 1 deletion examples/vuejectron/src/views/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
<template>
<v-main>
<v-btn @click="appStore.subscribeViaPush">Test</v-btn>
<v-alert
v-if="!appStore.pushSubscription"
color="warning"
icon="mdi-bell-cog-outline"
title="Notifications"
text="currently disabled"
>
<template #append>
<v-btn
color="secondary"
:loading="enableNotificationsLoading"
@click="enableNotifications"
>Enable</v-btn>
</template>
</v-alert>
<v-list>
<v-list-item v-for="agent in appStore.agents" :key="agent.id">
<router-link :to="{ name: 'agent', query: { agent: agent.id } }">
Expand All @@ -10,7 +26,22 @@
</v-main>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useAppStore } from '@/store/app';
const appStore = useAppStore();
const appStore = useAppStore()
const enableNotificationsLoading = ref(false)
async function enableNotifications() {
enableNotificationsLoading.value = true
await appStore.enableNotifications()
enableNotificationsLoading.value = false
}
await appStore.getPushSubscription()
// TODO: act differently depending on message.data
navigator.serviceWorker.onmessage = (message) => {
console.log('message', message)
};
</script>
16 changes: 15 additions & 1 deletion examples/vuejectron/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
// Utilities
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
import { VitePWA } from 'vite-plugin-pwa';

// https://vitejs.dev/config/
export default defineConfig({
Expand All @@ -18,7 +19,20 @@ export default defineConfig({
styles: {
configFile: 'src/styles/settings.scss'
}
})
}),
// https://vite-pwa-org.netlify.app/
VitePWA({
registerType: 'autoUpdate',
strategies: 'injectManifest',
injectManifest: {
injectionPoint: undefined
},
srcDir: 'src',
filename: 'sw.js',
devOptions: {
enabled: true
}
}),
],
define: { 'process.env': {} },
resolve: {
Expand Down
33 changes: 32 additions & 1 deletion packages/application/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
discoverAuthorizationAgent,
discoverAgentRegistration,
discoverAuthorizationRedirectEndpoint,
NOTIFY
NOTIFY,
discoverWebPushService
} from '@janeirodigital/interop-utils';

interface ApplicationDependencies {
Expand Down Expand Up @@ -37,6 +38,8 @@ export class Application {

authorizationRedirectEndpoint: string;

webPushService?: {id: string, vapidPublicKey: string};

registrationIri: string;

// TODO rename
Expand All @@ -58,6 +61,11 @@ export class Application {
this.authorizationAgentIri,
this.rawFetch
);
// TODO: avoid double fetch
this.webPushService = await discoverWebPushService(
this.authorizationAgentIri,
this.rawFetch
);
if (!this.registrationIri) return;
await this.buildRegistration();
await this.subscribeToRegistration();
Expand Down Expand Up @@ -89,6 +97,29 @@ export class Application {
};
}

async subscribeViaPush(subscription: PushSubscription, topic: string): Promise<void> {
if (!this.webPushService) throw new Error('Web Push Service not found');
const channel = {
"@context": [
"https://www.w3.org/ns/solid/notifications-context/v1"
],
type: "WebPushChannel2023",
topic,
sentTo: subscription.endpoint,
keys: subscription.toJSON()['keys']
};
const response = await this.fetch(this.webPushService.id, {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json'
},
body: JSON.stringify(channel)
});
if (!response.ok) {
throw new Error('Failed to subscribe via push');
}
}

get authorizationRedirectUri(): string {
return `${this.authorizationRedirectEndpoint}?client_id=${encodeURIComponent(this.applicationId)}`;
}
Expand Down
30 changes: 30 additions & 0 deletions packages/service/config/controllers/web-push.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"@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:WebPush",
"@type": "HttpHandlerController",
"label": "WebPush Controller",
"routes": [
{
"@type": "HttpHandlerRoute",
"path": "/agents/:encodedWebId/webpush",
"operations": [
{
"@type": "HttpHandlerOperation",
"method": "POST",
"publish": false
}
],
"handler": {
"@type": "WebPushHandler"
}
}
]
}
]
}
4 changes: 3 additions & 1 deletion packages/service/config/service.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"./controllers/login-redirect.json",
"./controllers/api.json",
"./controllers/webhooks.json",
"./controllers/invitations.json"
"./controllers/invitations.json",
"./controllers/web-push.json"
],
"@graph": [
{
Expand All @@ -33,6 +34,7 @@
{ "@id": "urn:solid:authorization-agent:controller:LoginRedirect" },
{ "@id": "urn:solid:authorization-agent:controller:API" },
{ "@id": "urn:solid:authorization-agent:controller:Webhooks" },
{ "@id": "urn:solid:authorization-agent:controller:WebPush" },
{ "@id": "urn:solid:authorization-agent:controller:Invitations" }
]
}
Expand Down
15 changes: 11 additions & 4 deletions packages/service/src/handlers/agents-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@ import { HttpHandler, HttpHandlerResponse } from '@digita-ai/handlersjs-http';
import { INTEROP } from '@janeirodigital/interop-utils';
import type { ISessionManager } from '@janeirodigital/sai-server-interfaces';
import type { AuthnContext } from '../models/http-solid-context';
import { agentRedirectUrl, agentUrl2webId } from '../url-templates';
import { agentRedirectUrl, agentUrl2encodedWebId, agentUrl2webId } from '../url-templates';

function clientIdDocument(agentUrl: string) {
return {
'@context': [
'https://www.w3.org/ns/solid/oidc-context.jsonld',
'https://www.w3.org/ns/solid/notifications-context/v1',
{
interop: 'http://www.w3.org/ns/solid/interop#'
}
interop: 'http://www.w3.org/ns/solid/interop#',
notify: 'http://www.w3.org/ns/solid/notifications#'
},
],
client_id: agentUrl,
client_name: 'Solid Authorization Agent',
redirect_uris: [agentRedirectUrl(agentUrl)],
grant_types: ['refresh_token', 'authorization_code'],
'interop:hasAuthorizationRedirectEndpoint': process.env.FRONTEND_AUTHORIZATION_URL!
'interop:hasAuthorizationRedirectEndpoint': process.env.FRONTEND_AUTHORIZATION_URL!,
'interop:pushService': {
'id': `${process.env.BASE_URL}/agents/${agentUrl2encodedWebId(agentUrl)}/webpush`,
"channelType": 'notify:WebPushChannel2023',
"notify:vapidPublicKey": process.env.VAPID_PUBLIC_KEY!,
}
};
}

Expand Down
34 changes: 34 additions & 0 deletions packages/service/src/handlers/web-push-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable class-methods-use-this */

import { from, Observable } from 'rxjs';
import { HttpHandler, HttpHandlerResponse, HttpHandlerContext } from '@digita-ai/handlersjs-http';
import { getLogger } from '@digita-ai/handlersjs-logging';
import { IQueue } from '@janeirodigital/sai-server-interfaces';
import { validateContentType } from '../utils/http-validators';
import { decodeWebId } from '../url-templates';
import { IDelegatedGrantsJobData, IPushNotificationsJobData } from '../models/jobs';

export class WebPushHandler extends HttpHandler {
private logger = getLogger();

constructor(
// private grantsQueue: IQueue,
// private pushQueue: IQueue
) {
super();
this.logger.info('WebPushHandler::constructor');
}

async handleAsync(context: HttpHandlerContext): Promise<HttpHandlerResponse> {
// validateContentType(context, 'application/ld+json');
const webId = decodeWebId(context.request.parameters!.encodedWebId);
console.log(context.request.body);

return { body: {}, status: 200, headers: {} };
}

handle(context: HttpHandlerContext): Observable<HttpHandlerResponse> {
this.logger.info('WebPushHandler::handle');
return from(this.handleAsync(context));
}
}
1 change: 1 addition & 0 deletions packages/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './handlers/authn-context-handler';
export * from './handlers/api-handler';
export * from './handlers/webhooks-handler';
export * from './handlers/invitations-handler';
export * from './handlers/web-push-handler';

// Models
export * from './models/http-solid-context';
Expand Down
7 changes: 5 additions & 2 deletions packages/service/src/url-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ export function webId2agentUrl(webId: string): string {
return `${baseUrl}/agents/${encoded}`;
}

export function agentUrl2encodedWebId(agentUrl: string): string {
return agentUrl.split('/').at(-1)!;
}

export function agentUrl2webId(agentUrl: string): string {
const encoded = agentUrl.split('/').at(-1)!;
return decodeWebId(encoded);
return decodeWebId(agentUrl2encodedWebId(agentUrl));
}

export function agentRedirectUrl(agentUrl: string): string {
Expand Down
Loading

0 comments on commit 35c46f3

Please sign in to comment.