This commit is contained in:
2024-01-05 22:06:34 -05:00
parent c8f00cdeaf
commit abc547c642
34 changed files with 6586 additions and 1561 deletions

7
models/index.js Normal file
View File

@ -0,0 +1,7 @@
'use strict';
module.exports = {
User: require('./ldap/user').User,
AuthToken: require('./sql').AuthToken,
Torrent: require('./sql').Torrent
};

99
models/ldap/group.js Normal file
View File

@ -0,0 +1,99 @@
'use strict';
const { Client, Attribute, Change } = require('ldapts');
const conf = require('>/conf').ldap;
const client = new Client({
url: conf.url,
});
async function getGroups(client, member){
try{
let memberFilter = member ? `(member=${member})`: ''
let groups = (await client.search(conf.groupBase, {
scope: 'sub',
filter: `(&(objectClass=groupOfNames)${memberFilter})`,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
})).searchEntries;
return groups.map(function(group){
if(!Array.isArray(group.member)) group.member = [group.member];
return group
});
}catch(error){
throw error;
}
}
var Group = {};
Group.list = async function(member){
try{
await client.bind(conf.bindDN, conf.bindPassword);
let groups = await getGroups(client, member)
await client.unbind();
return groups.map(group => group.cn);
}catch(error){
throw error;
}
}
Group.listDetail = async function(member){
try{
await client.bind(conf.bindDN, conf.bindPassword);
let groups = await getGroups(client, member)
await client.unbind();
return groups;
}catch(error){
throw error;
}
}
Group.get = async function(data){
try{
if(typeof data !== 'object'){
let name = data;
data = {};
data.name = name;
}
await client.bind(conf.bindDN, conf.bindPassword);
let group = (await client.search(conf.groupBase, {
scope: 'sub',
filter: `(&(objectClass=groupOfNames)(cn=${data.name}))`,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
})).searchEntries[0];
await client.unbind();
if(!Array.isArray(group.member)) group.member = [group.member];
if(group){
let obj = Object.create(this);
Object.assign(obj, group);
return obj;
}else{
let error = new Error('GroupNotFound');
error.name = 'GroupNotFound';
error.message = `LDAP:${data.cn} does not exists`;
error.status = 404;
throw error;
}
}catch(error){
throw error;
}
}
module.exports = {Group};

6
models/ldap/index.js Normal file
View File

@ -0,0 +1,6 @@
'use strict';
module.exports = {
User: require('./user').User,
Group: require('./group').Group,
};

159
models/ldap/user.js Normal file
View File

