Compare commits
6 Commits
2ebf92a5d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc8e81e97 | |||
| 37a7e5e67a | |||
| c98aea1b06 | |||
| b277b1f8f0 | |||
| 2a7005f53c | |||
| 23a6d9b10f |
@@ -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"
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ 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' })
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
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) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
.then(registration => {
|
.then(registration => {
|
||||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||||
|
|
||||||
@@ -27,8 +30,10 @@ navigator.serviceWorker.register('/service-worker.js')
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log('ServiceWorker registration failed: ', 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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 })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
const tags = JSON.parse(data);
|
|
||||||
res.json(tags);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error retrieving tags:', err);
|
logger.error('Error retrieving tags:', err);
|
||||||
res.status(500).json({ error: 'Error retrieving tags' });
|
res.status(500).json({ error: 'Error retrieving tags' });
|
||||||
|
} else {
|
||||||
|
const tags = rows.map(row => row.tag);
|
||||||
|
res.json(tags);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
25
server.js
25
server.js
@@ -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}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user