feat: add basic blog features

This commit is contained in:
2026-03-09 22:17:39 +01:00
commit c16657f996
29 changed files with 3095 additions and 0 deletions

64
.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
#postgres stuff
postgresql

44
app.js Normal file
View File

@@ -0,0 +1,44 @@
import createError from 'http-errors';
import express from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import indexRouter from './routes/index.js';
import usersRouter from './routes/users.js';
import postsRouter from './routes/posts.js';
const __dirname = import.meta.dirname;
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/posts', postsRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
export default app;

View File

@@ -0,0 +1,24 @@
import client from "../db/index.js";
export default class PostController {
static async getAll() {
const res = await client.query("SELECT * FROM posts");
return res.rows;
}
static async get(id) {
const res = await client.query({
text: "SELECT * FROM posts WHERE id=$1",
values: [id]
});
return res.rows[0];
}
static async create(title, content) {
const res = await client.query({
text: "INSERT INTO posts(title, content) VALUES($1, $2)",
values: [title, content]
});
console.log(res.rows);
}
}

15
db/index.js Normal file
View File

@@ -0,0 +1,15 @@
import { Pool } from 'pg';
const db = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.PORT,
database: process.env.DB_DATABASE,
max: 20,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
maxLifetimeSeconds: 60
});
export default db;

68
db/migrate.js Normal file
View File

@@ -0,0 +1,68 @@
import fs from "node:fs";
import { readdir } from "node:fs/promises";
import path from "node:path";
import dbClient from "./index.js";
const __dirname = import.meta.dirname;
const createMigrationTable = () => {
return dbClient.query(`
CREATE TABLE IF NOT EXISTS migrations (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
executed_at TIMESTAMP
)
`);
}
const getMigrationsFiles = () => {
try {
return readdir(path.resolve(__dirname, "./migrations"));
} catch(err) {
return new Error("[Migration] getMigrations err: ", err);
}
};
const runMigrations = async () => {
try {
// Create migration table if needed
await createMigrationTable();
const migrationFiles = await getMigrationsFiles();
const migrationsDb = await dbClient.query("SELECT name FROM migrations WHERE executed_at IS NOT NULL");
const migrationsToRun = migrationFiles.filter(m => !migrationsDb.rows.map(m => m.name).includes(m));
console.log("[Migration] migrations to run: ", migrationsToRun);
for (const migration of migrationsToRun) {
await dbClient.query("BEGIN");
const sqlQuery = fs.readFileSync(path.resolve(__dirname, `./migrations/${migration}`), "utf-8");
try {
await dbClient.query(sqlQuery);
await dbClient.query({
text: "INSERT INTO migrations(id, name, executed_at) VALUES($1, $2, $3)",
values: [migration.slice(0, -4), migration, new Date().toISOString().slice(0, 19).replace('T', ' ')]
});
await dbClient.query("COMMIT");
} catch(err) {
console.log(err);
await dbClient.query("ROLLBACK");
}
}
console.log("[Migration]: SUCCESS");
} catch (err) {
console.error("Error", err);
process.exit(1);
}
};
// Run this from CLI
if (import.meta.main) {
console.log("Run migrations");
runMigrations()
.then(() => process.exit(0))
.catch(() => process.exit(1));
}

View File

@@ -0,0 +1,9 @@
CREATE XTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR(255) NULL UNIQUE,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -0,0 +1,29 @@
CREATE TABLE users (
id TEXT PRIMARY KEY NOT NULL UNIQUE,
username VARCHAR(255)
);
CREATE TABLE passkeys (
id TEXT PRIMARY KEY NOT NULL UNIQUE,
public_key BYTEA,
webauthn_user__id TEXT UNIQUE,
counter BIGINT,
device_type VARCHAR(32),
transports VARCHAR(255)
);
-- User/passkey junction table
CREATE TABLE user_passkeys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id TEXT NOT NULL,
passkey_id TEXT NOT NULL,
-- Foreign key constraints
CONSTRAINT fk_user_passkeys_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_passkeys_passkeys FOREIGN KEY (passkey_id) REFERENCES passkeys(id) ON DELETE CASCADE,
-- Prevent duplicates
CONSTRAINT unique_user_passkeys UNIQUE (user_id, passkey_id)
);
CREATE INDEX index_passkeys ON passkeys (id, webauthn_user__id);

View File