@ -0,0 +1,159 @@
'use strict';
const {Client, Attribute} = require('ldapts');
const LRUCache = require('lru-native2');
const conf = require('>/conf').ldap;
var userLUR = new LRUCache({
// The maximum age (in milliseconds) of an item.
// The item will be removed if get() is called and the item is too old.
// Default: 0, meaning items will never expire.
maxAge: 60000,
});
const client = new Client({
url: conf.url,
});
const user_parse = function(data){
if(data[conf.userNameAttribute]){
data.username = data[conf.userNameAttribute];
data.userPassword = undefined;
data.userBacking = "LDAP";
}
return data;
}
var User = {}
User.backing = "LDAP";
User.list = async function(){
try{
await client.bind(conf.bindDN, conf.bindPassword);
const res = await client.search(conf.userBase, {
scope: 'sub',
filter: conf.userFilter,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
});
await client.unbind();
return res.searchEntries.map(function(user){return user.uid});
}catch(error){
throw error;
}
};
User.listDetail = async function(){
try{
await client.bind(conf.bindDN, conf.bindPassword);
const res = await client.search(conf.userBase, {
scope: 'sub',
filter: conf.userFilter,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
});
await client.unbind();
let users = [];
for(let user of res.searchEntries){
let obj = Object.create(this);
Object.assign(obj, user_parse(user));
users.push(obj);
}
return users;
}catch(error){
throw error;
}
};
User.get = async function(data, key){
try{
if(typeof data !== 'object'){
let uid = data;
data = {};
data.uid = uid;
}
data.searchKey = data.searchKey || key || conf.userNameAttribute;
data.searchValue = data.searchValue || data.uid;
let filter = `(&${conf.userFilter}(${data.searchKey}=${data.searchValue}))`;
if(userLUR.get(filter)) return userLUR.get(filter)
await client.bind(conf.bindDN, conf.bindPassword);
const res = await client.search(conf.userBase, {
scope: 'sub',
filter: filter,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
});
await client.unbind();
if(res.searchEntries[0]){
let obj = Object.create(this);
Object.assign(obj, user_parse(res.searchEntries[0]));
userLUR.set(filter, obj);
return obj;
}else{
let error = new Error('UserNotFound');
error.name = 'UserNotFound';
error.message = `LDAP:${data.searchValue} does not exists`;
error.status = 404;
throw error;
}
}catch(error){
throw error;
}
};
User.exists = async function(data, key){
// Return true or false if the requested entry exists ignoring error's.
try{
await this.get(data, key);
return true
}catch(error){
return false;
}
};
User.login = async function(data){
try{
let user = await this.get(data.uid || data[conf.userNameAttribute] || data.username);
await client.bind(user.dn, data.password);
await client.unbind();
return user;
}catch(error){
throw error;
}
};
module.exports = {User};
// (async function(){
// try{
// console.log(await User.list());
// console.log(await User.listDetail());
// console.log(await User.get('wmantly'))
// }catch(error){
// console.error(error)
// }
// })()

49
models/sql/authtoken.js Normal file
View File

@ -0,0 +1,49 @@
'use strict';
module.exports = (sequelize, DataTypes, Model) => {
class AuthToken extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
}
check(){
// check expires_on date
return this.is_valid;
}
}
AuthToken.init({
token:{
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
primaryKey: true
},
expires_on: {
type: DataTypes.DATE,
allowNull: true,
validate:{
isDate:true
}
},
username: {
type: DataTypes.STRING,
ldapModel: 'User',
allowNull: false,
validate:{
notNull: true,
},
},
is_valid: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
sequelize,
modelName: 'AuthToken',
});
return AuthToken;
};

View File

@ -0,0 +1,3 @@
const conf = require('../../../conf');
module.exports = conf.sql;

109
models/sql/index.js Normal file
View File

@ -0,0 +1,109 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { Sequelize, Utils } = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/config/config.js');
const db = {};
let sequelize;
// Connect sequelize models to LDAP models
const ldapModels = require('../ldap');
function getFieldWithLdap(attributes){
let out = [];
for (const [attribute, options] of Object.entries(attributes)) {
if(options.ldapModel) out.push(attribute)
}
return out;
}
const sequilze_conf = {
define: {
hooks: {
async afterValidate(instance) {
let hasError = false;
function itemError(key, validator, message){
let error = new Sequelize.ValidationErrorItem(message);
error.type = 'Validation error';
error.path = key;
error.origin = 'FUNCTION';
error.instance = instance;
error.validatorKey = 'validator';
error.validatorName = 'validator';
error.validatorArgs = [];
error.original = [];
throw new Sequelize.ValidationError(null, [error]);
}
for(let attribute of getFieldWithLdap(this.getAttributes())){
let externalModel = ldapModels[this.getAttributes()[attribute].ldapModel];
if(!externalModel) itemError(attribute, 'modelExists', `LDAP model ${this.getAttributes()[attribute].ldapModel} not found.`);
if(!hasError && !(await externalModel.exists(instance[attribute])) ) itemError(attribute, 'foreignKey', `LDAP model has no object ${instance[attribute]}`);
}
}
}
}
}
class _Model extends Sequelize.Model{
constructor(...args){
super(...args)
let hasLdap = getFieldWithLdap(this.constructor.getAttributes())
for(let attribute of hasLdap){
let externalModelName = this.constructor.getAttributes()[attribute].ldapModel;
this[`get${externalModelName}`] = async function(){
return await ldapModels[externalModelName].get(this[attribute]);
}
}
}
// Backward compatible with my LDAP and Redis models.
// I will update the LDAP and Redis stuff to have method interfaces inline with sequilze
static async get(token){
return await this.findByPk(token);
}
}
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, {...config, ...sequilze_conf});
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes, _Model);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@ -0,0 +1,38 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('AuthTokens', {
token: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
allowNull: false,
primaryKey: true
},
is_valid: {
type: Sequelize.BOOLEAN,
defaultValue: true,
allowNull: false,
},
username: {
type: Sequelize.STRING,
allowNull: false,
},
expires_on: {
allowNull: true,
type: Sequelize.DATE
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('AuthTokens');
}
};

