Adding new Admin UI. Updating Fredy to V3.0.0 as it has been a large rewrite. Thanks for all contributions and help on the way!
This commit is contained in:
Christian Kellner
2021-01-21 16:09:23 +01:00
committed by GitHub
parent 8185bfe818
commit b2847f6834
124 changed files with 9768 additions and 1495 deletions

View File

@@ -1,68 +1,42 @@
const { notificationAdapterRouter } = require('./routes/notificationAdapterRouter');
const { authInterceptor, cookieSession, adminInterceptor } = require('./security');
const { analyticsRouter } = require('./routes/analyticsRouter');
const { providerRouter } = require('./routes/providerRouter');
const { loginRouter } = require('./routes/loginRoute');
const config = require('../../conf/config.json');
const { userRouter } = require('./routes/userRoute');
const { jobRouter } = require('./routes/jobRouter');
const bodyParser = require('body-parser');
const config = require('../../conf/config');
const { getLastJobExecution, getLastProviderExecution, getTotalNumberOfListings } = require('../services/store');
const PORT = config.infoApiPort || 9998;
const service = require('restana')();
const enabled = config.infoApi == null ? false : config.infoApi;
const files = require('serve-static');
const path = require('path');
const staticService = files(path.join(__dirname, '../../ui/public'));
const PORT = config.port || 9998;
service.use(bodyParser.json());
service.get('/', async (req, res) => {
const result = {};
Object.keys(config.jobs).forEach((job) => {
result[job] = {
lastExecution: getLastJobExecution(job),
enabledProvider: Object.keys(config.jobs[job].provider)
.filter((providerKey) => config.jobs[job].provider[providerKey].enabled)
.map((providerKey) => {
return {
name: providerKey,
lastExecution: getLastProviderExecution(job, providerKey),
totalFindings: getTotalNumberOfListings(job, providerKey),
};
}),
};
});
res.body = result;
res.send();
});
service.use(cookieSession());
service.get('/jobs/:name', async (req, res) => {
const { name: jobKey } = req.params;
if (Object.keys(config.jobs).indexOf(jobKey) === -1) {
console.error(`Cannot find job with name ${jobKey}. Available Jobs are [${Object.keys(config.jobs)}]`);
res.send(404);
return;
}
res.body = {
lastExecution: getLastJobExecution(jobKey),
enabledProvider: Object.keys(config.jobs[jobKey].provider)
.filter((providerKey) => config.jobs[jobKey].provider[providerKey].enabled)
.map((providerKey) => {
return {
name: providerKey,
url: config.jobs[jobKey].provider[providerKey].url,
lastExecution: getLastProviderExecution(jobKey, providerKey),
totalFindings: getTotalNumberOfListings(jobKey, providerKey),
};
}),
};
res.send();
});
service.use(staticService);
service.get('/ping', function (req, res) {
res.body = {
pong: 'pong',
};
res.send();
});
service.use('/api/admin', authInterceptor());
service.use('/api/jobs', authInterceptor());
// /admin can only be accessed when user is having admin permissions
service.use('/api/admin', adminInterceptor());
service.use('/api/jobs/notificationAdapter', notificationAdapterRouter);
service.use('/api/jobs/provider', providerRouter);
service.use('/api/jobs/insights', analyticsRouter);
service.use('/api/admin/users', userRouter);
service.use('/api/jobs', jobRouter);
service.use('/api/login', loginRouter);
/* eslint-disable no-console */
if (enabled) {
service.start(PORT).then(() => {
console.info(`Started API service on port ${PORT}`);
});
} else {
console.info('Info Api is disabled.');
}
service.start(PORT).then(() => {
console.info(`Started API service on port ${PORT}`);
});
/* eslint-enable no-console */

View File

@@ -0,0 +1,12 @@
const service = require('restana')();
const analyticsRouter = service.newRouter();
const listingStorage = require('../../services/storage/listingsStorage');
analyticsRouter.get('/:jobId', async (req, res) => {
const { jobId } = req.params;
res.body = listingStorage.getListingProviderDataForAnalytics(jobId) || {};
res.send();
});
exports.analyticsRouter = analyticsRouter;

