6 Commits

10 changed files with 135 additions and 67 deletions

View File

@@ -2,6 +2,7 @@ version: '3.8'
services: services:
org-todo-pwa: org-todo-pwa:
container_name: org-todo-pwa
build: . build: .
ports: ports:
- "3044:3044" - "3044:3044"

View File

@@ -4,13 +4,13 @@ const logger = createLogger({
level: 'info', level: 'info',
format: format.combine( format: format.combine(
format.timestamp(), format.timestamp(),
format.json() format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
), ),
transports: [ transports: [
new transports.Console(), new transports.Console(),
new transports.File({ filename: 'app.log' }) new transports.File({ filename: '/data/app.log' })
] ]
}); });
module.exports = logger; module.exports = logger;

View File

@@ -1,10 +1,12 @@
const basicAuth = require('basic-auth'); const basicAuth = require('basic-auth');
const logger = require('../logger');
const auth = (req, res, next) => { const auth = (req, res, next) => {
if (req.session && req.session.user) { if (req.session && req.session.user) {
return next(); return next();
} else { } else {
res.status(401).send('Authentication required.'); res.status(401).send('Authentication required.');
logger.error(`Unauthorized access attempted from IP: ${req.ip}`);
} }
}; };

View File

@@ -56,7 +56,7 @@
<label for="time">Tid (valfritt)</label> <label for="time">Tid (valfritt)</label>
</div> </div>
<div class="input-field"> <div class="input-field">
<input type="text" id="tags" placeholder="Taggar"> <input type="text" id="tags" placeholder="Taggar" autocomplete="off">
<label for="tags">Taggar</label> <label for="tags">Taggar</label>
</div> </div>
<button class="btn waves-effect waves-light" type="submit">Spara uppgift</button> <button class="btn waves-effect waves-light" type="submit">Spara uppgift</button>

View File

@@ -1,34 +1,39 @@
import { checkSession, login, logout } from './auth.js'; import { checkSession, login, logout } from './auth.js';
import { saveTask } from './tasks.js'; import { saveTask } from './tasks.js';
import { saveTags, loadTags } from './tags.js'; import { saveTags, loadTags } from './tags.js';
import { idb } from './utils.js';
navigator.serviceWorker.register('/service-worker.js') if ('serviceWorker' in navigator) {
.then(registration => { window.addEventListener('load', () => {
console.log('ServiceWorker registration successful with scope: ', registration.scope); navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// New update available // New update available
console.log('New content is available; please refresh.'); console.log('New content is available; please refresh.');
if (confirm('New version available. Do you want to update?')) { if (confirm('New version available. Do you want to update?')) {
window.location.reload(); window.location.reload();
}
} else {
// Content is cached for offline use
console.log('Content is cached for offline use.');
}
} }
} else { };
// Content is cached for offline use };
console.log('Content is cached for offline use.'); })
} .catch(error => {
} console.log('ServiceWorker registration failed: ', error);
}; });
}; });
}) }
.catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', async function() {
const loginForm = document.getElementById('loginForm'); const loginForm = document.getElementById('loginForm');
const loginContainer = document.getElementById('loginContainer'); const loginContainer = document.getElementById('loginContainer');
const appContainer = document.getElementById('appContainer'); const appContainer = document.getElementById('appContainer');
@@ -50,12 +55,13 @@ document.addEventListener('DOMContentLoaded', function() {
const tomorrow = new Date(today); const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1); tomorrow.setDate(today.getDate() + 1);
// Initialize datepicker with tomorrow as the default date // Initialize datepicker with tomorrow as the default date and disable past dates
M.Datepicker.init(document.querySelectorAll('.datepicker'), { M.Datepicker.init(document.querySelectorAll('.datepicker'), {
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
defaultDate: tomorrow, defaultDate: tomorrow,
setDefaultDate: true, setDefaultDate: true,
firstDay: 1 firstDay: 1,
minDate: today // Disable past dates
}); });
// Initialize timepicker // Initialize timepicker
@@ -182,7 +188,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Save task to server or IndexedDB if offline // Save task to server or IndexedDB if offline
try { try {
const data = await saveTask(taskData); const data = await saveTask(taskData);
document.getElementById('responseMessage').textContent = "Task saved successfully!"; document.getElementById('responseMessage').textContent = data.message;
taskForm.reset(); // Reset the form after saving the task taskForm.reset(); // Reset the form after saving the task
} catch (error) { } catch (error) {
if (error.status === 401) { if (error.status === 401) {
@@ -194,4 +200,19 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
}); });
// Synchronize tasks when back online
window.addEventListener('online', async () => {
const db = await idb.openDB('org-todo-pwa', 1);
const tasks = await db.getAll('tasks');
for (const task of tasks) {
try {
await saveTask(task);
await db.delete('tasks', task.id);
console.log(`Task synchronized: ${task.subject}`);
} catch (error) {
console.error('Error synchronizing task:', error);
}
}
});
}); });

