diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index fd9294b..950c11a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "notificationlatch",
+ "name": "notificationlatch-frontend",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
@@ -1151,6 +1151,74 @@
"postcss": "^7.0.0"
}
},
+ "@intlify/core-base": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.7.tgz",
+ "integrity": "sha512-q1W2j81xbHyfKrNcca/CeJyf0Bcx4u9UDu05l7AaiJbqOseTme2o2I3wp1hDDCtmC7k7HgX0sAygyHNJH9swuQ==",
+ "requires": {
+ "@intlify/devtools-if": "9.1.7",
+ "@intlify/message-compiler": "9.1.7",
+ "@intlify/message-resolver": "9.1.7",
+ "@intlify/runtime": "9.1.7",
+ "@intlify/shared": "9.1.7",
+ "@intlify/vue-devtools": "9.1.7"
+ }
+ },
+ "@intlify/devtools-if": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.7.tgz",
+ "integrity": "sha512-/DcN5FUySSkQhDqx5y1RvxfuCXO3Ot/dUEIOs472qbM7Hyb2qif+eXCnwHBzlI4+wEfQVT6L0PiM1a7Er/ro9g==",
+ "requires": {
+ "@intlify/shared": "9.1.7"
+ }
+ },
+ "@intlify/message-compiler": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.7.tgz",
+ "integrity": "sha512-JZNkAhr3O7tnbdbRBcpYfqr/Ai26WTzX0K/lV8Y1KVdOIj/dGiamaffdWUdFiDXUnbJRNbPiOaKxy7Pwip3KxQ==",
+ "requires": {
+ "@intlify/message-resolver": "9.1.7",
+ "@intlify/shared": "9.1.7",
+ "source-map": "0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "@intlify/message-resolver": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.7.tgz",
+ "integrity": "sha512-WTK+OaXJYjyquLGhuCyDvU2WHkG+kXzXeHagmVFHn+s118Jf2143zzkLLUrapP5CtZ/csuyjmYg7b3xQRQAmvw=="
+ },
+ "@intlify/runtime": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.7.tgz",
+ "integrity": "sha512-QURPSlzhOVnRwS2XMGpCDsDkP42kfVBh94aAORxh/gVGzdgJip2vagrIFij/J69aEqdB476WJkMhVjP8VSHmiA==",
+ "requires": {
+ "@intlify/message-compiler": "9.1.7",
+ "@intlify/message-resolver": "9.1.7",
+ "@intlify/shared": "9.1.7"
+ }
+ },
+ "@intlify/shared": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.7.tgz",
+ "integrity": "sha512-zt0zlUdalumvT9AjQNxPXA36UgOndUyvBMplh8uRZU0fhWHAwhnJTcf0NaG9Qvr8I1n3HPSs96+kLb/YdwTavQ=="
+ },
+ "@intlify/vue-devtools": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.7.tgz",
+ "integrity": "sha512-DI5Wc0aOiohtBUGUkKAcryCWbbuaO4/PK4Pa/LaNCsFNxbtgR5qkIDmhBv9xVPYGTUhySXxaDDAMvOpBjhPJjw==",
+ "requires": {
+ "@intlify/message-resolver": "9.1.7",
+ "@intlify/runtime": "9.1.7",
+ "@intlify/shared": "9.1.7"
+ }
+ },
"@mrmlnc/readdir-enhanced": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@@ -1279,6 +1347,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
+ "@types/luxon": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.0.tgz",
+ "integrity": "sha512-L7iL3FitRSeuz8fbeLtql7qU6inHVtwEDWI1+vBXgyp0J2tmxOD7TgMBiEQjII/Y/TPcwrKasXb1BPuiCXRgxg==",
+ "dev": true
+ },
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -2465,6 +2539,14 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true
},
+ "axios": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+ "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -5369,8 +5451,7 @@
"follow-redirects": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
- "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==",
- "dev": true
+ "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
},
"for-in": {
"version": "1.0.2",
@@ -7074,6 +7155,11 @@
"yallist": "^3.0.2"
}
},
+ "luxon": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz",
+ "integrity": "sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg=="
+ },
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@@ -11087,6 +11173,17 @@
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
"dev": true
},
+ "vue-i18n": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.7.tgz",
+ "integrity": "sha512-ujuuDanoHqtEd4GejWrbG/fXE9nrP51ElsEGxp0WBHfv+/ki0/wyUqkO+4fLikki2obGtXdviTPH0VNpas5K6g==",
+ "requires": {
+ "@intlify/core-base": "9.1.7",
+ "@intlify/shared": "9.1.7",
+ "@intlify/vue-devtools": "9.1.7",
+ "@vue/devtools-api": "^6.0.0-beta.7"
+ }
+ },
"vue-loader": {
"version": "15.9.8",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.8.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 20e6d42..4953db4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,11 +7,15 @@
"build": "vue-cli-service build"
},
"dependencies": {
+ "axios": "^0.21.1",
"core-js": "^3.6.5",
+ "luxon": "^2.0.2",
"vue": "^3.0.0",
+ "vue-i18n": "^9.1.7",
"vue-router": "^4.0.0-0"
},
"devDependencies": {
+ "@types/luxon": "^2.0.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 3e5a139..bc3a307 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -5,11 +5,11 @@
-
<%= htmlWebpackPlugin.options.title %>
+ Notifications
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 3fa872e..8f7b9ff 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,30 +1,14 @@
-
- Home |
- About
-
-
+
diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png
deleted file mode 100644
index f3d2503..0000000
Binary files a/frontend/src/assets/logo.png and /dev/null differ
diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue
deleted file mode 100644
index 5d69cd7..0000000
--- a/frontend/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
{{ msg }}
-
- For a guide and recipes on how to configure / customize this project,
- check out the
- vue-cli documentation.
-
-
Installed CLI Plugins
-
-
Essential Links
-
-
Ecosystem
-
-
-
-
-
-
-
-
diff --git a/frontend/src/locale/en.ts b/frontend/src/locale/en.ts
new file mode 100644
index 0000000..4b08550
--- /dev/null
+++ b/frontend/src/locale/en.ts
@@ -0,0 +1,24 @@
+export default {
+ notification: {
+ loading: 'Loading, please wait...',
+ tokenInvalid: 'Invalid token',
+ listHeader: 'Disabled notifications',
+
+ latchTime: 'Sent on {latchTime}',
+ latched: 'This notification will not be sent again until it is re-enabled.',
+ enableNotification: 'Enable',
+
+ reminders: 'If enabled, a reminder will be sent every {interval}. Use the buttons below to disable or enable reminders. This will apply only to this notification.',
+ remindersDisabled: 'No reminders',
+ remindersEnabled: 'Send reminders'
+ },
+
+ duration: {
+ glue: ', ',
+ lastGlue: ' and ',
+ days: '{count} day | {count} days',
+ hours: '{count} hour | {count} hours',
+ minutes: '{count} minute | {count} minutes',
+ seconds: '{count} second | {count} seconds'
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts
new file mode 100644
index 0000000..7a2adc6
--- /dev/null
+++ b/frontend/src/locale/index.ts
@@ -0,0 +1,7 @@
+import en from './en';
+import nl from './nl';
+
+export default {
+ en,
+ nl
+};
\ No newline at end of file
diff --git a/frontend/src/locale/nl.ts b/frontend/src/locale/nl.ts
new file mode 100644
index 0000000..5348cc1
--- /dev/null
+++ b/frontend/src/locale/nl.ts
@@ -0,0 +1,24 @@
+export default {
+ notification: {
+ loading: 'Bezig met laden, een ogenblik geduld a.u.b....',
+ tokenInvalid: 'Ongeldig token',
+ listHeader: 'Uitgeschakelde meldingen',
+
+ latchTime: 'Verzonden op {latchTime}',
+ latched: 'Deze notificatie wordt niet meer verzonden totdat deze weer wordt ingeschakeld.',
+ enableNotification: 'Inschakelen',
+
+ reminders: 'Indien ingeschakeld wordt een herinnering elke {interval} gestuurd. Gebruik de knoppen hieronder om herinneringen uit of in te schakelen. Dit geldt alleen voor deze notificatie.',
+ remindersDisabled: 'Geen herinneringen',
+ remindersEnabled: 'Herinneringen sturen'
+ },
+
+ duration: {
+ glue: ', ',
+ lastGlue: ' en ',
+ days: '{count} dag | {count} dagen',
+ hours: '{count} uur | {count} uren',
+ minutes: '{count} minuut | {count} minuten',
+ seconds: '{count} seconde | {count} seconden'
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 3e79677..3d89f97 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,5 +1,17 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-import router from './router'
+import { createApp } from 'vue';
+import { createI18n } from 'vue-i18n';
+import App from './App.vue';
+import router from './router';
+import messages from './locale';
-createApp(App).use(router).mount('#app')
+const i18n = createI18n({
+ locale: navigator.language.substr(0, 2),
+ fallbackLocale: 'en',
+ messages
+});
+
+
+createApp(App)
+ .use(router)
+ .use(i18n)
+ .mount('#app')
diff --git a/frontend/src/model/notifications.ts b/frontend/src/model/notifications.ts
new file mode 100644
index 0000000..4b86a7c
--- /dev/null
+++ b/frontend/src/model/notifications.ts
@@ -0,0 +1,28 @@
+import { Duration } from 'luxon';
+
+
+export interface ILatchedNotifications
+{
+ reminders: INotificationReminders;
+ notifications: Array;
+}
+
+
+export interface INotificationReminders
+{
+ enabled: boolean;
+ interval: Duration;
+}
+
+
+export interface INotification
+{
+ token: string;
+ id: string;
+ title: string;
+ latched: boolean;
+ latchTime: number;
+ resetTime?: number;
+ reminders: boolean;
+ remindTime?: number;
+}
\ No newline at end of file
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index b4d0ef0..043df96 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,25 +1,25 @@
-import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
-import Home from '../views/Home.vue'
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
+import Default from '../views/Default.vue';
+import Notification from '../views/Notification.vue';
+
const routes: Array = [
{
path: '/',
- name: 'Home',
- component: Home
+ name: 'Default',
+ component: Default
},
{
- path: '/about',
- name: 'About',
- // route level code-splitting
- // this generates a separate chunk (about.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
+ path: '/n/:token',
+ name: 'Notification',
+ component: Notification,
+ props: true
}
-]
+];
const router = createRouter({
history: createWebHashHistory(),
routes
-})
+});
-export default router
+export default router;
\ No newline at end of file
diff --git a/frontend/src/views/About.vue b/frontend/src/views/About.vue
deleted file mode 100644
index 3fa2807..0000000
--- a/frontend/src/views/About.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
This is an about page
-
-
diff --git a/frontend/src/views/Default.vue b/frontend/src/views/Default.vue
new file mode 100644
index 0000000..503f72e
--- /dev/null
+++ b/frontend/src/views/Default.vue
@@ -0,0 +1,5 @@
+
+
+ Nothing to see here, move along.
+
+
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
deleted file mode 100644
index 50d8a19..0000000
--- a/frontend/src/views/Home.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/frontend/src/views/Notification.vue b/frontend/src/views/Notification.vue
new file mode 100644
index 0000000..3e07877
--- /dev/null
+++ b/frontend/src/views/Notification.vue
@@ -0,0 +1,342 @@
+
+
+
+ {{ $t('notification.loading') }}
+
+
+
+ {{ $t('notification.tokenInvalid') }}
+
+
+
+
+
+
+
+
{{ notification.title }}
+
+
+
{{ $t('notification.latchTime', { latchTime: formatDateTime(notification.latchTime) }) }}
+
{{ $t('notification.latched') }}
+
+
+
+
+
+
{{ $t('notification.reminders', { interval: formattedReminderInterval })}}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/notification/facade.js b/src/notification/facade.js
index 2ad2168..4fb245f 100644
--- a/src/notification/facade.js
+++ b/src/notification/facade.js
@@ -22,6 +22,9 @@ class NotificationFacade
return true;
});
+
+ this.publicUrl = config.publicUrl;
+ this.reminders = config.reminders;
}
@@ -41,7 +44,7 @@ class NotificationFacade
priority: priority,
sound: parsedSubject.sound,
timestamp: this.dateTimeProvider.unixTimestamp(),
- url: 'https://www.hierhaduwurlkunnenstaan.nl/'
+ url: new URL('/#/n/' + token, this.publicUrl).href
});
}
@@ -53,6 +56,28 @@ class NotificationFacade
+ getLatchedNotifications(token)
+ {
+ const notifications = this.notificationRepository.getLatchedNotifications(token);
+
+ return notifications !== null
+ ? {
+ reminders: {
+ enabled: this.reminders.enabled,
+ interval: this.reminders.interval
+ },
+ notifications: notifications
+ }
+ : null;
+ }
+
+
+ async setReminders(token, enabled)
+ {
+ await this.notificationRepository.setReminders(token, enabled);
+ }
+
+
_sendNotification(notification)
{
this.contacts.forEach(contact =>
diff --git a/src/notification/repository.js b/src/notification/repository.js
index f06cadd..489a24a 100644
--- a/src/notification/repository.js
+++ b/src/notification/repository.js
@@ -107,6 +107,57 @@ class NotificationRepository
}
+ getLatchedNotifications(token)
+ {
+ this._checkInitialized();
+
+ if (!this.notifications.hasOwnProperty(token))
+ return null;
+
+ return Object.keys(this.notifications)
+ .filter(notificationToken => this.notifications[notificationToken].latched)
+ .map(notificationToken => {
+ const notification = this.notifications[notificationToken];
+
+ return {
+ token: notificationToken,
+ id: notification.id,
+ title: notification.title,
+ latched: notification.latched,
+ latchTime: notification.latchTime,
+ resetTime: notification.resetTime,
+ reminders: notification.reminders,
+ remindTime: notification.remindTime
+ };
+ });
+ }
+
+
+ async setReminders(token, enabled)
+ {
+ this._checkInitialized();
+
+ if (!this.notifications.hasOwnProperty(token))
+ {
+ this.logger.info(`Notification token '${token}' does not exist, unable to change reminders setting`);
+ return;
+ }
+
+ const notification = this.notifications[token];
+
+ if (notification.reminders == enabled)
+ {
+ this.logger.info(`Notification with id '${notification.id}' and token '${token}' reminders is already ${enabled}`);
+ return;
+ }
+
+ this.logger.info(`Setting reminders for notification with id '${notification.id}' and token '${token}' to ${enabled}`);
+ notification.reminders = enabled;
+
+ await this._flush();
+ }
+
+
_getNotificationToken(id)
{
const hasher = crypto.createHmac("sha256", this.salt);
diff --git a/src/routes/api.js b/src/routes/api.js
index 43229a0..e948553 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -1,6 +1,6 @@
class ApiRoutes
{
- static create = container => new this(container.logger, container.NotificationFacade, container.Config);
+ static create = container => new this(container.Logger, container.NotificationFacade, container.Config);
constructor(logger, notificationFacade, config)
@@ -15,8 +15,11 @@ class ApiRoutes
{
const router = express.Router();
- router.post('/notification', (req, res) => this._wrapAsyncHandler(req, res, this._handlePostNotification.bind(this)));
- router.post('/notification/reset', (req, res) => this._wrapAsyncHandler(req, res, this._handleResetNotification.bind(this)));
+ router.post('/notification', this._wrapAsyncHandler(this._handlePostNotification));
+ router.get('/notification/latched', this._wrapAsyncHandler(this._handleLatchedNotifications));
+ router.post('/notification/:token/reset', this._wrapAsyncHandler(this._handleResetNotification));
+ router.post('/notification/:token/reminders/enable', this._wrapAsyncHandler(this._handleEnableReminders));
+ router.post('/notification/:token/reminders/disable', this._wrapAsyncHandler(this._handleDisableReminders));
return router;
}
@@ -45,29 +48,64 @@ class ApiRoutes
async _handleResetNotification(req, res)
{
- if (!req.body || !req.body.token)
- {
- this._logRequestWarning(req, 'Missing token');
- res.sendStatus(400);
- return;
- }
-
- await this.notificationFacade.resetNotification(req.body.token);
+ await this.notificationFacade.resetNotification(req.params.token);
res.sendStatus(200);
}
- async _wrapAsyncHandler(req, res, handler)
+ async _handleEnableReminders(req, res)
{
- try
+ await this.notificationFacade.setReminders(req.params.token, true);
+ res.sendStatus(200);
+ }
+
+
+ async _handleDisableReminders(req, res)
+ {
+ await this.notificationFacade.setReminders(req.params.token, false);
+ res.sendStatus(200);
+ }
+
+
+ async _handleLatchedNotifications(req, res)
+ {
+ if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer '))
{
- await handler(req, res);
+ this._logRequestWarning(req, 'Missing or invalid authorization header');
+ res.sendStatus(401);
+ return;
}
- catch (err)
+
+ const token = req.headers.authorization.substr(7);
+ const notifications = this.notificationFacade.getLatchedNotifications(token);
+
+ if (notifications == null)
{
- this.logger.error(`Unhandled exception in request handler: ${err}`);
- this.logger.verbose(err.stack);
- res.sendStatus(500);
+ this._logRequestWarning(req, `Invalid token: ${token}`);
+ res.sendStatus(401);
+ return;
+ }
+
+ res.send(JSON.stringify(notifications));
+ }
+
+
+ _wrapAsyncHandler(handler)
+ {
+ const boundHandler = handler.bind(this);
+
+ return async (req, res) =>
+ {
+ try
+ {
+ await boundHandler(req, res);
+ }
+ catch (err)
+ {
+ this.logger.error(`Unhandled exception in request handler: ${err}`);
+ this.logger.verbose(err.stack);
+ res.sendStatus(500);
+ }
}
}