diff --git a/app/index.js b/app/index.js
index 286d699..60669d5 100755
--- a/app/index.js
+++ b/app/index.js
@@ -8,6 +8,42 @@ import app from './server/app.js';
import debug from 'debug';
debug('pathtoglory:server');
import http from 'http';
+import rssGenerator from './server/rss/index.js';
+import PostController from './server/controllers/Post.controller.js';
+import OptionController from './server/controllers/Option.controller.js';
+
+// Check needed options
+Promise.all([
+ OptionController.get('blog_name'),
+ OptionController.get('blog_description'),
+ `https://${process.env.RP_ID}/`,
+ PostController.getAll()
+])
+ .then(([name, description, link, posts]) => {
+ rssGenerator.setChannel(
+ name || process.env.RP_NAME,
+ description || '',
+ link,
+ posts[0] ? new Date(posts[0].updated_at) : new Date()
+ );
+
+ posts
+ .map(post => {
+ return {
+ title: post.title,
+ description: post.content.slice(0, 255),
+ link: `${link}/posts/${post.id}`,
+ }
+ })
+ .forEach(post => {
+ rssGenerator.addItem(post);
+ });
+
+ rssGenerator.generateFile();
+ })
+ .catch(err => {
+ console.error(err);
+ });
/**
* Get port from environment and store in Express.
diff --git a/app/public/stylesheets/style.css b/app/public/stylesheets/style.css
index 33c29bd..2642ed7 100644
--- a/app/public/stylesheets/style.css
+++ b/app/public/stylesheets/style.css
@@ -15,3 +15,7 @@ footer {
border-top: 1px solid var(--primary);
margin-top: 1rem;
}
+
+li h2 {
+ display: inline-block;
+}
diff --git a/app/server/app.js b/app/server/app.js
index 8979c01..9df0387 100644
--- a/app/server/app.js
+++ b/app/server/app.js
@@ -4,9 +4,11 @@ import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
+import authenticateToken from './middlewares/authentication.js';
import indexRouter from './routes/index.js';
import usersRouter from './routes/users.js';
import postsRouter from './routes/posts.js';
+import conclaveRouter from './routes/conclave.js';
var app = express();
@@ -16,13 +18,14 @@ app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
-app.use(express.urlencoded({ extended: false }));
+app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// app.use(express.static(path.join(import.meta.dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/posts', postsRouter);
+app.use('/conclave', authenticateToken, conclaveRouter);
// catch 404 and forward to error handler
app.use(function(_req, _res, next) {
diff --git a/app/server/controllers/Option.controller.js b/app/server/controllers/Option.controller.js
new file mode 100644
index 0000000..67c7961
--- /dev/null
+++ b/app/server/controllers/Option.controller.js
@@ -0,0 +1,60 @@
+import dbClient from "../db/index.js";
+
+export default class OptionController {
+ static async getAll() {
+ let queryText = 'SELECT * FROM blog_options';
+ try {
+ const res = await dbClient.query(queryText);
+ return res.rows;
+ } catch (err) {
+ console.log(err);
+ return [];
+ }
+ }
+
+ static async get(name) {
+ const res = await dbClient.query({
+ text: 'SELECT value FROM blog_options WHERE name=$1',
+ values: [name]
+ });
+
+ if (res.rows[0]) {
+ return res.rows[0].value;
+ }
+
+ console.log('[OptionController] cannot find option ', name);
+ return null;
+ }
+
+ static async create(name, value) {
+ const res = await dbClient.query({
+ text: 'INSERT INTO blog_options(name, value) VALUES($1, $2)',
+ values: [name, value]
+ });
+ console.log('[OptionController] create:', res.rows);
+ }
+
+ static async update(name, value) {
+ try {
+ const res = await dbClient.query({
+ text: 'UPDATE blog_options SET value = $2 WHERE name = $1',
+ values: [name, value]
+ });
+ return res.rows;
+ } catch(err) {
+ console.error('[OptionController] update error: ', err);
+ }
+ }
+
+ static async delete(name) {
+ try {
+ const res = dbClient.query({
+ text: 'DELETE FROM blog_options WHERE name=$1',
+ values: [name]
+ });
+ return res.rows;
+ } catch(err) {
+ console.error('[OptionController] delete error: ', err);
+ }
+ }
+}
diff --git a/app/server/controllers/Post.controller.js b/app/server/controllers/Post.controller.js
index 9af9497..1f1c1d5 100644
--- a/app/server/controllers/Post.controller.js
+++ b/app/server/controllers/Post.controller.js
@@ -5,6 +5,8 @@ export default class PostController {
let queryText = "SELECT * FROM posts";
if (options && options.order === "asc") {
+ queryText += ' ORDER BY created_at ASC';
+ } else {
queryText += ' ORDER BY created_at DESC';
}
diff --git a/app/server/db/migrations/2026-03-13_21:00_create_website_infos.sql b/app/server/db/migrations/2026-03-13_21:00_create_website_infos.sql
new file mode 100644
index 0000000..351cffe
--- /dev/null
+++ b/app/server/db/migrations/2026-03-13_21:00_create_website_infos.sql
@@ -0,0 +1,10 @@
+CREATE TABLE blog_options (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(255) NOT NULL UNIQUE,
+ value TEXT
+);
+
+CREATE INDEX blog_options_name_index ON blog_options (name);
+
+-- Remove useless index
+DROP INDEX index_passkeys;
diff --git a/app/server/middlewares/authentication.js b/app/server/middlewares/authentication.js
index e8003e2..c475674 100644
--- a/app/server/middlewares/authentication.js
+++ b/app/server/middlewares/authentication.js
@@ -5,6 +5,7 @@ const authenticateToken = function(req, res, next) {
if(!token) {
res.redirect("/");
+ return;
}
const data = token.split('.');
diff --git a/app/server/routes/conclave.js b/app/server/routes/conclave.js
new file mode 100644
index 0000000..2b22acf
--- /dev/null
+++ b/app/server/routes/conclave.js
@@ -0,0 +1,63 @@
+import express from 'express';
+import PostController from '../controllers/Post.controller.js';
+import OptionController from '../controllers/Option.controller.js';
+
+var router = express.Router();
+
+function handleOptionOperation (option) {
+ switch(option.action) {
+ case 'create':
+ OptionController.create(option.name, option.value);
+ break;
+ case 'update':
+ OptionController.update(option.name, option.value);
+ break;
+ case 'delete':
+ OptionController.delete(option.name);
+ break;
+ }
+}
+
+router.get('/', function (_, res) {
+ res.render('conclave/admin_panel');
+});
+
+router.get('/new_post', async function(_, res) {
+ res.render('conclave');
+});
+
+router.post('/posts/new', async function(req, res) {
+ await PostController.create(req.body.title, req.body.content);
+ res.redirect('/conclave');
+});
+
+router.get('/options', async function(_, res) {
+ const options = await OptionController.getAll();
+ res.render('conclave/options', { options });
+});
+
+router.get('/options/edit', async function (_, res) {
+ const options = await OptionController.getAll();
+ res.render('conclave/options_edit', { options });
+});
+
+// TODO: currently we send the whole options even the one that arent changed. we need to update the front-end to only send the updated ones. I think we will need javascript.
+router.post('/options/edit', async function (req, res) {
+ if (Array.isArray(req.body.name)) {
+ const options = req.body.name.map((name, index) => {
+ return {
+ name,
+ action: req.body.action[index],
+ value: req.body.value[index]
+ }
+ });
+
+ options.forEach(handleOptionOperation);
+ } else {
+ handleOptionOperation(req.body);
+ }
+
+ res.redirect('/conclave/options');
+});
+
+export default router;
diff --git a/app/server/routes/index.js b/app/server/routes/index.js
index 3a7d06d..f9cb2be 100644
--- a/app/server/routes/index.js
+++ b/app/server/routes/index.js
@@ -1,21 +1,12 @@
import express from 'express';
-var router = express.Router();
import PostController from '../controllers/Post.controller.js';
-import authenticateToken from '../middlewares/authentication.js';
+
+var router = express.Router();
/* GET home page. */
router.get('/', async function(_, res) {
const posts = await PostController.getAll();
- res.render('index', { title: 'Path to glory', posts: posts });
+ res.render('index', { posts: posts });
});
-router.get('/conclave', authenticateToken, async function(_, res) {
- res.render('conclave');
-});
-
-router.post('/conclave/new', authenticateToken, async function(req, res) {
- console.log(req.body);
- await PostController.create(req.body.title, req.body.content);
- res.redirect('/conclave');
-});
export default router;
diff --git a/app/server/rss/index.js b/app/server/rss/index.js
new file mode 100644
index 0000000..5dfabb8
--- /dev/null
+++ b/app/server/rss/index.js
@@ -0,0 +1,13 @@
+import { RssGenerator } from "./rssGenerator.js";
+
+let generator = null;
+
+export default (() => {
+ if (!generator) {
+ generator = Object.freeze(new RssGenerator({
+ filePath: '/home/node/rss/rss.xml',
+ }));
+ }
+
+ return generator;
+})();
diff --git a/app/server/rss/rssGenerator.js b/app/server/rss/rssGenerator.js
new file mode 100644
index 0000000..ceb6eeb
--- /dev/null
+++ b/app/server/rss/rssGenerator.js
@@ -0,0 +1,123 @@
+import fs from 'node:fs';
+
+export class RssGenerator {
+ maxItem = 10;
+ items = [];
+ channel = {};
+
+ constructor(options) {
+ this.options = options;
+ }
+
+ // TODO: move to pass an option parameter and iterate over the key/values
+ setChannel (title, description, link, pubDate) {
+ this.channel.title = title;
+ this.channel.description = description;
+ this.channel.link = link;
+ this.channel.pubDate = pubDate;
+
+ return this;
+ }
+
+ addItem (item) {
+ if (!this.isItemValid(item)) {
+ console.error('[RssGenerator] addItem: item invalid.', item);
+ return this;
+ }
+
+ this.items.push(item);
+
+ if (this.items.lenght > this.maxItem) {
+ this.items.shift();
+ }
+
+ return this;
+ }
+
+ isItemValid(item) {
+ return true;
+ }
+
+ generateFile() {
+ let data = '';
+
+ data += '