View File

@@ -0,0 +1,84 @@
const service = require('restana')();
const jobRouter = service.newRouter();
const jobStorage = require('../../services/storage/jobStorage');
const userStorage = require('../../services/storage/userStorage');
const { isAdmin } = require('../security');
function doesJobBelongsToUser(job, req) {
const userId = req.session.currentUser;
if (userId == null) {
return false;
}
const user = userStorage.getUser(userId);
if (user == null) {
return false;
}
return user.isAdmin || job.userId === job.userId;
}
jobRouter.get('/', async (req, res) => {
const isUserAdmin = isAdmin(req);
//show only the jobs which belongs to the user (or all of the user is an admin)
res.body = jobStorage.getJobs().filter((job) => isUserAdmin || job.userId === req.session.currentUser);
res.send();
});
jobRouter.post('/', async (req, res) => {
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled } = req.body;
try {
jobStorage.upsertJob({
userId: req.session.currentUser,
jobId,
enabled,
name,
blacklist,
provider,
notificationAdapter,
});
} catch (error) {
res.send(new Error(error));
console.error(error);
}
res.send();
});
jobRouter.delete('', async (req, res) => {
const { jobId } = req.body;
try {
const job = jobStorage.getJob(jobId);
if (!doesJobBelongsToUser(job, req)) {
res.send(new Error('You are trying to remove a job that is not associated to your user'));
} else {
jobStorage.removeJob(jobId);
}
} catch (error) {
res.send(new Error(error));
console.error(error);
}
res.send();
});
jobRouter.put('/:jobId/status', async (req, res) => {
const { status } = req.body;
const { jobId } = req.params;
try {
const job = jobStorage.getJob(jobId);
if (!doesJobBelongsToUser(job, req)) {
res.send(new Error('You are trying change a job that is not associated to your user'));
} else {
jobStorage.setJobStatus({
jobId,
status,
});
}
} catch (error) {
res.send(new Error(error));
console.error(error);
}
res.send();
});
exports.jobRouter = jobRouter;

View File

@@ -0,0 +1,47 @@
const service = require('restana')();
const loginRouter = service.newRouter();
const userStorage = require('../../services/storage/userStorage');
const hasher = require('../../services/security/hash');
loginRouter.get('/user', async (req, res) => {
const currentUserId = req.session.currentUser;
const isAdmin = currentUserId == null ? false : userStorage.getUser(currentUserId).isAdmin;
if (currentUserId == null) {
res.body = {};
} else {
res.body = {
userId: currentUserId,
isAdmin,
};
}
res.send();
});
loginRouter.post('/', async (req, res) => {
const { username, password } = req.body;
const user = userStorage.getUsers(true).find((user) => user.username === username);
if (user == null) {
res.send(401);
return;
}
if (user.password === hasher.hash(password)) {
req.session.currentUser = user.id;
userStorage.setLastLoginToNow({ userId: user.id });
res.send(200);
return;
} else {
console.error(`User ${username} tried to login, but password was wrong.`);
}
res.send(401);
});
loginRouter.post('/logout', async (req, res) => {
req.session = null;
res.send(200);
});
exports.loginRouter = loginRouter;

View File

@@ -0,0 +1,54 @@
const fs = require('fs');
const service = require('restana')();
const notificationAdapterRouter = service.newRouter();
const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js'));
const notificationAdapter = notificationAdapterList.map((pro) => {
return require(`../../notification/adapter/${pro}`);
});
notificationAdapterRouter.post('/try', async (req, res) => {
const { id, fields } = req.body;
const adapter = notificationAdapter.find((adapter) => adapter.config.id === id);
if (adapter == null) {
res.send(404);
}
const notificationConfig = [];
const notificationObject = {};
Object.keys(fields).forEach((key) => {
notificationObject[key] = fields[key].value;
});
notificationConfig.push({
...notificationObject,
enabled: true,
id,
});
Promise.all(
adapter.send({
serviceName: 'TestCall',
newListings: [
{
price: '42 €',
title: 'This is a test listing',
address: 'some address',
size: '666 2m',
link: 'https://www.orange-coding.net',
},
],
notificationConfig,
jobKey: 'TestJob',
})
)
.then(() => res.send())
.catch((error) => {
res.send(new Error(error));
});
});
notificationAdapterRouter.get('/', async (req, res) => {
res.body = notificationAdapter.map((adapter) => adapter.config);
res.send();
});
exports.notificationAdapterRouter = notificationAdapterRouter;

