Compare commits
8 Commits
material-d
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
| adbe5fcfb4 | |||
| a58da5398c | |||
| e091da93e9 | |||
| 70365715fe | |||
| 6c72c02896 | |||
| eb5adae406 | |||
| 892c20dfae | |||
| d75a0882dd |
@@ -16,5 +16,8 @@ COPY . .
|
|||||||
# Expose the port the app runs on
|
# Expose the port the app runs on
|
||||||
EXPOSE 3044
|
EXPOSE 3044
|
||||||
|
|
||||||
|
# Set the DEBUG environment variable
|
||||||
|
ENV DEBUG=app
|
||||||
|
|
||||||
# Command to run the application
|
# Command to run the application
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
@@ -9,6 +9,15 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /srv/swarm/org-todo-pwa/data:/data
|
- /srv/swarm/org-todo-pwa/data:/data
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
- DEBUG=app
|
||||||
|
- AUTH_USERNAME=fredrik
|
||||||
|
- AUTH_PASSWORD=apa
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3044"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -16,3 +25,4 @@ services:
|
|||||||
- "traefik.http.routers.plan.tls.certresolver=myhttpchallenge"
|
- "traefik.http.routers.plan.tls.certresolver=myhttpchallenge"
|
||||||
- "traefik.http.routers.plan.rule=Host(`todo.casablanca.wahlberg.se`)"
|
- "traefik.http.routers.plan.rule=Host(`todo.casablanca.wahlberg.se`)"
|
||||||
- "traefik.http.routers.plan.entrypoints=websecure"
|
- "traefik.http.routers.plan.entrypoints=websecure"
|
||||||
|
|
||||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
98
logger.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const { createLogger, format, transports } = require('winston');
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp(),
|
||||||
|
format.json()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new transports.Console(),
|
||||||
|
new transports.File({ filename: 'app.log' })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = logger;
|
||||||
|
|
||||||
|
// filepath: /home/fredrik/dev/org-todo-pwa/routes/tasks.js
|
||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('../logger');
|
||||||
|
const auth = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const dataDir = '/data';
|
||||||
|
|
||||||
|
// Ensure the /data directory exists
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the tags.json file exists
|
||||||
|
const tagsFilePath = path.join(dataDir, 'tags.json');
|
||||||
|
if (!fs.existsSync(tagsFilePath)) {
|
||||||
|
fs.writeFileSync(tagsFilePath, JSON.stringify([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect the /add-task endpoint with authentication
|
||||||
|
router.post('/add-task', auth, async (req, res) => {
|
||||||
|
const { subject, description, scheduled } = req.body;
|
||||||
|
const currentDateTime = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||||
|
|
||||||
|
let orgFormattedData = `
|
||||||
|
* TODO ${subject}
|
||||||
|
SCHEDULED: <${scheduled}>
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "TODO" from "TODO" [${currentDateTime}]
|
||||||
|
:END:
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
orgFormattedData = `
|
||||||
|
* TODO ${subject}
|
||||||
|
${description}
|
||||||
|
SCHEDULED: <${scheduled}>
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "TODO" from "TODO" [${currentDateTime}]
|
||||||
|
:END:
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(dataDir, 'tasks.org');
|
||||||
|
try {
|
||||||
|
await fs.promises.appendFile(filePath, orgFormattedData);
|
||||||
|
res.send({ message: 'Task added successfully!' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error writing to file:', err);
|
||||||
|
res.status(500).send('Error writing to file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint to save tags
|
||||||
|
router.post('/save-tags', auth, async (req, res) => {
|
||||||
|
const { tags } = req.body;
|
||||||
|
const filePath = path.join(dataDir, 'tags.json');
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(tags));
|
||||||
|
res.send({ message: 'Tags saved successfully!' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error saving tags:', err);
|
||||||
|
res.status(500).send('Error saving tags.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint to retrieve tags
|
||||||
|
router.get('/get-tags', auth, async (req, res) => {
|
||||||
|
const filePath = path.join(dataDir, 'tags.json');
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(filePath);
|
||||||
|
const tags = JSON.parse(data);
|
||||||
|
res.send(tags);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error retrieving tags:', err);
|
||||||
|
res.status(500).send('Error retrieving tags.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
16
middleware/auth.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const basicAuth = require('basic-auth');
|
||||||
|
|
||||||
|
const auth = (req, res, next) => {
|
||||||
|
const user = basicAuth(req);
|
||||||
|
const username = process.env.AUTH_USERNAME; // Use environment variables
|
||||||
|
const password = process.env.AUTH_PASSWORD; // Use environment variables
|
||||||
|
|
||||||
|
if (user && user.name === username && user.pass === password) {
|
||||||
|
return next();
|
||||||
|
} else {
|
||||||
|
res.set('WWW-Authenticate', 'Basic realm="401"');
|
||||||
|
return res.status(401).send('Authentication required.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = auth;
|
||||||
92
package-lock.json
generated
@@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"fs": "^0.0.1-security"
|
"fs": "^0.0.1-security"
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,19 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -139,11 +154,19 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.0.0"
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
@@ -163,6 +186,17 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
|
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -274,6 +308,19 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
@@ -291,6 +338,19 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/finalhandler/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -490,9 +550,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
@@ -632,6 +692,19 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/send/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/debug/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
|
},
|
||||||
"node_modules/send/node_modules/encodeurl": {
|
"node_modules/send/node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
@@ -640,11 +713,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
|
||||||
},
|
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.16.2",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"fs": "^0.0.1-security"
|
"fs": "^0.0.1-security"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -10,21 +10,68 @@ if ('serviceWorker' in navigator) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('taskForm').addEventListener('submit', function(e) {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const loginContainer = document.getElementById('loginContainer');
|
||||||
|
const appContainer = document.getElementById('appContainer');
|
||||||
|
const loginMessage = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
// Check if user is already logged in
|
||||||
|
if (sessionStorage.getItem('loggedIn') === 'true') {
|
||||||
|
loginContainer.style.display = 'none';
|
||||||
|
appContainer.style.display = 'block';
|
||||||
|
loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
// Simple authentication check (replace with your own logic)
|
||||||
|
if (username === 'fredrik' && password === 'apa') {
|
||||||
|
sessionStorage.setItem('loggedIn', 'true');
|
||||||
|
loginContainer.style.display = 'none';
|
||||||
|
appContainer.style.display = 'block';
|
||||||
|
loadTags();
|
||||||
|
} else {
|
||||||
|
loginMessage.textContent = 'Invalid username or password';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('taskForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Get form values
|
// Get form values
|
||||||
const subject = document.getElementById('subject').value;
|
const subject = document.getElementById('subject').value;
|
||||||
const description = document.getElementById('description').value;
|
const description = document.getElementById('description').value;
|
||||||
const scheduled = document.getElementById('scheduled').value;
|
const scheduled = document.getElementById('scheduled').value;
|
||||||
|
const tagsInput = document.getElementById('tags').value;
|
||||||
|
const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag).join(':');
|
||||||
|
|
||||||
// Structure data for Org mode
|
// Structure data for Org mode
|
||||||
const taskData = {
|
const taskData = {
|
||||||
subject,
|
subject: `${subject} :${tags}:`,
|
||||||
description,
|
description,
|
||||||
scheduled
|
scheduled
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save tags to server
|
||||||
|
const savedTags = JSON.parse(localStorage.getItem('tags')) || [];
|
||||||
|
const newTags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag && !savedTags.includes(tag));
|
||||||
|
const allTags = [...savedTags, ...newTags];
|
||||||
|
localStorage.setItem('tags', JSON.stringify(allTags));
|
||||||
|
fetch('/save-tags', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ tags: allTags })
|
||||||
|
}).then(() => {
|
||||||
|
loadTags(); // Force refresh tags after saving
|
||||||
|
});
|
||||||
|
|
||||||
// Send data to backend using fetch
|
// Send data to backend using fetch
|
||||||
fetch('/add-task', {
|
fetch('/add-task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -40,4 +87,46 @@ document.getElementById('taskForm').addEventListener('submit', function(e) {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
document.getElementById('responseMessage').textContent = "Error saving task!";
|
document.getElementById('responseMessage').textContent = "Error saving task!";
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set tomorrow's date as the default for the date input
|
||||||
|
const today = new Date();
|
||||||
|
const tomorrow = new Date(today);
|
||||||
|
tomorrow.setDate(today.getDate() + 1);
|
||||||
|
const tomorrowString = tomorrow.toISOString().split('T')[0];
|
||||||
|
document.getElementById('scheduled').value = tomorrowString;
|
||||||
|
|
||||||
|
// Initialize flatpickr with Swedish locale and Monday as the first day of the week
|
||||||
|
flatpickr("#scheduled", {
|
||||||
|
weekNumbers: true, // Show week numbers
|
||||||
|
firstDayOfWeek: 1 // Start weeks on Monday
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load tags from server and initialize autocomplete
|
||||||
|
function loadTags() {
|
||||||
|
fetch('/get-tags')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(tags => {
|
||||||
|
localStorage.setItem('tags', JSON.stringify(tags));
|
||||||
|
const autocompleteData = {};
|
||||||
|
tags.forEach(tag => {
|
||||||
|
autocompleteData[tag] = null; // Materialize autocomplete requires a key-value pair
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagsInput = document.getElementById('tags');
|
||||||
|
M.Autocomplete.init(tagsInput, {
|
||||||
|
data: autocompleteData,
|
||||||
|
onAutocomplete: function(selectedTag) {
|
||||||
|
const currentTags = tagsInput.value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||||
|
if (!currentTags.includes(selectedTag)) {
|
||||||
|
currentTags.push(selectedTag);
|
||||||
|
tagsInput.value = currentTags.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading tags:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 249 B After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -13,7 +13,23 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div id="loginContainer" class="container">
|
||||||
|
<h1 class="center-align">Login</h1>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="input-field">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn waves-effect waves-light" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<p id="loginMessage" class="center-align"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="appContainer" class="container" style="display:none;">
|
||||||
<h1 class="center-align">TODO</h1>
|
<h1 class="center-align">TODO</h1>
|
||||||
<form id="taskForm">
|
<form id="taskForm">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@@ -31,6 +47,11 @@
|
|||||||
<input type="date" id="scheduled" lang="sv-SE" required>
|
<input type="date" id="scheduled" lang="sv-SE" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="input-field">
|
||||||
|
<label for="tags">Tags (separated by commas):</label>
|
||||||
|
<input type="text" id="tags" class="autocomplete">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn waves-effect waves-light" type="submit">Spara</button>
|
<button class="btn waves-effect waves-light" type="submit">Spara</button>
|
||||||
</form>
|
</form>
|
||||||
<p id="responseMessage" class="center-align"></p>
|
<p id="responseMessage" class="center-align"></p>
|
||||||
@@ -43,7 +64,6 @@
|
|||||||
document.getElementById('scheduled').value = today;
|
document.getElementById('scheduled').value = today;
|
||||||
// Initialize flatpickr with Swedish locale and Monday as the first day of the week
|
// Initialize flatpickr with Swedish locale and Monday as the first day of the week
|
||||||
flatpickr("#scheduled", {
|
flatpickr("#scheduled", {
|
||||||
//locale: "sv-SE", // Set Swedish locale
|
|
||||||
weekNumbers: true, // Show week numbers
|
weekNumbers: true, // Show week numbers
|
||||||
firstDayOfWeek: 1 // Start weeks on Monday
|
firstDayOfWeek: 1 // Start weeks on Monday
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ self.addEventListener('install', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.url.includes('/get-tags') || event.request.url.includes('/save-tags')) {
|
||||||
|
// Bypass cache for tags endpoints
|
||||||
|
event.respondWith(fetch(event.request));
|
||||||
|
} else {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request).then((cachedResponse) => {
|
caches.match(event.request).then((cachedResponse) => {
|
||||||
return cachedResponse || fetch(event.request);
|
return cachedResponse || fetch(event.request);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
79
routes/tasks.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const debug = require('debug')('app');
|
||||||
|
const auth = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const dataDir = '/data';
|
||||||
|
|
||||||
|
// Ensure the /data directory exists
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the tags.json file exists
|
||||||
|
const tagsFilePath = path.join(dataDir, 'tags.json');
|
||||||
|
if (!fs.existsSync(tagsFilePath)) {
|
||||||
|
fs.writeFileSync(tagsFilePath, JSON.stringify([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect the /add-task endpoint with authentication
|
||||||
|
router.post('/add-task', auth, async (req, res) => {
|
||||||
|
const { subject, description, scheduled } = req.body;
|
||||||
|
const currentDateTime = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||||
|
|
||||||
|
let orgFormattedData = `
|
||||||
|
* TODO ${subject}
|
||||||
|
SCHEDULED: <${scheduled}>
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "TODO" from "TODO" [${currentDateTime}]
|
||||||
|
:END:`;
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
orgFormattedData = `
|
||||||
|
* TODO ${subject}
|
||||||
|
${description}
|
||||||
|
SCHEDULED: <${scheduled}>
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "TODO" from "TODO" [${currentDateTime}]
|
||||||
|
:END:`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(dataDir, 'tasks.org');
|
||||||
|
try {
|
||||||
|
await fs.promises.appendFile(filePath, orgFormattedData);
|
||||||
|
res.send({ message: 'Task added successfully!' });
|
||||||
|
} catch (err) {
|
||||||
|
debug('Error writing to file:', err);
|
||||||
|
res.status(500).send('Error writing to file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint to save tags
|
||||||
|
router.post('/save-tags', auth, async (req, res) => {
|
||||||
|
const { tags } = req.body;
|
||||||
|
const filePath = path.join(dataDir, 'tags.json');
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(tags));
|
||||||
|
res.send({ message: 'Tags saved successfully!' });
|
||||||
|
} catch (err) {
|
||||||
|
debug('Error saving tags:', err);
|
||||||
|
res.status(500).send('Error saving tags.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint to retrieve tags
|
||||||
|
router.get('/get-tags', auth, async (req, res) => {
|
||||||
|
const filePath = path.join(dataDir, 'tags.json');
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(filePath);
|
||||||
|
const tags = JSON.parse(data);
|
||||||
|
res.send(tags);
|
||||||
|
} catch (err) {
|
||||||
|
debug('Error retrieving tags:', err);
|
||||||
|
res.status(500).send('Error retrieving tags.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
62
server.js
@@ -1,68 +1,16 @@
|
|||||||
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const fs = require('fs');
|
const debug = require('debug')('app');
|
||||||
const path = require('path');
|
const tasksRouter = require('./routes/tasks');
|
||||||
const basicAuth = require('basic-auth');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3044;
|
const port = 3044;
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
app.use('/', tasksRouter);
|
||||||
// Ensure the /data directory exists
|
|
||||||
const dataDir = path.join('/data');
|
|
||||||
if (!fs.existsSync(dataDir)) {
|
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication middleware
|
|
||||||
const auth = (req, res, next) => {
|
|
||||||
const user = basicAuth(req);
|
|
||||||
const username = 'fredrik'; // Replace with your desired username
|
|
||||||
const password = 'apa'; // Replace with your desired password
|
|
||||||
|
|
||||||
if (user && user.name === username && user.pass === password) {
|
|
||||||
return next();
|
|
||||||
} else {
|
|
||||||
res.set('WWW-Authenticate', 'Basic realm="401"');
|
|
||||||
return res.status(401).send('Authentication required.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Protect the /add-task endpoint with authentication
|
|
||||||
app.post('/add-task', auth, (req, res) => {
|
|
||||||
const { subject, description, scheduled } = req.body;
|
|
||||||
const currentDateTime = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
|
||||||
|
|
||||||
let orgFormattedData = `
|
|
||||||
* TODO ${subject}
|
|
||||||
SCHEDULED: <${scheduled}>
|
|
||||||
:LOGBOOK:
|
|
||||||
- State "TODO" from "TODO" [${currentDateTime}]
|
|
||||||
:END:
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
orgFormattedData = `
|
|
||||||
* TODO ${subject}
|
|
||||||
${description}
|
|
||||||
SCHEDULED: <${scheduled}>
|
|
||||||
:LOGBOOK:
|
|
||||||
- State "TODO" from "TODO" [${currentDateTime}]
|
|
||||||
:END:
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(dataDir, 'tasks.org');
|
|
||||||
fs.appendFile(filePath, orgFormattedData, (err) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).send('Error writing to file.');
|
|
||||||
}
|
|
||||||
res.send({ message: 'Task added successfully!' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running at http://localhost:${port}`);
|
debug(`Server running at http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|||||||