@@ -0,0 +1,16 @@
export default class Post {
id;
title;
content;
created_at;
updated_at;
constructor(data) {
this.title = data.title;
this.content = data.content;
this.id = data.id || undefined;
this.created_at = data.created_at || undefined;
this.updated_at = data.updated_at || undefined;
}
}

98
index.js Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from './app.js';
import debug from 'debug';
debug('pathtoglory:server');
import https from 'https';
import path from 'node:path';
import fs from 'node:fs';
const sslOptions = {
key: fs.readFileSync(path.join(import.meta.dirname, 'localhost-key.pem')),
cert: fs.readFileSync(path.join(import.meta.dirname, 'localhost.pem'))
};
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '1234');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = https.createServer(sslOptions, app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

28
localhost-key.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCiYoJCZdHcVH/w
GFQZUhY59jxiJ6GlKbo2cu7ozp34WYyIKun5cPXyWxWUtY0yuf6jUd8+JtL/T0uo
6qBMdnznE8uP0Ol7XmiozZ156celbbhO4VFUDu0wesXGktAPQllmIF7tB3zUhb2R
6PUao8lzn/Um6kLD7FFriTqOJL4X2d9cpAVpvTmjl5YqNRwiPQPOhPH1yyOv6iin
aRjklk00krWlp1Ezd4gitTqzMdGc72cUIVvC3JWLFUUHG4Xjkl7va4FPWCz6P7Di
kq3gadnOE+sWuQNNyVEjnU4hwTUZ8AYEVM23Siq2+7IkKu/oiX+zeBCfZhLNgilf
eTMxxUyhAgMBAAECggEAReo1/VKLhdrX7s76vqAqM7CCFRzNKyiJJVJc7N2xBDHC
IQqhDKYHLt7qrslwTsvoB/eDL+ZVaFmC0OqcM++8HV3XgkdHj7d5RlypFcmDDQXt
mgDHHHMEyp/BsZqafEdr6F29oT5dD7+5fC4aAetNHDxdt/Ca6HJCKBPAo1zMf8W+
HXepvb3eTTIDG+msclY9b6gCPxuJU3rM+JzkohwClQJJe40lBu4qyMeiGYynjHY2
/98M21dja02RFBP66S+S9UaPQ5/h1LRMz2vI+njX3qgAWjdTsazoa0jzZsqbIl/u
6NnGlScIGDAQaBpkb2YhhCJxR7Q3f9PGd2leGo1zsQKBgQDITbw05KaKU1hZ0R+0
196rurSUDbsFmamwFouzRWSwTCMwJlGcoMQ0mChfwwWKAXiNJRlMf0biywIGZisD
CApjgpRNuOdIqy3rU4iDVxwj45XNNgobfF1ZKAHum5q9KjIchUZsI8htC7KgH0S6
zMsPbxk56ueKGXF6kyZpoW4Z1QKBgQDPiZYkPyk153J72O+V+kwOTOdGoBMETXdY
wPf8gd/j5oNwuQV7DWxMyq4ku4uc9fJn9rAxymHlbFqWce8h2jYVPOfak0Om6N2z
IDjlLKWzvjzpiUNZ8QsODfm4rFYni3sEhC0K3IkDzEDtSr3UakL66Uqjsqg4cn7S
Ik4u0r8hnQKBgQCq+l/7LmpSjQ5PrMjJz7LNGBRohMft4dsM6lHZdxSZwIQQ58Sm
VDznQDLGe2xQ/yxuHwrXV5WkpfFWkQOKFOT5SE9bgMg8KZKK28Udh9AHeo82mjhK
egAcyJ/Nk5mke05HNiSEzo6ZNnEFaWt7oLB8vjLkU3XNViadoNobNKcM+QKBgAku
KeESli0XPt4xm2+D8edUCYr7O7wd/SCE8LNPv2qiYMAUvyRRVLAU6x0e2q8nxgBJ
TkP1kt0GLP+orI5Py8Kmvg7SItT4Sg5JZ5rjnbTUvncKJluNKRMHFTvRC8KWDewG
OMPZO4pad6jHfJwv0ySsOywAlCZjEi8Ta2fw1JmVAoGASSUtaNsjsTJ5ikRXPZTQ
MS0Vb+leH5+M9umbV4sxqsY4oz6l3Dv7fEdigJmrrBTKlH5uRKFxqE91FcOYbyH4
C9HyzJgiujvfWoFqF5H0EoIpsHvtydGMr+JkHbwDSZfeRelFnUEFLhTQSu/fyHeW
FX/eT/HtOo4D1NKZ5UDG1O8=
-----END PRIVATE KEY-----

24
localhost.pem Normal file
View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID/jCCAmagAwIBAgIRAIckhgeUs09k09IblD8x28EwDQYJKoZIhvcNAQELBQAw
VTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMRUwEwYDVQQLDAxhcnRo
dXJAYmVhY2gxHDAaBgNVBAMME21rY2VydCBhcnRodXJAYmVhY2gwHhcNMjYwMzA0
MTY0OTQwWhcNMjgwNjA0MTU0OTQwWjBAMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxv
cG1lbnQgY2VydGlmaWNhdGUxFTATBgNVBAsMDGFydGh1ckBiZWFjaDCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJigkJl0dxUf/AYVBlSFjn2PGInoaUp
ujZy7ujOnfhZjIgq6flw9fJbFZS1jTK5/qNR3z4m0v9PS6jqoEx2fOcTy4/Q6Xte
aKjNnXnpx6VtuE7hUVQO7TB6xcaS0A9CWWYgXu0HfNSFvZHo9RqjyXOf9SbqQsPs
UWuJOo4kvhfZ31ykBWm9OaOXlio1HCI9A86E8fXLI6/qKKdpGOSWTTSStaWnUTN3
iCK1OrMx0ZzvZxQhW8LclYsVRQcbheOSXu9rgU9YLPo/sOKSreBp2c4T6xa5A03J
USOdTiHBNRnwBgRUzbdKKrb7siQq7+iJf7N4EJ9mEs2CKV95MzHFTKECAwEAAaNe
MFwwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQY
MBaAFNLXA4V6orI5Z7VVA/5i5ga0AjShMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAN
BgkqhkiG9w0BAQsFAAOCAYEAqJ/+hvJ/xFQ7GBzPteLNcfBtfokSJ5siynMokU3b
mOoKl+jR9r+4/X5aaSe5sOXAaE0kB3dliYxF9J4FMa5y4E5yluYAdt6vFYOI2u4/
zOuo1VStMhkStXc8h41jd2jfApoiZaicQYv8LwdJ34oHNq+phbUmxxggFLSd2tsD
oaVKUjMe15QvI8HsB+czmpPJUMO8u9ajHUxJZOKyYvTOjdefGMpjEhUpBwOCcFxd
TTybDD/+FmV3h/0m1U0CVrK+TN+hGje97DNK+bV7OxYL/RhoCFm99TOdKJxmV78X
AEIVb6360GVo7ySWxsFRXHu3vzBBoDPwWIieEksqCyfA7jhkaCKRVWZLmwUl4lrg
wGBYJCzibjFZihlgslGAavxBk1cL2Eu5KANkp+ybmP4assbzoXrTZGfY3Va10q3r
5u8oovVVTcVlthVGjpP90qSBogwdlDjtDYFq8PpTyIcqJQTneJ/F4KDf9gawOSqF
1KEt58J/uzK159CrXzJVIXJq
-----END CERTIFICATE-----

View File

@@ -0,0 +1,21 @@
import crypto from 'node:crypto';
const authenticateToken = function(req, res, next) {
const token = req.cookies.auth_token;
if(!token) {
res.redirect("/");
}
const data = token.split('.');
const hash = crypto.createHmac('sha256', process.env.AUTH_JWT_SECRET).update(data[0] + '.' + data[1]).digest('base64url');
if (hash === data[2]) {
next();
} else {
res.redirect('/users/login');
}
};
export default authenticateToken;

View File

@@ -0,0 +1,12 @@
import dbClient from '../db/index.js';
async function limitedUsers(_, res, next) {
const countQuery = await dbClient.query('SELECT COUNT(*) FROM users');
if(countQuery.rows[0].count > 0) {
res.redirect('/users/login');
}
next();
}
export default limitedUsers;

2211
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "pathtoglory",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node --env-file=.env ./index.js",
"db:migrate": "node db/migrate.js"
},
"dependencies": {
"@simplewebauthn/server": "^13.2.3",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "^4.22.1",
"http-errors": "~1.6.3",
"morgan": "^1.10.1",
"pg": "^8.18.0",
"pug": "^2.0.4",
"rb26": "^1.1.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,25 @@
fetch('/users/generate-auth-options/arthur')
.then(resp => resp.json())
.then((optionsJSON) => {
console.log(optionsJSON);
return SimpleWebAuthnBrowser.startAuthentication({ optionsJSON })
})
.then(authResp => {
console.log("verify");
return fetch("/users/verify-auth", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
authResp: authResp,
username: "arthur"
})
});
})
.then(res => {
console.log(res);
})
.catch(err => {
console.error(err);
});

