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