View File

@@ -1,10 +1,13 @@
export function saveTags(tags) { export async function saveTags(newTags) {
const existingTags = await loadTags();
const allTags = Array.from(new Set([...existingTags, ...newTags]));
return fetch('/save-tags', { return fetch('/save-tags', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ tags }) body: JSON.stringify({ tags: allTags })
}); });
} }

View File

@@ -2,9 +2,14 @@ const CACHE_NAME = 'org-todo-pwa-cache-v1';
const urlsToCache = [ const urlsToCache = [
'/', '/',
'/index.html', '/index.html',
'/style.css', '/css/style.css',
'/app.js', '/js/auth.js',
'/manifest.json' '/js/tasks.js',
'/js/tags.js',
'/js/utils.js',
'/js/main.js',
'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js'
]; ];
self.addEventListener('install', event => { self.addEventListener('install', event => {
@@ -14,22 +19,21 @@ self.addEventListener('install', event => {
return cache.addAll(urlsToCache); return cache.addAll(urlsToCache);
}) })
); );
self.skipWaiting(); // Force the waiting service worker to become the active service worker
}); });
self.addEventListener('activate', event => { self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil( event.waitUntil(
caches.keys().then(cacheNames => { caches.keys().then(cacheNames => {
return Promise.all( return Promise.all(
cacheNames.map(cacheName => { cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) { if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName); return caches.delete(cacheName);
} }
}) })
); );
}) })
); );
self.clients.claim(); // Take control of all clients immediately
}); });
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {
@@ -39,13 +43,19 @@ self.addEventListener('fetch', event => {
if (response) { if (response) {
return response; return response;
} }
return fetch(event.request); return fetch(event.request).then(
response => {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
}
);
}) })
); );
}); });
self.addEventListener('message', event => {
if (event.data === 'skipWaiting') {
self.skipWaiting();
}
});

View File