View File

@@ -0,0 +1,45 @@
window.addEventListener("DOMContentLoaded", function() {
const registerBtn = document.querySelector("#register");
registerBtn?.addEventListener("click", async function() {
await fetch("/users/register/setup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: "arthur"
})
})
.then((data) => {
return data.json();
})
.then((data) => {
console.log("we received:", data);
return SimpleWebAuthnBrowser.startRegistration({ optionsJSON: data });
})
.then((attResp) => {
return fetch("/users/register/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
registration: attResp,
username: 'arthur'
})
});
})
.then((response) => {
return response.json();
})
.then((data) => {
console.log("Le finish");
console.log(data);
})
.catch((err) => {
console.error("Something went wrong");
console.error(err);
});
});
});

View File

@@ -0,0 +1,17 @@
:root {
--primary: #fd5e53;
}
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
footer {
border-top: 1px solid var(--primary);
margin-top: 1rem;
}

21
routes/index.js Normal file
View File

@@ -0,0 +1,21 @@
import express from 'express';
var router = express.Router();
import PostController from '../controllers/Post.controller.js';
import authenticateToken from '../middlewares/authentication.js';
/* GET home page. */
router.get('/', async function(req, res, next) {
const posts = await PostController.getAll();
res.render('index', { title: 'Path to glory', posts: posts });
});
router.get('/conclave', authenticateToken, async function(req, res, next) {
res.render('conclave');
});
router.post('/conclave/new', authenticateToken, async function(req, res, next) {
console.log(req.body);
await PostController.create(req.body.title, req.body.content);
res.redirect('/conclave');
});
export default router;