View File

@@ -0,0 +1,16 @@
const fs = require('fs');
const service = require('restana')();
const providerRouter = service.newRouter();
const providerList = fs.readdirSync('./lib/provider').filter((file) => file.endsWith('.js'));
const provider = providerList.map((pro) => {
return require(`../../provider/${pro}`).metaInformation;
});
providerRouter.get('/', async (req, res) => {
res.body = provider;
res.send();
});
exports.providerRouter = providerRouter;

View File

@@ -0,0 +1,76 @@
const service = require('restana')();
const userRouter = service.newRouter();
const userStorage = require('../../services/storage/userStorage');
const jobStorage = require('../../services/storage/jobStorage');
function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) {
return allUser.filter((user) => user.id !== userIdToBeRemoved && user.isAdmin).length > 0;
}
function checkIfUserToBeRemovedIsLoggedIn(userIdToBeRemoved, req) {
return req.session.currentUser === userIdToBeRemoved;
}
const nullOrEmpty = (str) => str == null || str.length === 0;
userRouter.get('/', async (req, res) => {
res.body = userStorage.getUsers(false);
res.send();
});
userRouter.get('/:userId', async (req, res) => {
const { userId } = req.params;
res.body = userStorage.getUser(userId);
res.send();
});
userRouter.delete('/', async (req, res) => {
const { userId } = req.body;
const allUser = userStorage.getUsers(false);
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
res.send(new Error('You are trying to remove the last admin user. This is prohibited.'));
return;
}
if (checkIfUserToBeRemovedIsLoggedIn(userId, req)) {
res.send(new Error('You are trying to remove yourself. This is prohibited.'));
return;
}
//TODO: Remove also analytics
jobStorage.removeJobsByUserId(userId);
userStorage.removeUser(userId);
res.send();
});
userRouter.post('/', async (req, res) => {
const { username, password, password2, isAdmin, userId } = req.body;
if (password !== password2) {
res.send(new Error('Passwords does not match'));
return;
}
if (nullOrEmpty(username) || nullOrEmpty(password) || nullOrEmpty(password2)) {
res.send(new Error('Username and password are mandatory.'));
return;
}
const allUser = userStorage.getUsers(false);
if (!isAdmin && !checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
res.send(
new Error('You cannot change the admin flag for this user as otherwise, there is no other user in the system')
);
return;
}
userStorage.upsertUser({
userId,
username,
password,
isAdmin,
});
res.send();
});
exports.userRouter = userRouter;

53
lib/api/security.js Normal file
View File

@@ -0,0 +1,53 @@
const userStorage = require('../services/storage/userStorage');
const cookieSession = require('cookie-session');
const { nanoid } = require('nanoid');
const unauthorized = (res) => {
return res.send(401);
};
const isUnauthorized = (req) => {
return req.session.currentUser == null;
};
const isAdmin = (req) => {
if (!isUnauthorized(req)) {
const user = userStorage.getUser(req.session.currentUser);
return user != null && user.isAdmin;
}
return false;
};
const authInterceptor = () => {
return (req, res, next) => {
if (isUnauthorized(req)) {
return unauthorized(res);
} else {
next();
}
};
};
const adminInterceptor = () => {
return (req, res, next) => {
if (!isAdmin(req)) {
return unauthorized(res);
} else {
next();
}
};
};
exports.cookieSession = (userId) => {
return cookieSession({
name: 'fredy-admin-session',
keys: ['fredy', 'super', 'fancy', 'key', nanoid()],
userId,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});
};
exports.adminInterceptor = adminInterceptor;
exports.authInterceptor = authInterceptor;
exports.isUnauthorized = isUnauthorized;
exports.isAdmin = isAdmin;