Skip to content

Commit

Permalink
Add Panel with params
Browse files Browse the repository at this point in the history
  • Loading branch information
serguun42 committed Dec 9, 2023
1 parent d9325e0 commit c840105
Show file tree
Hide file tree
Showing 25 changed files with 1,069 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
cp $CONFIGS_LOCATION_BASE/notifier-and-logger.config.json ./notifier/;
cp $CONFIGS_LOCATION_BASE/scrapper.config.json ./scrapper/;
cp $CONFIGS_LOCATION_BASE/backend.config.json ./backend/;
cp $CONFIGS_LOCATION_BASE/panel.config.json ./panel/;
cp $CONFIGS_LOCATION_BASE/healthcheck.config.json ./healthcheck/;
cp $CONFIGS_LOCATION_BASE/telegram-bot.config.json ./telegram/
[[ -f $CONFIGS_LOCATION_BASE/tag-manager.html ]] &&
Expand Down
8 changes: 4 additions & 4 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
.vscode
/node_modules/
node_modules

/data/
/out/
/local-stuff/
data
out
local-stuff
tsconfig.json
*.bat
*.example.json
11 changes: 10 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ volumes:
name: mss-keycloak-postgres

services:
mss-backend:
mss-backend:
container_name: mss-backend
image: mss-backend:latest
build:
Expand Down Expand Up @@ -89,3 +89,12 @@ services:
volumes:
- mss-keycloak-postgres:/var/lib/postgresql/data
- ./keycloak/postgresql.conf:/etc/postgresql/postgresql.conf

mss-panel:
container_name: mss-panel
image: mss-panel:latest
build:
context: ./panel
restart: always
ports:
- 127.0.0.1:${PANEL_PUBLISH_PORT}:80
3 changes: 3 additions & 0 deletions panel/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
dist/
node_modules/
9 changes: 9 additions & 0 deletions panel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.vscode
node_modules

data
out
local-stuff
tsconfig.json
*.bat
*.example.json
9 changes: 9 additions & 0 deletions panel/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:lts-alpine

WORKDIR /usr/src/app
COPY . /usr/src/app

ENV NODE_ENV=production
RUN npm ci --omit=dev

CMD [ "npm", "run", "production" ]
14 changes: 14 additions & 0 deletions panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# MIREA Schedule System

## Panel

Panel for changing MSS params, runs behind NGINX, uses Keycloak to validate user.

## Commands

1. Install all dependencies `npm ci --only=prod`
2. Run backend `npm run production`

## Some other files

`panel.config.json` – Config file with ports, DB, logging, Keycloak, etc.
39 changes: 39 additions & 0 deletions panel/auth/exchange-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fetch from "node-fetch";
import ReadConfig from "../util/read-config.js";

const PANEL_CONFIG = ReadConfig();

/**
* @param {string} code
* @returns {Promise<string>}
*/
export default function ExchangeCodeToToken(code) {
if (!code) return Promise.reject(new Error("No code was passed"));

return fetch(`${PANEL_CONFIG.KEYCLOAK_ORIGIN}/realms/${PANEL_CONFIG.KEYCLOAK_REALM}/protocol/openid-connect/token`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "MSS-Panel/1.0.0"
},
body: `client_id=${encodeURIComponent(PANEL_CONFIG.KEYCLOAK_CLIENT_ID)}&client_secret=${encodeURIComponent(
PANEL_CONFIG.KEYCLOAK_CLIENT_SECRET
)}&grant_type=authorization_code&scope=${encodeURIComponent(
["openid", "profile"].join(" ")
)}&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(PANEL_CONFIG.PANEL_ORIGIN)}`
})
.then((res) => {
if (!res.ok)
return res.text().then(
(text) => Promise.reject(new Error(`Keycloak returned ${res.status} ${res.statusText}${text}`)),
() => () => Promise.reject(new Error(`Keycloak returned ${res.status} ${res.statusText}`))
);

return res.json();
})
.then(
/** @param {import('../types').TokenResponse} tokenResponse */ (tokenResponse) =>
Promise.resolve(tokenResponse.access_token)
);
}
39 changes: 39 additions & 0 deletions panel/auth/validate-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fetch from "node-fetch";
import ReadConfig from "../util/read-config.js";

const PANEL_CONFIG = ReadConfig();

