add database code

This commit is contained in:
fullgream 2025-08-17 17:55:23 +03:00
parent 8471e5177c
commit 9544bc70d6
7 changed files with 344 additions and 96 deletions

View File

@ -31,14 +31,11 @@ class APIMethods {
info (con, req, cb) {
if (!req.fields) req.fields = "*";
if (!req.fieldsExtSource) req.fieldsExtSource = "*";
const cache = this.info.cache[
JSON.stringify({
f: req.fields, e: req.fieldsExtSource
})
];
const cacheKey = `${req.fields}\u0000${req.fieldsExtSource}`;
const cache = this.info.cache[cacheKey];
if (cache) {
console.debug("CACHE!");
return cb({ result: cache, trace_id: req.trace_id, ended: true });
}
@ -55,11 +52,7 @@ class APIMethods {
if (!fields[k]) serverInfo.extSource[k] = undefined;
});
}
this.info.cache[
JSON.stringify({
f: req.fields, e: req.fieldsExtSource
})
] = serverInfo;
this.info.cache[cacheKey] = serverInfo;
cb({ result: serverInfo, trace_id: req.trace_id, ended: true });
}
@ -104,6 +97,35 @@ class APIMethods {
});
}
async characters (con, req, cb) {
const authedMethod = this.authed;
const isAuthed = await (new Promise((rs, rj) => {
authedMethod(con, req, ({ result, ended }) => {
if (ended)
return rs(result);
});
}));
if (!isAuthed) {
return cb({
error: "require authorize",
trace_id: req.trace_id,
ended: true
});
}
// getting characters
const user = global.authed.get(con);
const result = (await user.getCharacters())
.map(character => {
return character;
});
cb({
result, trace_id: req.trace_id, ended: true
});
}
/*setSession (isEncrypted, address, { key, counter }, cb) {
if (!global.config.server.secureMode)
return cb({ error: "Encryption is off by configuration file into server" });

View File

@ -13,7 +13,7 @@ module.exports.registerToken = function (user) {
token = module.exports();
} while (global.authed.tokens[token] !== undefined);
global.authed.tokens[token] = user;
// (!) Потом добавить регистрацию в БД!
// TODO: Потом добавить регистрацию в БД!
return token;
};

View File

@ -9,26 +9,46 @@
* * 5 - root
*/
class User {
constructor(name, accessLevel) {
if (
typeof accessLevel !== "number" ||
{
"-1": true,
0: true,
1: true,
2: true,
3: true,
4: true,
5: true,
}["" + accessLevel] !== true
) {
throw new Error(
`Invalid access level: ${accessLevel} (must be ranged from -1 to 5)`,
const ACCESS_LEVELS = {
"-1": "banned",
0: "user",
1: "helper",
2: "moderator",
3: "admin",
4: "superadmin",
5: "root",
};
class AccessLevel {
static validate (value) {
const access = Object.fromEntries(Object.keys(ACCESS_LEVELS)
.map(level => [level, true]));
return (
Number.isInteger(value) &&
access["" + value] === true
);
}
constructor (accessLevel) {
if (!this.__proto__.constructor.validate(accessLevel))
throw new Error(
`Invalid access level: ${accessLevel}`,
);
this.code = accessLevel;
}
get accessName () {
return ACCESS_LEVELS[""+this.code]
}
}
class User {
constructor (name, accessLevel) {
this.name = name;
if (accessLevel instanceof AccessLevel)
this.accessLevel = accessLevel;
else
this.accessLevel = new AccessLevel(accessLevel);
}
static async getUserByName(name) {
@ -41,8 +61,15 @@ class User {
User.cache[name] = user;
return user;
}
// Потом добавить чтение из БД
// TODO: Потом добавить чтение из БД
//return null;
const userdata = await global.database.getUserRowByName(name);
if (!userdata)
return null;
const user = new User(userdata.username, userdata.accessLevel);
User.cache[name] = user;
return user;
}
static getByToken(token) {
@ -50,13 +77,27 @@ class User {
return user ?? null;
}
getUserForAPI(getFullInfo = false) {
return {
getUserForAPI(getFullInfo = false, fields = "*") {
const getted = {
username: this.name,
accessLevel: this.accessLevel,
accessLevel: this.accessLevel.code,
};
if (fields !== "*") {
const selected = Object.fromEntries(fields.split(/\s*,\s*/g).map(field => [field, true]));
for (let field in getted) {
if (!selected[field])
getted[field] = undefined;
}
}
return getted;
}
async getCharacters () {
return [];
}
}
User.cache = {};
module.exports = { User };
module.exports = { User, AccessLevel };

View File

@ -18,7 +18,8 @@
},
"database": {
"dialect": "sqlite",
"path": "./data"
"storage": "./data.db",
"logging": false
},
"ai": {
"apiType": "kobold"

124
server/database/dbModel.js Normal file
View File

@ -0,0 +1,124 @@
const { DataTypes } = require("sequelize");
const { AccessLevel } = require("../api/utils/user");
const logger = require("../logger");
const bcrypt = require("bcrypt");
/*const SALT_ROUNDS = Math.round(Math.r) + 6;
const BCRYPT_SALT = bcrypt.genSaltSync(SALT_ROUNDS);*/
function getSalt () {
//return bcrypt.genSaltSync(10);
return bcrypt.genSaltSync(Math.round(Math.random() * 4) + 6);
}
module.exports = async function (sequelize, dbObj) {
const dbModel = {
users: sequelize.define("users", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "ID of user",
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: "Username of user",
validate: {
not: /null/iu,
},
},
accessLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: false,
comment: "access level of user",
get: function () {
return new AccessLevel(this.getDataValue("accessLevel"));
},
set: function (value) {
if (value instanceof AccessLevel)
return this.setDataValue("accessLevel", value.code);
if (Number.isInteger(value)) {
if (AccessLevel.validate(value))
return this.setDataValue("accessLevel", value);
}
throw new TypeError("accessLevel must be AccessLevel object or integer");
//this.setDataValue('inventory', JSON.stringify(value));
},
},
password: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
comment: "Hash code of password",
set: function (value) {
const hash = bcrypt.hashSync(value, getSalt());
this.setDataValue('password', hash);
},
get: function () {
const checkHash = this.getDataValue("password");
return async function (password) {
return await new Promise((rs, rj) => {
bcrypt.compare(password, checkHash, (err, res) => {
if (err)
return rs(false);
logger.debug("isRes is", res);
rs(res);
});
});
}
},
},
isActivate: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: "Is user activated",
},
}),
userCharacters: sequelize.define("userCharacters", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "ID of character",
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: "Name of character",
},
ownerId: {
type: DataTypes.INTEGER,
allowNull: false,
comment: "ID of character's owner",
},
}),
};
dbModel.userCharacters.belongsTo(dbModel.users, {
foreignKey: "ownerId", // field in userCharacters
targetKey: "id", // field in users
onDelete: "CASCADE", // If delete user, characters will be deleted
onUpdate: "CASCADE",
});
dbModel.users.hasMany(dbModel.userCharacters, {
foreignKey: "ownerId", // field ownerId
sourceKey: "id",
});
await sequelize.authenticate();
const initPromise = Object.entries(dbModel).map(async ([tableName, table]) => {
logger.log(`* init table: ${tableName}`);
await table.sync({ alter: true });
const rows = await table.findAll();
await dbObj.cache[tableName]?.(rows, table);
logger.log(`* inited: ${tableName}`);
});
await Promise.all(initPromise);
};

