Demo Mode (#117)

* Adding Demo Mode to Fredy
This commit is contained in:
Christian Kellner
2024-11-22 09:11:10 +01:00
committed by GitHub
parent b3ae5f640c
commit 337ee922a6
20 changed files with 406 additions and 193 deletions

View File

@@ -12,6 +12,7 @@ import restana from 'restana';
import files from 'serve-static';
import path from 'path';
import { getDirName } from '../utils.js';
import {demoRouter} from './routes/demoRouter.js';
const service = restana();
const staticService = files(path.join(getDirName(), '../ui/public'));
const PORT = config.port || 9998;
@@ -30,6 +31,9 @@ service.use('/api/jobs/insights', analyticsRouter);
service.use('/api/admin/users', userRouter);
service.use('/api/jobs', jobRouter);
service.use('/api/login', loginRouter);
//this route is unsecured intentionally as it is being queried from the login page
service.use('/api/demo', demoRouter);
/* eslint-disable no-console */
service.start(PORT).then(() => {
console.info(`Started API service on port ${PORT}`);

View File

@@ -0,0 +1,11 @@
import restana from 'restana';
import {config} from '../../utils.js';
const service = restana();
const demoRouter = service.newRouter();
demoRouter.get('/', async (req, res) => {
res.body = Object.assign({}, {demoMode: config.demoMode});
res.send();
});
export { demoRouter };

View File

@@ -1,6 +1,7 @@
import restana from 'restana';
import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js';
import fs from 'fs';
import {handleDemoUser} from '../../services/storage/userStorage.js';
const service = restana();
const generalSettingsRouter = service.newRouter();
generalSettingsRouter.get('/', async (req, res) => {
@@ -10,9 +11,14 @@ generalSettingsRouter.get('/', async (req, res) => {
generalSettingsRouter.post('/', async (req, res) => {
const settings = req.body;
try {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to change these settings.'));
return;
}
const currentConfig = await readConfigFromStorage();
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings}));
await refreshConfig();
handleDemoUser();
} catch (err) {
console.error(err);
res.send(new Error('Error while trying to write settings.'));

View File

@@ -6,6 +6,7 @@ import * as immoscoutProvider from '../../provider/immoscout.js';
import { config } from '../../utils.js';
import { isAdmin } from '../security.js';
import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js';
import {trackDemoJobCreated} from '../../services/tracking/Tracker.js';
const service = restana();
const jobRouter = service.newRouter();
function doesJobBelongsToUser(job, req) {
@@ -68,6 +69,11 @@ jobRouter.post('/', async (req, res) => {
res.send(new Error(error));
console.error(error);
}
trackDemoJobCreated({
name,
provider,
adapter: notificationAdapter
});
res.send();
});
jobRouter.delete('', async (req, res) => {

View File

@@ -1,6 +1,8 @@
import restana from 'restana';
import * as userStorage from '../../services/storage/userStorage.js';
import * as hasher from '../../services/security/hash.js';
import {config} from '../../utils.js';
import {trackDemoAccessed} from '../../services/tracking/Tracker.js';
const service = restana();
const loginRouter = service.newRouter();
loginRouter.get('/user', async (req, res) => {
@@ -24,6 +26,11 @@ loginRouter.post('/', async (req, res) => {
return;
}
if (user.password === hasher.hash(password)) {
if(config.demoMode){
trackDemoAccessed();
}
req.session.currentUser = user.id;
userStorage.setLastLoginToNow({ userId: user.id });
res.send(200);

View File

@@ -1,6 +1,7 @@
import restana from 'restana';
import * as userStorage from '../../services/storage/userStorage.js';
import * as jobStorage from '../../services/storage/jobStorage.js';
import {config} from '../../utils.js';
const service = restana();
const userRouter = service.newRouter();
function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) {
@@ -20,6 +21,11 @@ userRouter.get('/:userId', async (req, res) => {
res.send();
});
userRouter.delete('/', async (req, res) => {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to remove user.'));
return;
}
const { userId } = req.body;
const allUser = userStorage.getUsers(false);
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
@@ -36,6 +42,12 @@ userRouter.delete('/', async (req, res) => {
res.send();
});
userRouter.post('/', async (req, res) => {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to change or add user.'));
return;
}
const { username, password, password2, isAdmin, userId } = req.body;
if (password !== password2) {
res.send(new Error('Passwords does not match'));

View File

@@ -0,0 +1,29 @@
import { setInterval } from 'node:timers';
import {removeJobsByUserName} from './storage/jobStorage.js';
import {config} from '../utils.js';
/**
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
*/
export function cleanupDemoAtMidnight() {
const now = new Date();
const millisUntilMidnightUTC = (24 - now.getUTCHours()) * 60 * 60 * 1000
- now.getUTCMinutes() * 60 * 1000
- now.getUTCSeconds() * 1000
- now.getUTCMilliseconds();
setTimeout(() => {
cleanup();
setInterval(() => {
cleanup();
}, 24 * 60 * 60 * 1000);
}, millisUntilMidnightUTC);
}
function cleanup(){
if(config.demoMode){
removeJobsByUserName('demo');
}
}

View File

@@ -77,6 +77,17 @@ export const removeJobsByUserId = (userId) => {
.value();
db.write();
};
export const removeJobsByUserName = (userName) => {
db.chain
.get('jobs')
.filter((job) => job.username === userName)
.forEach((job) => listingStorage.removeListings(job.id));
db.chain
.get('jobs')
.remove((job) => job.username === userName)
.value();
db.write();
};
export const getJobs = () => {
return db.chain
.get('jobs')

View File

@@ -1,5 +1,5 @@
import { JSONFileSync } from 'lowdb/node';
import { getDirName } from '../../utils.js';
import {config, getDirName} from '../../utils.js';
import * as hasher from '../security/hash.js';
import { nanoid } from 'nanoid';
import * as jobStorage from './jobStorage.js';
@@ -16,6 +16,13 @@ const defaultData = {
password: hasher.hash('admin'),
isAdmin: true,
},
{
id: nanoid(),
lastLogin: Date.now(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
},
],
};
@@ -84,3 +91,29 @@ export const removeUser = (userId) => {
.value();
db.write();
};
export const handleDemoUser = () => {
if(!config.demoMode){
const user = db.chain.get('user').value();
db.chain.get('user').value();
db.chain.set('user', user.filter((u) => u.username !== 'demo')).value();
db.write();
}else {
const demoUser = db.chain
.get('user')
.filter((u) => u.username === 'demo')
.value();
if (demoUser == null || demoUser.length === 0) {
db.chain.get('user')
.value()
.push({
id: nanoid(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
});
db.write();
}
}
};

View File

@@ -1,20 +1,16 @@
import Mixpanel from 'mixpanel';
import {getJobs} from '../storage/jobStorage.js';
import {config} from '../../utils.js';
import {config, inDevMode} from '../../utils.js';
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
export const track = function () {
//only send tracking information if the user allowed to do so.
if (config.analyticsEnabled) {
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
if (config.analyticsEnabled && !inDevMode()) {
const activeProvider = new Set();
const activeAdapter = new Set();
const platform = process.platform;
const arch = process.arch;
const language = process.env.LANG || 'en';
const nodeVersion = process.version || 'N/A';
const jobs = getJobs();
@@ -28,15 +24,45 @@ export const track = function () {
});
});
mixpanelTracker.track('fredy_tracking', {
mixpanelTracker.track('fredy_tracking', enrichTrackingObject({
adapter: Array.from(activeAdapter),
provider: Array.from(activeProvider),
isDemo: config.demoMode,
platform,
arch,
nodeVersion,
language
});
}));
}
}
};
};
/**
* Note, this will only be used when Fredy runs in demo mode
*/
export function trackDemoJobCreated(jobData) {
if (config.analyticsEnabled && !inDevMode() && config.demoMode) {
mixpanelTracker.track('demoJobCreated', enrichTrackingObject(jobData));
}
}
/**
* Note, this will only be used when Fredy runs in demo mode
*/
export function trackDemoAccessed() {
if (config.analyticsEnabled && !inDevMode() && config.demoMode) {
mixpanelTracker.track('demoAccessed', enrichTrackingObject({}));
}
}
function enrichTrackingObject(trackingObject) {
const platform = process.platform;
const arch = process.arch;
const language = process.env.LANG || 'en';
const nodeVersion = process.version || 'N/A';
return {
...trackingObject,
isDemo: config.demoMode,
platform,
arch,
nodeVersion,
language
};
}

View File

@@ -4,6 +4,10 @@ import {readFile} from 'fs/promises';
import {createHash} from 'crypto';
import {DEFAULT_CONFIG} from './defaultConfig.js';
function inDevMode(){
return process.env.NODE_ENV == null || process.env.NODE_ENV !== 'production';
}
function isOneOf(word, arr) {
if (arr == null || arr.length === 0) {
return false;
@@ -72,6 +76,7 @@ export async function refreshConfig(){
await refreshConfig();
export {isOneOf};
export {inDevMode};
export {nullOrEmpty};
export {duringWorkingHoursOrNotSet};
export {getDirName};