View File

@ -0,0 +1,63 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Torrents', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
torrent_id: {
type: Sequelize.STRING
},
hashString: {
type: Sequelize.STRING
},
magnetLink: {
type: Sequelize.STRING
},
name: {
type: Sequelize.STRING
},
status: {
type: Sequelize.NUMBER
},
isPrivate: {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
},
percentDone: {
type: Sequelize.FLOAT
},
errorString: {
type: Sequelize.STRING,
allowNull: true
},
downloadDir: {
type: Sequelize.STRING,
allowNull: true
},
sizeWhenDone: {
type: Sequelize.NUMBER,
allowNull: true
},
added_by: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Torrents');
}
};

126
models/sql/torrent.js Normal file
View File

@ -0,0 +1,126 @@
'use strict';
const Transmission = require('transmission-promise');
const conf = require('>/conf');
const tr_client = new Transmission(conf.transmission)
const statusMap = [
'STOPPED',
'CHECK_WAIT',
'CHECK',
'DOWNLOAD_WAIT',
'DOWNLOAD',
'SEED_WAIT',
'SEED',
'ISOLATED',
];
module.exports = (sequelize, DataTypes, Model) => {
class Torrent extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
static trClient = tr_client;
static async create(data, ...args){
try{
// let instance = this.build(data);
// console.log('instance', instance)
await this.build(data).validate();
// console.log('validate', val);
let res = await tr_client.addUrl(data.magnetLink);
return await super.create({
magnetLink: data.magnetLink,
torrent_id: res.id,
hashString: res.hashString,
name: res.name,
added_by: data.added_by,
status: 0,
percentDone: 0,
}, args);
}catch (error){
// console.log('Torrent create error', error);
throw error;
}
}
async getTorrentData(noUpdate){
try{
let res = ( await tr_client.get(Number(this.torrent_id), [
"eta", "percentDone", "status", "rateDownload",
"errorString", "hashString", 'name',
'downloadDir',
'files', //array of files
'filesStats', // array of files with status
'isFinished',
'isStalled',
'peers',
'peersConnected', // array of peers,
'sizeWhenDone',
]) ).torrents[0];
await this.update(res);
if(noUpdate) await this.save();
return {...res, ...this.dataValues};
}catch(error){
console.error(`Torrent ${this.id} getTorrentData error`, error);
throw error;
}
}
}
Torrent.init({
torrent_id: DataTypes.STRING,
magnetLink: {
type: DataTypes.STRING,
allowNull: false,
validate:{
notNull: true,
notEmpty: true,
},
},
isPrivate: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
hashString: DataTypes.STRING,
name: DataTypes.STRING,
added_by: {
type: DataTypes.STRING,
ldapModel: 'User',
allowNull: false,
validate:{
notNull: true,
notEmpty: true,
},
},
status: DataTypes.NUMBER,
percentDone: DataTypes.FLOAT,
downloadDir: {
type: DataTypes.STRING,
allowNull: true,
},
errorString: {
type: DataTypes.STRING,
allowNull: true,
},
sizeWhenDone: {
type: DataTypes.NUMBER,
allowNull: true,
},
}, {
sequelize,
modelName: 'Torrent',
logging: false,
});
return Torrent;
};