add database code
This commit is contained in:
parent
8471e5177c
commit
9544bc70d6
@ -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" });
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -18,7 +18,8 @@
|
||||
},
|
||||
"database": {
|
||||
"dialect": "sqlite",
|
||||
"path": "./data"
|
||||
"storage": "./data.db",
|
||||
"logging": false
|
||||
},
|
||||
"ai": {
|
||||
"apiType": "kobold"
|
||||
|
124
server/database/dbModel.js
Normal file
124
server/database/dbModel.js
Normal 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
89
server/database/index.js
Normal 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;
|
@ -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);
|
||||
|
||||
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);
|
||||
|
||||
/*
|
||||
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,
|
||||
})
|
||||
.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);
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user