add notifications
This commit is contained in:
parent
0f2bfbb6a0
commit
74ad2750b0
@ -4,4 +4,6 @@ JWT_SECRET="" ##random number string
|
|||||||
NEXTCLOUD_URL=
|
NEXTCLOUD_URL=
|
||||||
NEXTCLOUD_USER=
|
NEXTCLOUD_USER=
|
||||||
NEXTCLOUD_APP_PASSWORD=
|
NEXTCLOUD_APP_PASSWORD=
|
||||||
NEXTCLOUD_CALENDAR_URL=
|
NEXTCLOUD_CALENDAR_URL=
|
||||||
|
publicVapidKey="YOUR_PUBLIC_VAPID_KEY"
|
||||||
|
privateVapidKey="YOUR_PRIVATE_VAPID_KEY"
|
||||||
@ -11,6 +11,7 @@
|
|||||||
"pm2": "^6.0.8",
|
"pm2": "^6.0.8",
|
||||||
"sqlite": "^5.1.1",
|
"sqlite": "^5.1.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"tailwindcss": "^4.1.11"
|
"tailwindcss": "^4.1.11",
|
||||||
|
"web-push": "^3.6.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -388,6 +388,70 @@ function setupTabbedInterface() {
|
|||||||
document.getElementById(`tab-content-${tabTarget}`).classList.remove('hidden');
|
document.getElementById(`tab-content-${tabTarget}`).classList.remove('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// This converts the VAPID public key string to the format the browser needs.
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Subscription Logic ---
|
||||||
|
async function subscribeToNotifications() {
|
||||||
|
// IMPORTANT: Replace with your actual public VAPID key
|
||||||
|
const publicVapidKey = 'YOUR_PUBLIC_VAPID_KEY';
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Check if user is logged in first
|
||||||
|
if (!token) {
|
||||||
|
console.error('User is not logged in.');
|
||||||
|
alert('You must be logged in to enable notifications.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Register the service worker
|
||||||
|
const register = await navigator.serviceWorker.register('/sw.js', {
|
||||||
|
scope: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Get the push subscription
|
||||||
|
const subscription = await register.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Send the subscription to your authenticated backend
|
||||||
|
await fetch('/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}` // The crucial update!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alert('You are now subscribed to notifications!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Attach to a Button ---
|
||||||
|
// Assuming you have a button in your HTML with id="notifyBtn"
|
||||||
|
const notifyBtn = document.getElementById('notifyBtn');
|
||||||
|
if (notifyBtn) {
|
||||||
|
notifyBtn.addEventListener('click', () => {
|
||||||
|
subscribeToNotifications().catch(error => {
|
||||||
|
console.error('Error subscribing to notifications:', error);
|
||||||
|
alert('Failed to subscribe. Please make sure notifications are allowed for this site.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- START THE APP ---
|
// --- START THE APP ---
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
120
server.js
120
server.js
@ -8,12 +8,14 @@ const cors = require('cors');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const ics = require('ics');
|
const ics = require('ics');
|
||||||
|
const webpush = require('web-push');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'default_secret_key';
|
const JWT_SECRET = process.env.JWT_SECRET || 'default_secret_key';
|
||||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
|
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'adminpassword';
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'adminpassword';
|
||||||
|
const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
|
||||||
|
const privateVapidKey = process.env.PRIVATE_VAPID_KEY;
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -35,6 +37,12 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webpush.setVapidDetails(
|
||||||
|
'mailto:utility@beachpartyballoons.com', // A contact email
|
||||||
|
publicVapidKey,
|
||||||
|
privateVapidKey
|
||||||
|
);
|
||||||
|
|
||||||
async function initializeDatabase() {
|
async function initializeDatabase() {
|
||||||
console.log("Initializing database schema...");
|
console.log("Initializing database schema...");
|
||||||
await db.exec(`CREATE TABLE IF NOT EXISTS users (
|
await db.exec(`CREATE TABLE IF NOT EXISTS users (
|
||||||
@ -71,6 +79,12 @@ async function initializeDatabase() {
|
|||||||
status TEXT NOT NULL DEFAULT 'pending',
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
)`);
|
)`);
|
||||||
|
await db.exec(`CREATE TABLE IF NOT EXISTS subscriptions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
subscription_object TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
)`);
|
||||||
await db.exec(`CREATE TABLE IF NOT EXISTS notes (
|
await db.exec(`CREATE TABLE IF NOT EXISTS notes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
admin_username TEXT NOT NULL,
|
admin_username TEXT NOT NULL,
|
||||||
@ -186,7 +200,28 @@ function setupRoutes() {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
app.post('/subscribe', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const subscription = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// Save the subscription to the database, linked to the user
|
||||||
|
await db.run(
|
||||||
|
'INSERT INTO subscriptions (user_id, subscription_object) VALUES (?, ?)',
|
||||||
|
[userId, JSON.stringify(subscription)]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({ message: 'Subscribed successfully.' });
|
||||||
|
|
||||||
|
// Optional: Send a welcome notification
|
||||||
|
const payload = JSON.stringify({ title: 'Time Pulse', body: 'You are now subscribed to notifications!' });
|
||||||
|
webpush.sendNotification(subscription, payload).catch(err => console.error("Error sending welcome notification:", err));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save subscription:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to subscribe.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
const requireRole = (role) => (req, res, next) => {
|
const requireRole = (role) => (req, res, next) => {
|
||||||
if (req.user && req.user.role === role) next();
|
if (req.user && req.user.role === role) next();
|
||||||
else res.status(403).json({ message: "Access denied." });
|
else res.status(403).json({ message: "Access denied." });
|
||||||
@ -255,6 +290,33 @@ function setupRoutes() {
|
|||||||
const { startDate, endDate, reason } = req.body;
|
const { startDate, endDate, reason } = req.body;
|
||||||
const { id, username } = req.user;
|
const { id, username } = req.user;
|
||||||
await db.run(`INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`, [id, username, startDate, endDate, reason]);
|
await db.run(`INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`, [id, username, startDate, endDate, reason]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adminSubs = await db.all(`
|
||||||
|
SELECT s.subscription_object
|
||||||
|
FROM subscriptions s
|
||||||
|
JOIN users u ON s.user_id = u.id
|
||||||
|
WHERE u.role = 'admin'
|
||||||
|
`);
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: 'New Time-Off Request',
|
||||||
|
body: `${username} has requested time off.`
|
||||||
|
});
|
||||||
|
|
||||||
|
const promises = adminSubs.map(s => {
|
||||||
|
const subscription = JSON.parse(s.subscription_object);
|
||||||
|
return webpush.sendNotification(subscription, payload).catch(err => {
|
||||||
|
if (err.statusCode === 410) db.run('DELETE FROM subscriptions WHERE subscription_object = ?', [s.subscription_object]);
|
||||||
|
else console.error('Error sending admin notification:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (notifyError) {
|
||||||
|
console.error('Failed to send admin notifications:', notifyError);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({ message: "Time off request submitted." });
|
res.status(201).json({ message: "Time off request submitted." });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ message: "Failed to submit request." });
|
res.status(500).json({ message: "Failed to submit request." });
|
||||||
@ -282,6 +344,27 @@ function setupRoutes() {
|
|||||||
app.get('/api/user/notes', authenticateToken, async (req, res) => {
|
app.get('/api/user/notes', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const notes = await db.all("SELECT admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC", [req.user.id]);
|
const notes = await db.all("SELECT admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC", [req.user.id]);
|
||||||
|
try {
|
||||||
|
const userSubs = await db.all('SELECT subscription_object FROM subscriptions WHERE user_id = ?', [userId]);
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: 'You Have a New Note',
|
||||||
|
body: `A new note has been added by ${adminUsername}.`
|
||||||
|
});
|
||||||
|
|
||||||
|
const promises = userSubs.map(s => {
|
||||||
|
const subscription = JSON.parse(s.subscription_object);
|
||||||
|
return webpush.sendNotification(subscription, payload).catch(err => {
|
||||||
|
if (err.statusCode === 410) db.run('DELETE FROM subscriptions WHERE subscription_object = ?', [s.subscription_object]);
|
||||||
|
else console.error('Error sending employee notification:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (notifyError) {
|
||||||
|
console.error('Failed to send employee notification:', notifyError);
|
||||||
|
}
|
||||||
|
|
||||||
res.json(notes);
|
res.json(notes);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ message: 'Failed to fetch notes.' });
|
res.status(500).json({ message: 'Failed to fetch notes.' });
|
||||||
@ -397,7 +480,38 @@ function setupRoutes() {
|
|||||||
res.status(500).json({ message: "Server error forcing clock out." });
|
res.status(500).json({ message: "Server error forcing clock out." });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// server.js - Inside setupRoutes(), with other admin routes
|
||||||
|
app.post('/api/admin/notify', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId, title, message } = req.body;
|
||||||
|
|
||||||
|
// Get all subscriptions for the target user
|
||||||
|
const subs = await db.all('SELECT subscription_object FROM subscriptions WHERE user_id = ?', [userId]);
|
||||||
|
if (subs.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'User has no active notification subscriptions.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.stringify({ title: title, body: message });
|
||||||
|
|
||||||
|
// Send a notification to each subscription
|
||||||
|
const promises = subs.map(s => {
|
||||||
|
const subscription = JSON.parse(s.subscription_object);
|
||||||
|
return webpush.sendNotification(subscription, payload).catch(err => {
|
||||||
|
// If a subscription is expired (410 Gone), delete it from the DB
|
||||||
|
if (err.statusCode === 410) {
|
||||||
|
db.run('DELETE FROM subscriptions WHERE subscription_object = ?', [s.subscription_object]);
|
||||||
|
}
|
||||||
|
console.error('Error sending notification, user ID:', userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
res.json({ message: 'Notification sent successfully.' });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'Failed to send notification.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => {
|
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId, username, punchInTime, punchOutTime } = req.body;
|
const { userId, username, punchInTime, punchOutTime } = req.body;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user