Compare commits

..

No commits in common. "b5c164236eb39ffc48674c078d3835f9340ee3be" and "adfc83c78f9049f07223d6d1ab5306e36a061680" have entirely different histories.

18 changed files with 239 additions and 84 deletions

View File

@ -1,4 +1,4 @@
name: Build and Push Backend Docker Image name: Build and Push Docker Image
on: on:
push: push:
@ -9,46 +9,34 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository with submodules - uses: actions/checkout@v4
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.ACCESS_TOKEN }}
- name: Initialize submodules
run: |
git submodule sync --recursive
git submodule update --init --recursive
git submodule status
ls -la src/modules/db
ls -la src/modules/redis
- name: Login to Registry - name: Login to Registry
uses: docker/login-action@v3 uses: docker/login-action@v2
with: with:
registry: gitea.spilum.net registry: gitea.spilum.net
username: ${{ secrets.REGISTRY_USER }} username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set SHORT_SHA - name: Build Docker Image
id: vars env:
run: echo "short_sha=${GITHUB_SHA::5}" >> "$GITHUB_OUTPUT" BRANCH_NAME: ${{ github.ref_name }}
SHORT_HASH: ${{ github.sha }}
run: |
# Build the image with the commit hash tag
docker build --build-arg BUILD_IDENTIFIER=${SHORT_HASH:0:5} -t gitea.spilum.net/spilum.net/mtf-backend:${BRANCH_NAME}-${SHORT_HASH:0:5} .
- name: Set up Docker Buildx # Tag the same image as "latest"
uses: docker/setup-buildx-action@v3 docker tag gitea.spilum.net/spilum.net/mtf-backend:${BRANCH_NAME}-${SHORT_HASH:0:5} gitea.spilum.net/spilum.net/mtf-backend:${BRANCH_NAME}-latest
- name: Build and Push Backend (multi-arch) - name: Push Docker Image
uses: docker/build-push-action@v6 env:
with: BRANCH_NAME: ${{ github.ref_name }}
context: . SHORT_HASH: ${{ github.sha }}
push: true run: |
platforms: linux/amd64,linux/arm64 docker push gitea.spilum.net/spilum.net/mtf-backend:${BRANCH_NAME}-${SHORT_HASH:0:5}
build-args: | docker push gitea.spilum.net/spilum.net/mtf-backend:${BRANCH_NAME}-latest
BUILD_IDENTIFIER=${{ steps.vars.outputs.short_sha }}
tags: |
gitea.spilum.net/spilum.net/mtf-backend:${{ steps.vars.outputs.short_sha }}
gitea.spilum.net/spilum.net/mtf-backend:latest
- name: Log out from registry - name: Log out from registry
if: always() if: always()
run: docker logout gitea.spilum.net run: docker logout gitea.spilum.net

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "src/modules/db"]
path = src/modules/db
url = https://gitea.spilum.net/Spilum.Net/mtf-module-db
[submodule "src/modules/redis"]
path = src/modules/redis
url = https://gitea.spilum.net/Spilum.Net/mtf-module-redis

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{ {
"name": "mtf-backend", "name": "spilum-backend",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mtf-backend", "name": "spilum-backend",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {

View File

@ -0,0 +1,4 @@
module.exports = {
REDIS_HOST: process.env.REDIS_HOST,
REDIS_PORT: process.env.REDIS_PORT
}

View File

@ -1,4 +1,5 @@
const {db} = require('../modules/db'); const models = require("../models");
const db = models.db;
const config = require("../config/auth.config"); const config = require("../config/auth.config");
const User = db.user; const User = db.user;
const Role = db.role; const Role = db.role;

View File

@ -1,4 +1,5 @@
const {db} = require('../modules/db'); const models = require("../models");
const db = models.db;
const {sequelize, Sequelize} = db; const {sequelize, Sequelize} = db;
const {literal} = Sequelize; const {literal} = Sequelize;
const Bill = db.bill; const Bill = db.bill;

View File

@ -1,10 +1,10 @@
const {db} = require('../modules/db'); const models = require("../models");
const db = models.db;
const redis = models.redis;
const {sequelize, Sequelize} = db; const {sequelize, Sequelize} = db;
const Intent = db.intent; const Intent = db.intent;
const config = require("../config"); const config = require("../config");
const {redis} = require('../modules/redis');
// Get all intents // Get all intents
exports.getAllIntents = async (req, res) => { exports.getAllIntents = async (req, res) => {
try { try {
@ -18,21 +18,7 @@ exports.getAllIntents = async (req, res) => {
} }
}; };
// Get intent by name
exports.getIntentByName = async (req, res) => {
try {
const intent = await Intent.findOne({where: {name: req.params.name}});
return res.status(200).send(intent);
} catch (err) {
console.error("getIntentByName error:", err);
return res.status(500).send({message: err.message || "Internal server error."});
}
}
// Get intent command (for now by description) // Get intent command (for now by description)
// [UNUSED FOR NOW] Doesn't work with the current intent model
exports.getIntentCommand = async (req, res) => { exports.getIntentCommand = async (req, res) => {
try { try {
const {text, device_id} = req.body; const {text, device_id} = req.body;
@ -45,6 +31,8 @@ exports.getIntentCommand = async (req, res) => {
console.log(`\n[Backend Intents] Processing intent.`); console.log(`\n[Backend Intents] Processing intent.`);
await redis.client.connect().catch(console.error)
const intent = await Intent.findOne({ where: { description: req.body.text } }); const intent = await Intent.findOne({ where: { description: req.body.text } });
if (intent) { if (intent) {
@ -59,6 +47,8 @@ exports.getIntentCommand = async (req, res) => {
} }
); );
await redis.client.quit();
console.log(`Intent matched and published to stream: ${commandId}`); console.log(`Intent matched and published to stream: ${commandId}`);
return res.json({ return res.json({
@ -78,6 +68,8 @@ exports.getIntentCommand = async (req, res) => {
} }
); );
await redis.client.quit();
console.log(`No intent match, error published: ${errorId}`); console.log(`No intent match, error published: ${errorId}`);
return res.status(400).json({ return res.status(400).json({
@ -89,6 +81,8 @@ exports.getIntentCommand = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
await redis.client.quit();
console.error("getIntentCommand error:", err); console.error("getIntentCommand error:", err);
return res.status(500).send({message: err.message || "Internal server error."}); return res.status(500).send({message: err.message || "Internal server error."});
@ -98,22 +92,38 @@ exports.getIntentCommand = async (req, res) => {
// Create intent // Create intent
exports.createIntent = async (req, res) => { exports.createIntent = async (req, res) => {
try { try {
const {name, examples} = req.body; const {command, description, type, device_id} = req.body;
console.log(`\n[Backend Intents] Creating intent.`) console.log(`\n[Backend Intents] Creating intent.`)
await redis.client.connect().catch(console.error); await redis.client.connect().catch(console.error);
if (!name || !examples) { if (!command || !description || !type || !device_id) {
return res.status(400).send({message: "Missing required fields: name, examples"}); return res.status(400).send({message: "Missing required fields: command, description, type, device_id"});
} }
const intent = await Intent.create({ const intent = await Intent.create({
name, command,
examples description,
type
}); });
return res.status(201).send({message: "Intent created successfully.", intent}); await redis.client.connect().catch(console.error);
const commandId = await redis.client.xAdd(
'communication',
'*',
{
device_id: device_id,
command: "CREATE_INTENT",
processed_at: new Date().toISOString(),
source: config.DEVICE_ID,
}
);
await redis.client.quit();
return res.status(201).send({message: "Intent created successfully.", intent, commandId});
} catch (err) { } catch (err) {
console.error("createIntent error:", err); console.error("createIntent error:", err);

View File

@ -1,6 +1,6 @@
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const config = require("../config/auth.config"); const config = require("../config/auth.config");
const {db} = require('../modules/db'); const db = require("../models");
const User = db.user; const User = db.user;
verifyToken = (req, res, next) => { verifyToken = (req, res, next) => {

View File

@ -1,4 +1,4 @@
const {db} = require('../modules/db'); const db = require('../models');
const ROLES = db.ROLES; const ROLES = db.ROLES;
const User = db.user; const User = db.user;

36
src/models/bill.model.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = (sequelize, DataTypes) => {
const Bill = sequelize.define("bills", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
amount: {
type: DataTypes.INTEGER,
allowNull: false
},
due: {
type: DataTypes.DATEONLY,
allowNull: false
},
type: {
type: DataTypes.STRING,
allowNull: true
},
shared: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
}
}, {
tableName: "bills",
timestamps: true
});
return Bill;
};

72
src/models/index.js Normal file
View File

@ -0,0 +1,72 @@
const config = require("../config");
const dbConfig = require("../config/db.config");
const redisConfig = require("../config/redis.config");
const Sequelize = require("sequelize");
const sequelize = new Sequelize(
dbConfig.DB,
dbConfig.USER,
dbConfig.PASSWORD,
{
host: dbConfig.HOST,
dialect: dbConfig.dialect,
port: dbConfig.PORT,
pool: {
max: dbConfig.pool.max,
min: dbConfig.pool.min,
acquire: dbConfig.pool.acquire,
idle: dbConfig.pool.idle
}
}
);
// SQL Connection
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.user = require('../models/user.model')(sequelize, Sequelize);
db.role = require('../models/role.model')(sequelize, Sequelize);
db.bill = require('../models/bill.model')(sequelize, Sequelize);
db.intent = require('../models/intent.model')(sequelize, Sequelize);
db.role.belongsToMany(db.user, {
through: "user_roles"
});
db.user.belongsToMany(db.role, {
through: "user_roles"
});
db.ROLES = ["user", "admin"];
// Redis Connection
const Redis = require('redis');
const redis = {};
redis.client = Redis.createClient({
url: `redis://default@${redisConfig.REDIS_HOST}:${redisConfig.REDIS_PORT}`
});
redis.ensureGroup = async (stream, group) => {
await redis.client.connect().catch(console.error);
try {
await redis.client.xGroupCreate(stream, group, "0", {MKSTREAM: true});
console.log(`Created consumer group: ${group} (${stream})`);
} catch (err) {
if (err.message.includes('BUSYGROUP')) {
console.log(`Consumer group already exists: ${group} (${stream})`);
} else {
console.error(err);
}
}
await redis.client.quit();
}
module.exports = {
db,
redis
};

View File

@ -0,0 +1,23 @@
module.exports = (sequelize, DataTypes) => {
const Intent = sequelize.define("intents", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
examples: {
type: DataTypes.ARRAY(DataTypes.STRING),
allowNull: true
}
}, {
tableName: "intents",
timestamps: false
});
return Intent;
};

19
src/models/role.model.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = (sequelize, DataTypes) => {
const Role = sequelize.define("roles", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: "roles",
timestamps: true
});
return Role;
}

15
src/models/user.model.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = (sequelize, Sequelize) => {
const User = sequelize.define("users", {
username: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
}
});
return User;
}

@ -1 +0,0 @@
Subproject commit 94a4550d0fb1b6e91dcbf8af78bb701720ad9bc4

@ -1 +0,0 @@
Subproject commit e41a75e7436a059ab963c5db7b62ca524cacc4e5

View File

@ -15,14 +15,10 @@ const controller = require("../controllers/intent.controller");
// Get all intents // Get all intents
router.get("/", /*[authJwt.verifyToken, authJwt.isAdmin],*/ controller.getAllIntents); router.get("/", /*[authJwt.verifyToken, authJwt.isAdmin],*/ controller.getAllIntents);
// Get intent by name // Get intent command (for now by description)
router.get("/name/:name", controller.getIntentByName); router.post("/getintentcommand", controller.getIntentCommand);
// Get intent command by description
// [UNUSED FOR NOW]
//router.post("/getintentcommand", controller.getIntentCommand);
// Create intent // Create intent
router.post("/", controller.createIntent); router.post("/createintent", controller.createIntent);
module.exports = router; module.exports = router;

View File

@ -8,19 +8,17 @@ const cors = require("cors");
const helmet = require("helmet"); const helmet = require("helmet");
const rateLimit = require("express-rate-limit"); const rateLimit = require("express-rate-limit");
const morgan = require("morgan"); const morgan = require("morgan");
const models = require('./models');
const {db} = require('./modules/db'); const db = models.db;
const redis = models.redis;
const {sequelize, Sequelize} = db; const {sequelize, Sequelize} = db;
const Role = db.role; const Role = db.role;
const {redis} = require('./modules/redis');
const authConfig = require("./config/auth.config"); const authConfig = require("./config/auth.config");
const config = require("./config"); const config = require("./config");
const createApp = async () => { const createApp = async () => {
await redis.connect(); await redis.ensureGroup("commands", `${config.DEVICE_ID}-cg`);
await redis.ensureGroup("commands", `${config.DEVICE_ID}`);
const app = express(); const app = express();
@ -60,7 +58,7 @@ const createApp = async () => {
app.use("/auth", require("./routes/auth.routes")); app.use("/auth", require("./routes/auth.routes"));
app.use("/user", require("./routes/user.routes")); app.use("/user", require("./routes/user.routes"));
app.use("/bills", require("./routes/bill.routes")); app.use("/bills", require("./routes/bill.routes"));
app.use("/intent", require("./routes/intent.routes")); app.use("/fallback/intent", require("./routes/intent.routes"));
// 404 handler // 404 handler
app.use((req, res) => res.status(404).send({message: "Not Found"})); app.use((req, res) => res.status(404).send({message: "Not Found"}));
@ -97,7 +95,7 @@ const start = async () => {
await initRoles(); await initRoles();
const app = await createApp(); const app = createApp();
// Set port and listen // Set port and listen
const PORT = parseInt(config.PORT, 10) || 5000; const PORT = parseInt(config.PORT, 10) || 5000;