89
server/database/index.js Normal file
View File

@ -0,0 +1,89 @@
const { Sequelize } = require("sequelize");
const dbModel = require("./dbModel");
const logger = require("../logger");
class DatabaseCache {
async users (rows, table) {
// Add root user only where root user is activated
if (global.config.rootuser.activated) {
const isRootHasOnDB = rows.find(x => x.username === global.config.rootuser.login);
const rootLevel = 5; // TODO: replace it to AccessLevel.root in future
if (!isRootHasOnDB) {
await table.create({
username: global.config.rootuser.login,
accessLevel: rootLevel,
password: "",
});
return await this.users((
await table.findAll()
), table);
} else if (!rows.find(x => x.username === global.config.rootuser.login && x.accessLevel.code === rootLevel)) {
throw new Error(`Username ${
global.config.rootuser.login
} is busy`);
}
}
this.users.rows = rows;
this.users.byUsername = new Object();
this.users.byId = new Object();
for (let row of rows) {
this.users.byUsername[row.username] = row;
this.users.byId[row.id] = row;
}
// Add user info
this.users.add = async (row, isCreate = false) => {
if (isCreate)
await table.create(row);
let addedData = null;
if (row.username)
addedData = await table.findAll({
where: {
username: row.username
}
})[0];
else if (row.id)
addedData = await table.findAll({
where: {
id: row.id
}
})[0];
if (!addedData) throw new Error("Invalid row information");
this.users.rows.push(addedData);
// By keys
this.users.byUsername[addedData.username] = addedData;
this.users.byId[addedData.id] = addedData;
}
}
}
class Database {
constructor(sequlizeData) {
this.sequlize = new Sequelize(sequlizeData);
}
async init(fromHandler = false) {
global.server.toggleLock(true);
this.cache = new DatabaseCache();
await dbModel(this.sequlize, this);
global.server.toggleLock(false);
if (!fromHandler)
global.server.on("config.update", async ({
oldConfig, newConfig
}) => {
if (JSON.stringify(oldConfig.database) !== JSON.stringify(newConfig.database))
await this.init(true);
});
}
// Return ROW of 'users' table by username
async getUserRowByName (name) {
return this.cache.users.byUsername[name];
}
}
module.exports = Database;