/**
* @param {string} token
* @returns {Promise<void>}
*/
export default function ValidateAccessToken(token) {
if (!token) return Promise.reject(new Error("No token was passed"));

return fetch(
`${PANEL_CONFIG.KEYCLOAK_ORIGIN}/realms/${PANEL_CONFIG.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
{
method: "POST",
headers: {
Accept: "application/json",
"User-Agent": "MSS-Panel/1.0.0",
Authorization: `Bearer ${token}`
}
}
)
.then((res) => {
if (!res.ok)
return res.text().then(
(text) => Promise.reject(new Error(`Keycloak returned ${res.status} ${res.statusText}${text}`)),
() => () => Promise.reject(new Error(`Keycloak returned ${res.status} ${res.statusText}`))
);

return res.json();
})
.then(
/** @param {import('../types').UserInfo} userInfo */ (userInfo) =>
userInfo.email_verified && userInfo.email === PANEL_CONFIG.PANEL_USER_EMAIL
? Promise.resolve()
: Promise.reject(new Error("Email validation failed"))
);
}
100 changes: 100 additions & 0 deletions panel/database/dispatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { MongoClient } from "mongodb";
import ReadConfig from "../util/read-config.js";
import Logging from "../util/logging.js";

const MONGO_CONNECTION_URL = ReadConfig().DATABASE_CONNECTION_URI || "mongodb://127.0.0.1:27017/";

/**
* @callback MongoDispatcherCallback
* @param {import("mongodb").Db} db
* @returns {void}
*/

/**
* @class
* @classdesc Various events and callbacks for DB
*/
export default class MongoDispatcher {
/**
* @param {string} dbName
*/
constructor(dbName) {
/**
* @private
* @type {DB}
*/
this.DB = null;

/**
* @private
* @type {{[eventName: string]: MongoDispatcherCallback[]}}
*/
this.events = {};

MongoClient.connect(MONGO_CONNECTION_URL, {})
.then((connectedClient) => {
this.DB = connectedClient.db(dbName);

this.on("close", () => {
this.DB = null;
connectedClient.close();
});
})
.catch((e) => {
Logging("Error with connection to MongoDB on start-up", mongoError);
});
}

/**
* @param {string} eventName
* @param {MongoDispatcherCallback} eventHandler
* @returns {void}
*/
on(eventName, eventHandler) {
if (!this.events[eventName] || !(this.events[eventName] instanceof Array)) this.events[eventName] = [];

this.events[eventName].push(eventHandler);
}

/**
* @param {string} eventName
* @returns {void}
*/
off(eventName) {
delete this.events[eventName];
}

/**
* @param {string} eventName
* @returns {void}
*/
dispatchEvent(eventName) {
if (this.events[eventName] && this.events[eventName] instanceof Array)
this.events[eventName].forEach((eventHandler) => {
if (typeof eventHandler == "function") eventHandler(this.DB);
});
}

/**
* @returns {void}
*/
closeConnection() {
this.dispatchEvent("close");
}

/**
* @returns {Promise<import('mongodb').Db>}
*/
callDB() {
return new Promise((resolve) => {
if (this.DB) return resolve(this.DB);

const waitingInterval = setInterval(() => {
if (this.DB) {
clearInterval(waitingInterval);
resolve(this.DB);
}
});
});
}
}
40 changes: 40 additions & 0 deletions panel/database/methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import ReadConfig from "../util/read-config.js";
import MongoDispatcher from "./dispatcher.js";

const mongoDispatcher = new MongoDispatcher(ReadConfig().DATABASE_NAME);

const DB_METHODS = {
listAllParams() {
return mongoDispatcher
.callDB()
.then((db) => db.collection("params").find({}).project({ name: true, value: true, _id: false }).toArray());
},

/**
* @param {{ name: string, value: any }} [payload]
*/
setParam(payload) {
if (typeof payload !== "object") return Promise.reject(new Error("Malformed payload"));
if (!("name" in payload)) return Promise.reject(new Error("Missing name"));
if (!("value" in payload)) return Promise.reject(new Error("Missing value"));
if (typeof payload.name !== "string") return Promise.reject(new Error("Malformed name"));

const trueParamValue =
payload.name === "start_of_weeks"
? parseInt(payload.value)
: payload.name === "scrapper_updated_date"
? new Date(payload.value)
: payload.name === "scrapper_interval_seconds"
? parseInt(payload.value)
: payload.value;

return mongoDispatcher
.callDB()
.then((db) =>
db.collection("params").updateOne({ name: payload.name }, { $set: { value: trueParamValue } }, { upsert: true })
)
.then((updateResult) => Promise.resolve(updateResult.modifiedCount));
}
};

export default DB_METHODS;
Loading

0 comments on commit c840105

Please sign in to comment.