first commit
This commit is contained in:
commit
40f6ade790
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
*.db
|
40
cli.js
Normal file
40
cli.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
const { argv } = require('node:process');
|
||||||
|
const { addUser, resetPassword } = require('./utilities')
|
||||||
|
|
||||||
|
function userCreate(username, directory) {
|
||||||
|
if (!username || !directory) {
|
||||||
|
console.log(' User-create requires a username and directory.')
|
||||||
|
console.log(' e.g. `user-create sam gemini.example.com`.\n')
|
||||||
|
} else {
|
||||||
|
addUser(username, directory, password => {
|
||||||
|
console.log(` User ${username} created with password ${password}`)
|
||||||
|
console.log(' Keep it secret, keep it safe.\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function passwordReset(username) {
|
||||||
|
if (!username) {
|
||||||
|
console.log(' User-create requires a username.')
|
||||||
|
console.log(' e.g. `password-reset sam`.\n')
|
||||||
|
} else {
|
||||||
|
resetPassword(username, null, password => {
|
||||||
|
console.log(` Password for ${username} is now ${password}`)
|
||||||
|
console.log(' Keep it secret, keep it safe.\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: delete user from database, (and user's files?)
|
||||||
|
|
||||||
|
switch (argv[2]) {
|
||||||
|
case 'user-create':
|
||||||
|
userCreate(argv[3], argv[4]);
|
||||||
|
break;
|
||||||
|
case 'password-reset':
|
||||||
|
passwordReset(argv[3]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(' Command not recognised.')
|
||||||
|
console.log(' Possible commands are `user-create` or `password-reset`\n')
|
||||||
|
}
|
1858
package-lock.json
generated
Normal file
1858
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "soyuz-web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Web app for publishing Gemini posts",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"password-reset": "node cli.js password-reset",
|
||||||
|
"user-create": "node cli.js user-create"
|
||||||
|
},
|
||||||
|
"author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net/)",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"keywords": ["gemini"],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/hughrun/soyuz-web.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^8.0.1",
|
||||||
|
"better-sqlite3-session-store": "^0.1.0",
|
||||||
|
"body-parser": "^1.20.1",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"sprightly": "^1.0.3"
|
||||||
|
}
|
||||||
|
}
|
136
server.js
Normal file
136
server.js
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
require('dotenv').config()
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const { getLatestPost, getNow, publishNewPost, requireLoggedIn, resetPassword, saveFile, updatePost, verifyUser, getSavedFile } = require('./utilities')
|
||||||
|
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const session = require('express-session')
|
||||||
|
const sprightly = require('sprightly');
|
||||||
|
const SqliteStore = require("better-sqlite3-session-store")(session)
|
||||||
|
|
||||||
|
// configure Express
|
||||||
|
const app = express()
|
||||||
|
const PORT = process.env.SOYUZ_PORT
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }))
|
||||||
|
app.use(express.static('static'))
|
||||||
|
|
||||||
|
// configure session store
|
||||||
|
db = new Database('soyuz.db', {});
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
store: new SqliteStore({
|
||||||
|
client: db,
|
||||||
|
expired: {
|
||||||
|
clear: true,
|
||||||
|
intervalMs: 900000 //ms = 15min
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
saveUninitialized: false,
|
||||||
|
secret: process.env.SOYUZ_SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
cookie: {
|
||||||
|
sameSite: 'strict',
|
||||||
|
maxAge: 1.21e+9 // 2 weeks
|
||||||
|
},
|
||||||
|
name: 'soyuz-web'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// configure template engine
|
||||||
|
app.engine('spy', sprightly);
|
||||||
|
app.set('views', './templates');
|
||||||
|
app.set('view engine', 'spy');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ROUTES
|
||||||
|
*/
|
||||||
|
|
||||||
|
// GET
|
||||||
|
|
||||||
|
app.get('/', requireLoggedIn, (req, res) => {
|
||||||
|
let data = {
|
||||||
|
disabled: '',
|
||||||
|
message: getSavedFile(req.session.user.username)}
|
||||||
|
let today = getNow().toISOString().slice(0,10)
|
||||||
|
let latestPost = req.session.user.latest_post
|
||||||
|
if (today === latestPost) {
|
||||||
|
data.disabled = 'disabled'
|
||||||
|
data.message = `Relax, ${req.session.user.username}, you have already posted today.`
|
||||||
|
}
|
||||||
|
res.render('index.spy', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/login', (req, res) => {
|
||||||
|
if (req.session.user) {
|
||||||
|
res.redirect('/')
|
||||||
|
} else {
|
||||||
|
res.render('login.spy')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/edit', requireLoggedIn, (req, res) => {
|
||||||
|
getLatestPost( req.session.user.directory, (data, path) => {
|
||||||
|
res.render('edit.spy', {data: data, path: path})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/settings', requireLoggedIn, (req, res) => {
|
||||||
|
res.render('settings.spy')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/try-again', requireLoggedIn, (req, res, next) => {
|
||||||
|
res.render('try-again.spy')
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST
|
||||||
|
|
||||||
|
app.post('/login', verifyUser,
|
||||||
|
function(req, res){
|
||||||
|
if (req.session.user) {
|
||||||
|
res.redirect('/')
|
||||||
|
} else {
|
||||||
|
res.redirect('/try-again')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/logout', function(req, res, next){
|
||||||
|
req.session.destroy( (err) => {
|
||||||
|
if (err) {console.error(err)}
|
||||||
|
res.redirect('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/publish', requireLoggedIn, (req, res) => {
|
||||||
|
publishNewPost(req, () => {
|
||||||
|
res.redirect('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/save', requireLoggedIn, (req, res) => {
|
||||||
|
saveFile(req.session.user, req.body.textarea, () => {
|
||||||
|
res.redirect('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/update', requireLoggedIn, (req, res) => {
|
||||||
|
updatePost(req, () => {
|
||||||
|
res.redirect('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/reset-password', requireLoggedIn, (req, res) => {
|
||||||
|
resetPassword(req.session.user.username, req.body.password, password => {
|
||||||
|
return req.session.destroy( (err) => {
|
||||||
|
if (err) {console.error(err)}
|
||||||
|
res.redirect('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's go!
|
||||||
|
*/
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Soyuz Web listening on port ${PORT}`)
|
||||||
|
})
|
2
static/soyuz.js
Normal file
2
static/soyuz.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// use this regex to find links that are the wrong way around:
|
||||||
|
// /=>\s[^:\/]*\s/
|
94
static/style.css
Normal file
94
static/style.css
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #3d3d3d;
|
||||||
|
padding: 0;
|
||||||
|
margin: auto;
|
||||||
|
min-height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 2fr auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
color: #ddd;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
grid-row-start: 3;
|
||||||
|
grid-row-end: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
margin: 1em auto 0;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em auto 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header form {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea,
|
||||||
|
main {
|
||||||
|
outline: none;
|
||||||
|
width: 90%;
|
||||||
|
height:80vh;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-buttons {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
color: #ddd;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background-color: #999;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-reset {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings section:nth-of-type(even) {
|
||||||
|
margin-top: 1em;
|
||||||
|
border-top: 1px solid #999;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
18
templates/edit.spy
Normal file
18
templates/edit.spy
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<< partials/head >>
|
||||||
|
<body>
|
||||||
|
<< partials/header >>
|
||||||
|
|
||||||
|
<section class="textarea">
|
||||||
|
<form method="post">
|
||||||
|
<textarea name="textarea" autofocus>{{ data }}</textarea>
|
||||||
|
<input type="text" name="path" value="{{ path }}" hidden>
|
||||||
|
<section class="post-buttons">
|
||||||
|
<input class="action-button" type="submit" name="save" value="Save" formaction="/save"> |
|
||||||
|
<input class="action-button" type="submit" name="publish" value="Update" formaction="/update">
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
templates/index.spy
Normal file
17
templates/index.spy
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<< partials/head >>
|
||||||
|
<body class="{{ disabled }}">
|
||||||
|
<< partials/header >>
|
||||||
|
|
||||||
|
<section class="textarea">
|
||||||
|
<form method="post">
|
||||||
|
<textarea name="textarea" autofocus class="{{ disabled }}" {{ disabled }}>{{ message }}</textarea>
|
||||||
|
<section class="post-buttons">
|
||||||
|
<input class="action-button" type="submit" name="save" value="Save" formaction="/save"> |
|
||||||
|
<input class="action-button" type="submit" name="publish" value="Publish" formaction="/publish">
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
templates/login.spy
Normal file
16
templates/login.spy
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<< partials/head >>
|
||||||
|
<body>
|
||||||
|
<header class="welcome">
|
||||||
|
<h1>Welcome to Soyuz</h1>
|
||||||
|
</header>
|
||||||
|
<section class="login">
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<label for="username">username </label><br/>
|
||||||
|
<input type="text" name="username"><br/>
|
||||||
|
<label for="password">password </label><br/>
|
||||||
|
<input type="password" name="password"><br/><br/>
|
||||||
|
<button type="submit">Log in!</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
9
templates/partials/head.spy
Normal file
9
templates/partials/head.spy
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||||
|
<title>Soyuz</title>
|
||||||
|
</head>
|
5
templates/partials/header.spy
Normal file
5
templates/partials/header.spy
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<header>
|
||||||
|
<a class="action-button" href="/">New</a> |
|
||||||
|
<a class="action-button" href="/edit">Edit</a> |
|
||||||
|
<a class="action-button" href="/settings">Settings</a>
|
||||||
|
</header>
|
18
templates/settings.spy
Normal file
18
templates/settings.spy
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<< partials/head >>
|
||||||
|
<body>
|
||||||
|
<< partials/header >>
|
||||||
|
<main id="settings">
|
||||||
|
<form method="post">
|
||||||
|
<section>
|
||||||
|
<label for="password">Reset password:</label><br/>
|
||||||
|
<input class="password-reset" type="password" name="password" id="password">
|
||||||
|
<input type="submit" name="logout" value="Reset Password" formaction="/reset-password">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<input type="submit" name="logout" value="Log Out" formaction="/logout">
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
<footer></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
templates/try-again.spy
Normal file
15
templates/try-again.spy
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<< partials/head >>
|
||||||
|
<body>
|
||||||
|
<p>Welcome to Soyuz</p>
|
||||||
|
<div>Incorrect username or password, please try again</div>
|
||||||
|
<section>
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<label for="username">username </label>
|
||||||
|
<input type="text" name="username">
|
||||||
|
<label for="password">username </label>
|
||||||
|
<input type="password" name="password">
|
||||||
|
<button type="submit">Log in!</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
317
utilities.js
Normal file
317
utilities.js
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
const GEMINI_PATH = process.env.GEMINI_PATH
|
||||||
|
const Database = require("better-sqlite3")
|
||||||
|
const {mkdir, readFile, writeFile} = require('node:fs')
|
||||||
|
const { pbkdf2, randomBytes } = require('node:crypto')
|
||||||
|
db = new Database('soyuz.db', {})
|
||||||
|
|
||||||
|
function getNow() {
|
||||||
|
// we want to be able to use toISOString but that always returns
|
||||||
|
// the date in UTC timezone. Here we adjust the UTC date to align
|
||||||
|
// with the local timezone
|
||||||
|
let localNow = new Date()
|
||||||
|
let now = new Date(localNow.getTime() - localNow.getTimezoneOffset()*60000)
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
||||||
|
// DATABASE FUNCTIONS
|
||||||
|
const addUser = function(username, directory, callback){
|
||||||
|
|
||||||
|
let buf = randomBytes(32);
|
||||||
|
let salt = buf.toString('hex');
|
||||||
|
let pbuf = randomBytes(16);
|
||||||
|
let password = pbuf.toString('hex');
|
||||||
|
directory = directory.toString()
|
||||||
|
|
||||||
|
// prepare db table
|
||||||
|
let createTable = db.prepare(
|
||||||
|
'CREATE TABLE IF NOT EXISTS users (username TEXT UNIQUE, password TEXT, salt TEXT, directory TEXT UNIQUE, latest_post TEXT, saved_post TEXT)'
|
||||||
|
);
|
||||||
|
createTable.run();
|
||||||
|
|
||||||
|
// save to db
|
||||||
|
pbkdf2(password, salt, 310000, 32, 'sha512', (err, derivedKey) => {
|
||||||
|
if (err) throw err;
|
||||||
|
let hash = derivedKey.toString('hex');
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'INSERT INTO users (username, directory, password, salt, saved_post) VALUES (?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
stmt.run(username, directory, hash, salt, '# Title of my note');
|
||||||
|
return callback(password)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPassword = function(username, pass, callback) {
|
||||||
|
|
||||||
|
let buf = randomBytes(32)
|
||||||
|
let salt = buf.toString('hex')
|
||||||
|
let password
|
||||||
|
if (pass) {
|
||||||
|
password = pass
|
||||||
|
} else {
|
||||||
|
let pbuf = randomBytes(16)
|
||||||
|
password = pbuf.toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
pbkdf2(password, salt, 310000, 32, 'sha512', (err, derivedKey) => {
|
||||||
|
if (err) throw err;
|
||||||
|
let hash = derivedKey.toString('hex');
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'UPDATE users SET password = ?, salt = ? WHERE username = ?'
|
||||||
|
);
|
||||||
|
stmt.run(hash, salt, username);
|
||||||
|
return callback(password)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update latest post in db
|
||||||
|
const updateLatestPostDate = function(username, callback) {
|
||||||
|
|
||||||
|
let dateString = getNow().toISOString().slice(0,10)
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'UPDATE users SET latest_post = ? WHERE username = ?'
|
||||||
|
);
|
||||||
|
stmt.run(dateString, username);
|
||||||
|
callback(dateString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTHORISATION MIDDLEWARE
|
||||||
|
const verifyUser = function (req, res, next) {
|
||||||
|
let username = req.body.username
|
||||||
|
let password = req.body.password
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'SELECT * FROM users WHERE username = ?'
|
||||||
|
)
|
||||||
|
user = stmt.get(username)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
pbkdf2(password, user.salt, 310000, 32, 'sha512', function(err, hashedPassword) {
|
||||||
|
if (err) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
if (user.password !== hashedPassword.toString('hex')) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
req.session.user = {
|
||||||
|
username: user.username,
|
||||||
|
directory: user.directory,
|
||||||
|
latest_post: user.latest_post,
|
||||||
|
};
|
||||||
|
next()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireLoggedIn = function(req, res, next) {
|
||||||
|
if (req.session.user) {
|
||||||
|
return next()
|
||||||
|
} else {
|
||||||
|
return res.redirect('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUBLISHING
|
||||||
|
const publishNewPost = function(req, cb) {
|
||||||
|
let post = req.body.textarea
|
||||||
|
let title = req.body.textarea.split('\n')[0].split('# ')[1].trim()
|
||||||
|
let year = getNow().toISOString().slice(0,4)
|
||||||
|
let dateString = getNow().toISOString().slice(0,10)
|
||||||
|
let yearDir = `${GEMINI_PATH}/${req.session.user.directory}/${year}`
|
||||||
|
let fileName = `${GEMINI_PATH}/${req.session.user.directory}/${year}/${dateString}.gmi`
|
||||||
|
|
||||||
|
function updateArchivePage() {
|
||||||
|
// update or create year's archive page
|
||||||
|
let yearIndex = `${GEMINI_PATH}/${req.session.user.directory}/${year}/index.gmi`
|
||||||
|
let updated = ''
|
||||||
|
readFile(yearIndex, {encoding: 'utf8'}, (err, data) => {
|
||||||
|
// if the file doesn't exist, create it
|
||||||
|
if (err) {
|
||||||
|
if (err.code == 'ENOENT') {
|
||||||
|
let string = `# ${year} Notes\n\n=> ${dateString}.gmi ${dateString} (${title})\n`
|
||||||
|
writeFile(yearIndex, string, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let lines = data.split('\n')
|
||||||
|
lines[1] = `\n=> ${dateString}.gmi ${dateString} (${title})`
|
||||||
|
updated = lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(yearIndex, updated, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// clear any saved post now that it is published
|
||||||
|
saveFile(req.session.user.username, '# Title of my note', () => {
|
||||||
|
// delete active page on db and in session
|
||||||
|
updateLatestPostDate(req.session.user.username, datestring => {
|
||||||
|
req.session.user.latest_post = datestring
|
||||||
|
return cb()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIndexListing() {
|
||||||
|
// update index.gmi listing
|
||||||
|
let indexFile = `${GEMINI_PATH}/${req.session.user.directory}/index.gmi`
|
||||||
|
readFile(indexFile, {encoding: 'utf8'}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
// if the file doesn't exist, create it
|
||||||
|
if (err.code == 'ENOENT') {
|
||||||
|
let string = `## Latest notes\n\n=> /${year}/${dateString}.gmi ${dateString} (${title})\n`
|
||||||
|
writeFile(indexFile, string, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let links = data.split('## Latest notes')
|
||||||
|
let lines = links[1].split('\n')
|
||||||
|
for (let i = 6; i < 2; i--) {
|
||||||
|
if (lines[i] && lines[i].startsWith('=>')) {
|
||||||
|
lines[i] = lines[i-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines[0] = '## Latest notes'
|
||||||
|
lines[2] = `=> /${year}/${dateString}.gmi ${dateString} (${title})`
|
||||||
|
updated = links[0] + lines.join('\n')
|
||||||
|
writeFile(indexFile, updated, (err) => {
|
||||||
|
if (err) {
|
||||||
|
// if the directory doesn't exist, create it and try again
|
||||||
|
if (err.code == 'ENOENT') {
|
||||||
|
mkdir(yearDir, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
writeFile(indexFile, updated, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updateArchivePage()
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(fileName, post, (err) => {
|
||||||
|
if (err) {
|
||||||
|
// if the directory doesn't exist, create it and try again
|
||||||
|
if (err.code == 'ENOENT') {
|
||||||
|
mkdir(yearDir, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
writeFile(fileName, post, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updateIndexListing()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let getLatestPost = function(directory, callback) {
|
||||||
|
// we check the index file because
|
||||||
|
// a new post could have come from
|
||||||
|
// somewhere other than the app
|
||||||
|
// e.g. from a CLI on a laptop etc
|
||||||
|
let indexFile = `${GEMINI_PATH}/${directory}/index.gmi`
|
||||||
|
readFile(indexFile, {encoding: 'utf8'}, (err, data) => {
|
||||||
|
if (err) throw err;
|
||||||
|
let links = data.split('## Latest notes')
|
||||||
|
let parts = links[1].split('\n')[2].split(' ')
|
||||||
|
let filePath = `${GEMINI_PATH}/${directory}/${parts[1]}`
|
||||||
|
|
||||||
|
readFile(filePath, {encoding: 'utf8'}, (err, file) => {
|
||||||
|
if (err) throw err;
|
||||||
|
return callback(file, filePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatePost = function(req, callback) {
|
||||||
|
let contents = req.body.textarea
|
||||||
|
let path = req.body.path
|
||||||
|
let title = contents.split('\n')[0].split('# ')[1].trim()
|
||||||
|
let year = getNow().toISOString().slice(0,4)
|
||||||
|
let dateString = getNow().toISOString().slice(0,10)
|
||||||
|
let indexFile = `${GEMINI_PATH}/${req.session.user.directory}/index.gmi`
|
||||||
|
let yearIndex = `${GEMINI_PATH}/${req.session.user.directory}/${year}/index.gmi`
|
||||||
|
let updated = ''
|
||||||
|
|
||||||
|
// we update the index and archive listings in case the title has changed
|
||||||
|
readFile(indexFile, {encoding: 'utf8'}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
let links = data.split('## Latest notes')
|
||||||
|
let lines = links[1].split('\n')
|
||||||
|
lines[0] = '## Latest notes'
|
||||||
|
lines[2] = `=> /${year}/${dateString}.gmi ${dateString} (${title})`
|
||||||
|
updated = links[0] + lines.join('\n')
|
||||||
|
// update index on homepage
|
||||||
|
writeFile(indexFile, updated, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
readFile(yearIndex, {encoding: 'utf8'}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
} else {
|
||||||
|
let lines = data.split('\n')
|
||||||
|
lines[2] = `=> ${dateString}.gmi ${dateString} (${title})`
|
||||||
|
updated = lines.join('\n')
|
||||||
|
// update archive page
|
||||||
|
writeFile(yearIndex, updated, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
//write out the updated post
|
||||||
|
writeFile(path, contents, (err) => {
|
||||||
|
if (err) {
|
||||||
|
if (err) throw err;
|
||||||
|
}
|
||||||
|
return callback()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let saveFile = function(user, text, callback) {
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'UPDATE users SET saved_post = ? WHERE username = ?'
|
||||||
|
);
|
||||||
|
stmt.run(text, user.username);
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
let getSavedFile = function(user) {
|
||||||
|
let stmt = db.prepare(
|
||||||
|
'SELECT saved_post FROM users WHERE username = ?'
|
||||||
|
)
|
||||||
|
stmt.pluck(true)
|
||||||
|
let post = stmt.get(user)
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
let savePictures = function(text) {
|
||||||
|
// we will need to save pictures to the server
|
||||||
|
// separately when publishing
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addUser: addUser,
|
||||||
|
getLatestPost: getLatestPost,
|
||||||
|
getNow: getNow,
|
||||||
|
getSavedFile: getSavedFile,
|
||||||
|
publishNewPost: publishNewPost,
|
||||||
|
resetPassword: resetPassword,
|
||||||
|
requireLoggedIn: requireLoggedIn,
|
||||||
|
saveFile: saveFile,
|
||||||
|
updatePost: updatePost,
|
||||||
|
verifyUser: verifyUser
|
||||||
|
}
|
Loading…
Reference in a new issue