View File

@ -4,6 +4,8 @@ const parser = require("./packet-parser");
const frontend = require("./frontend");
const websocket = require("./ws-handler");
const crypt = require("./crypt");
const Database = require("./database");
const EventEmitter = require("events");
const net = require("net");
const fs = require("fs");
@ -11,10 +13,29 @@ const path = require("path");
const { API } = require("./api");
// So.. what part of the WeakMap/Server class made you think
// Oh! Developer of this AI client agrees that
// [Object Server].isLocked can be writeable?
const isLockedServer = new WeakMap();
class Server extends EventEmitter {
constructor() {
super();
this.on("config.update", ({ oldConfig, newConfig }) => {});
//this.isLocked = false;
isLockedServer.set(this, false);
}
set isLocked (_) {
throw new Error("isLocked not writeable");
}
get isLocked () {
return isLockedServer.get(this);
}
toggleLock (mode = true) {
isLockedServer.set(this, !!mode);
}
run(address, port) {
@ -185,7 +206,8 @@ const defaultConfigData = {
},
database: {
dialect: "sqlite",
path: "./data",
storage: "./data.db",
logging: false
},
ai: {
apiType: "kobold",
@ -199,63 +221,12 @@ const defaultConfigData = {
global.server = new Server();
cfgHandle(global.server, defaultConfigData, logger);
logger.log("Current config:", global.config);
global.server.run(global.config.server.address, global.config.server.port);
/*
if (global.config.server.secureMode) {
async function initEncryption () {
const datadir = global.config.server["ssl-path"] ?? path.join(__dirname, "/encryption");
function generate () {
logger.log("Generate new keys...");
global.openSSH = crypt.generateOpenSSH({
commonName: global.config.server.info.name
}, {
days: 365,
pkcs7: true,
clientCertificate: true,
clientCertificateCN: "AI Adventure Labs",
algorithm: 'sha256',
keySize: 2048,
global.database = new Database(global.config.database);
global.database.init()
.then(() => {
logger.log("Current config:", global.config);
global.server.run(global.config.server.address, global.config.server.port);
})
.catch((err) => {
throw err;
});
fs.writeFileSync(path.join(datadir, "public.pem"), global.openSSH.public_key.bytes);
fs.writeFileSync(path.join(datadir, "private.pem"), global.openSSH.private_key.bytes);
fs.writeFileSync(path.join(datadir, "cert.crt"), global.openSSH.cert);
}
const readFilePromise = new Promise((resolve, reject) => {
fs.readFile(path.join(datadir, "public.pem"), (err, public_key) => {
if (err) {
return resolve(generate());
}
try {
public_key = new crypt.CryptKey(public_key);
const private_key = new crypt.CryptKey(fs.readFileSync(path.join(datadir, "private.pem")));
const cert = fs.readFileSync(path.join(datadir, "cert.crt"));
global.openSSH = new crypt.OpenSSH({
public_key, private_key, cert
});
resolve();
} catch (_) {
resolve(generate());
}
});
});
await readFilePromise;
logger.log(`loaded from ${datadir}`);
};
initEncryption().then(() => {
server.run(global.config.server.address, global.config.server.port);
}).catch(e => {
throw e;
});
}
else
server.run(global.config.server.address, global.config.server.port);
*/