14
routes/posts.js Normal file
View File

@@ -0,0 +1,14 @@
import express from "express";
import convertToHTML from "rb26";
import PostController from "./../controllers/Post.controller.js";
var router = express.Router();
router.get("/:id", async function (req, res, next) {
const post = await PostController.get(req.params.id);
console.log(post);
res.render("post", { post: post, html: convertToHTML(post.content) });
});
export default router;

219
routes/users.js Normal file
View File

@@ -0,0 +1,219 @@
import express from 'express';
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import dbClient from './../db/index.js';
import crypto from 'node:crypto';
import limitedUsers from '../middlewares/limitedUsers.js';
var router = express.Router();
const userPasskeys = [];
const origin = `https://${process.env.RP_ID}`;
const challenges = {};
const users = {};
const currentLogin = {};
router.get("/register", limitedUsers, function(_, res) {
res.render("users/register");
});
router.get("/login", function(_, res) {
res.render("users/login");
});
router.get("/generate-auth-options/:username", async function(req, res, next) {
dbClient.query({
text: 'SELECT * FROM users WHERE username=$1',
values: [req.params.username]
})
.then((result) => {
const user = result.rows[0];
if (!user) {
throw new Error("ya pas de user wesh");
}
return dbClient.query({
text: `
SELECT *
FROM user_passkeys up
JOIN passkeys ON up.passkey_id = passkeys.id
WHERE up.user_id = $1
`,
values: [user.id]
});
})
.then(async (passkeyData) => {
const options = await generateAuthenticationOptions({
rpID: process.env.RP_ID,
allowCredentials: passkeyData.rows.map(passkey => ({
id: passkey.id,
transports: passkey.transports,
})),
});
currentLogin[req.params.username] = options;
return options;
})
.then((options) => {
res.json(options);
})
.catch((err) => {
console.error("Error in login");
console.error(err);
next(err);
});
});
router.post("/verify-auth/", function(req, res) {
console.log("Verify auth");
const { authResp, username } = req.body;
dbClient.query({
text: 'SELECT * FROM users WHERE username=$1',
values: [username]
})
.then((result) => {
const user = result.rows[0];
if (!user) {
throw new Error("ya pas de user wesh");
}
return dbClient.query({
text: `
SELECT *
FROM passkeys
WHERE id = $1
`,
values: [authResp.id]
});
})
.then(async (passkeyData) => {
const passkey = passkeyData.rows[0];
return verifyAuthenticationResponse({
response: authResp,
expectedChallenge: currentLogin[username].challenge,
expectedOrigin: origin,
expectedRPID: process.env.RP_ID,
credential: {
id: passkey.id,
publicKey: new Uint8Array(passkey['public_key']),
counter: passkey.counter,
transports: passkey.transports,
},
equireUserVerification: false,
});
})
.then(verification => {
const header = Buffer.from(JSON.stringify({
'alg': 'HS256',
'type': "JWT"
})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
verified: true,
username: username
})).toString('base64url');
const hash = crypto.createHmac('sha256', process.env.AUTH_JWT_SECRET);
hash.update(header + "." + payload);
const token = header + "." + payload + "." + hash.digest('base64url');
res.cookie('auth_token', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
res.send(verification);
})
.catch(err => {
console.log("ya erreur")
console.error(err);
res.status(400).send({ error: err.message });
});
});
router.post("/register/setup", limitedUsers, async function(req, res, next) {
const { username } = req.body;
generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.RP_ID,
userName: username,
attestationType: 'none',
excludeCredentials: userPasskeys.map(passkey => ({
id: passkey.id,
transports: passkey.transports,
})),
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
authenticatorAttachment: 'platform',
},
})
.then((options) => {
challenges[username] = options.challenge;
users[username] = options.user;
res.send(options);
})
.catch((err) => {
console.error("Something went wrong");
console.error(err);
next(err);
});
});
router.post("/register/verify", limitedUsers, function(req, res, next) {
const { registration, username } = req.body;
verifyRegistrationResponse({
response: registration,
expectedChallenge: challenges[username],
expectedOrigin: origin,
expectedRPID: process.env.RP_ID,
})
.then(async (verification) => {
const user = users[username];
await dbClient.query("BEGIN");
await dbClient.query({
text: 'INSERT INTO users(id, username) VALUES($1, $2)',
values: [user.id, user.name],
});
await dbClient.query({
text: 'INSERT INTO passkeys(id, public_key, counter, transports) VALUES($1, $2, $3, $4)',
values: [
verification.registrationInfo.credential.id,
Buffer.from(verification.registrationInfo.credential.publicKey),
verification.registrationInfo.credential.counter,
verification.registrationInfo.credential.transports.join(',')
]
});
await dbClient.query({
text: 'INSERT INTO user_passkeys(user_id, passkey_id) VALUES($1, $2)',
values: [user.id, verification.registrationInfo.credential.id]
});
dbClient.query("COMMIT");
res.send(verification);
})
.catch((err) => {
dbClient.query("ROLLBACK");
console.error("Something went wrong")
console.error(err);
next(err);
})
.finally(() => {
delete challenges[username];
delete users[username];
});
});
export default router;