@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const basicAuth = require('basic-auth'); const basicAuth = require('basic-auth');
const logger = require('../logger');
const router = express.Router(); const router = express.Router();
router.post('/login', (req, res) => { router.post('/login', (req, res) => {
@@ -10,6 +11,7 @@ router.post('/login', (req, res) => {
if (user && user.name === username && user.pass === password) { if (user && user.name === username && user.pass === password) {
req.session.user = user.name; req.session.user = user.name;
res.status(200).send('Login successful'); res.status(200).send('Login successful');
logger.info(`User ${user.name} logged in`);
} else { } else {
res.status(401).send('Authentication required'); res.status(401).send('Authentication required');
} }

View File

@@ -3,10 +3,17 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const auth = require('../middleware/auth'); const auth = require('../middleware/auth');
const logger = require('../logger'); const logger = require('../logger');
const sqlite3 = require('sqlite3').verbose();
const router = express.Router(); const router = express.Router();
const dataDir = '/data'; const dataDir = '/data';
const db = new sqlite3.Database('/data/sessions.sqlite', (err) => {
if (err) {
console.error('Error opening database:', err);
}
});
// Ensure the /data directory exists // Ensure the /data directory exists
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true }); fs.mkdirSync(dataDir, { recursive: true });
@@ -58,6 +65,7 @@ router.post('/add-task', auth, async (req, res) => {
try { try {
await fs.promises.appendFile(filePath, orgFormattedData); await fs.promises.appendFile(filePath, orgFormattedData);
res.json({ message: 'Task added successfully' }); res.json({ message: 'Task added successfully' });
logger.info(`Task added: ${orgFormattedData}`);
} catch (error) { } catch (error) {
logger.error('Error writing to tasks.org file:', error); logger.error('Error writing to tasks.org file:', error);
res.status(500).json({ message: 'Error adding task' }); res.status(500).json({ message: 'Error adding task' });
@@ -67,27 +75,31 @@ router.post('/add-task', auth, async (req, res) => {
// Endpoint to save tags // Endpoint to save tags
router.post('/save-tags', auth, async (req, res) => { router.post('/save-tags', auth, async (req, res) => {
const { tags } = req.body; const { tags } = req.body;
const filePath = path.join(dataDir, 'tags.json'); const placeholders = tags.map(() => '(?)').join(',');
try { const sql = `INSERT OR IGNORE INTO tags (tag) VALUES ${placeholders}`;
await fs.promises.writeFile(filePath, JSON.stringify(tags));
res.send({ message: 'Tags saved successfully!' }); db.run(sql, tags, function(err) {
} catch (err) { if (err) {
logger.error('Error saving tags:', err); logger.error('Error saving tags:', err);
res.status(500).send('Error saving tags.'); res.status(500).send('Error saving tags.');
} } else {
res.send({ message: 'Tags saved successfully!' });
logger.info(`New tags saved: ${tags}`);
}
});
}); });
// Endpoint to retrieve tags // Endpoint to retrieve tags
router.get('/get-tags', auth, async (req, res) => { router.get('/get-tags', auth, async (req, res) => {
const filePath = path.join(dataDir, 'tags.json'); db.all('SELECT tag FROM tags', [], (err, rows) => {
try { if (err) {
const data = await fs.promises.readFile(filePath, 'utf-8'); logger.error('Error retrieving tags:', err);
const tags = JSON.parse(data); res.status(500).json({ error: 'Error retrieving tags' });
res.json(tags); } else {
} catch (err) { const tags = rows.map(row => row.tag);
logger.error('Error retrieving tags:', err); res.json(tags);
res.status(500).json({ error: 'Error retrieving tags' }); }
} });
}); });
module.exports = router; module.exports = router;

View File

@@ -4,14 +4,30 @@ const bodyParser = require('body-parser');
const session = require('express-session'); const session = require('express-session');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const SQLiteStore = require('connect-sqlite3')(session); const SQLiteStore = require('connect-sqlite3')(session);
const debug = require('debug')('app'); const sqlite3 = require('sqlite3').verbose();
const tasksRouter = require('./routes/tasks'); const tasksRouter = require('./routes/tasks');
const authRouter = require('./routes/auth'); const authRouter = require('./routes/auth');
const authMiddleware = require('./middleware/auth'); const authMiddleware = require('./middleware/auth');
const logger = require('./logger');
const app = express(); const app = express();
const port = 3044; const port = 3044;
const db = new sqlite3.Database('/data/sessions.sqlite', (err) => {
if (err) {
console.error('Error opening database:', err);
} else {
db.run(`CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tag TEXT UNIQUE
)`, (err) => {
if (err) {
console.error('Error creating tags table:', err);
}
});
}
});
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static('public')); app.use(express.static('public'));
@@ -27,8 +43,7 @@ app.use(session({
ttl: 30 * 24 * 60 * 60 // 1 month ttl: 30 * 24 * 60 * 60 // 1 month
}), }),
cookie: { cookie: {
//secure: process.env.NODE_ENV === 'production', // Ensure cookies are only sent over HTTPS in production secure: process.env.NODE_ENV === 'production', // Ensure cookies are only sent over HTTPS in production
secure: false,
maxAge: 30 * 24 * 60 * 60 * 1000 // 1 month maxAge: 30 * 24 * 60 * 60 * 1000 // 1 month
} }
})); }));
@@ -36,6 +51,8 @@ app.use(session({
app.use('/', authRouter); app.use('/', authRouter);
app.use('/', authMiddleware, tasksRouter); app.use('/', authMiddleware, tasksRouter);
app.listen(port, () => { app.listen(port, () => {
debug(`Server running at http://localhost:${port}`); logger.info(`Server running at http://localhost:${port}`);
}); });