8
views/conclave.pug Normal file
View File

@@ -0,0 +1,8 @@
extends layout
block content
h1 Conclave
form(method="POST" action="/conclave/new")
input(type="text" name="title" placeholder="Title")
textarea(name="content")
button(type="submit") Publish

6
views/error.pug Normal file
View File

@@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

9
views/index.pug Normal file
View File

@@ -0,0 +1,9 @@
extends layout
block content
section
ul
each post in posts
li
a(href=`/posts/${post.id}`)
h2= post.title

16
views/layout.pug Normal file
View File

@@ -0,0 +1,16 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
block head
body
header
h1
a(href="/") Pathtoglory.quest
p I'm on a quest to learn and understand things.
block content
footer
p Pathtoglory.quest No copyright. Copy and paste me.

6
views/post.pug Normal file
View File

@@ -0,0 +1,6 @@
extends layout
block content
article
h1= post.title
div!= html

12
views/users/login.pug Normal file
View File

@@ -0,0 +1,12 @@
extends ../layout
block head
script(defer src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js")
script(defer src="/javascripts/user-login.js")
block content
h2 Login
form
label(for="username") Username
input(type="text" name="username" autocomplete="webauth" placeholder="Username")
input(type="submit" value="Login with passkey")

13
views/users/register.pug Normal file
View File

@@ -0,0 +1,13 @@
extends ../layout
block head
script(defer src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js")
script(defer src="/javascripts/users.js")
block content
h2 Register new user
form
label(for="username") Username
input(type="text" name="username" autocomplete="username webauth" placeholder="Username")
button(id="register") Register