Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:e2e:debug": "jest --no-cache --config jest-e2e.config.js --verbose --forceExit",
"init-database": "node ./scripts/init-database.js",
"migrate-database": "node ./scripts/migrate-database.js",
"build-packages": "node ./scripts/build-packages.js"
"build-packages": "node ./scripts/build-packages.js",
"run-crons": "node run-crons.js"
},
"dependencies": {
"@azure/identity": "^4.6.0",
Expand Down
103 changes: 103 additions & 0 deletions apps/api-server/run-crons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env node

/**
* Standalone script for running cron jobs via Kubernetes CronJob.
*
* Loads all cron definitions from src/cron/ and executes any job whose
* cronTime schedule has at least one match in the past CRON_WINDOW_MINUTES
* (default: 15). This allows a single K8s CronJob running every X minutes
* to cover all the different schedules defined in the application.
*
* Usage:
* CRON_WINDOW_MINUTES=15 node run-crons.js
*/

require('dotenv').config();

const config = require('config');

// Env variable used by npm's `debug` package.
process.env.DEBUG = config.logging;

require('./config/promises');
require('./config/moment');

const { CronTime } = require('cron');
const moment = require('moment-timezone');
const path = require('path');
const fs = require('fs');

const windowMinutes = parseInt(process.env.CRON_WINDOW_MINUTES, 10) || 15;
const timeZone = config.get('timeZone');
const cronDir = path.join(__dirname, 'src', 'cron');

/**
* Check if a cron schedule had at least one tick in the window [now - windowMs, now].
*
* Uses CronTime._getNextDateFrom(windowStart) — if that next fire time falls
* before "now", the job should have fired within the window.
*/
function shouldRunInWindow(cronTime, windowMs) {
try {
const ct = new CronTime(cronTime, timeZone);
const now = moment();
const windowStart = moment().subtract(windowMs, 'milliseconds');
const nextFromWindow = ct._getNextDateFrom(windowStart);
return nextFromWindow.isSameOrBefore(now);
} catch (e) {
console.warn(`[run-crons] Invalid cron expression: ${cronTime}`, e.message);
return false;
}
}

async function main() {
const windowMs = windowMinutes * 60 * 1000;
console.log(
`[run-crons] Checking cron jobs with a ${windowMinutes}-minute window`
);

const files = fs
.readdirSync(cronDir)
.filter((f) => f.endsWith('.js') && f !== 'index.js');

const jobs = [];

for (const file of files) {
const jobDef = require(path.join(cronDir, file));
const name = file.replace(/\.js$/, '');

if (!jobDef.cronTime || !jobDef.onTick) {
console.log(`[run-crons] Skipping ${name}: missing cronTime or onTick`);
continue;
}

if (shouldRunInWindow(jobDef.cronTime, windowMs)) {
console.log(
`[run-crons] Running: ${name} (schedule: ${jobDef.cronTime})`
);
jobs.push(
Promise.resolve()
.then(() => jobDef.onTick())
.then(() => console.log(`[run-crons] Completed: ${name}`))
.catch((err) => console.error(`[run-crons] Failed: ${name}`, err))
);
} else {
console.log(`[run-crons] Skipping ${name}: not scheduled in window`);
}
}

if (jobs.length === 0) {
console.log('[run-crons] No jobs to run in this window');
} else {
await Promise.allSettled(jobs);
}

console.log('[run-crons] Done');
}

main()
.then(() => process.exit(0))
.catch((err) => {
console.error('[run-crons] Fatal error:', err);
process.exit(1);
});
5 changes: 5 additions & 0 deletions apps/api-server/src/cron-calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ module.exports = {
jobs: new Map(),

start: function () {
if (process.env.USE_KUBERNETES_CRONJOBS === 'true') {
console.log('Cron jobs disabled: USE_KUBERNETES_CRONJOBS is enabled');
return this;
}

var jobs = this.jobs;
util.invokeDir('./cron', function (jobDef, fileName) {
try {
Expand Down
4 changes: 2 additions & 2 deletions apps/auth-server/app.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const initializeApp = require('./app-init');
const config = require('./config');
const memoryStorage = require('./memoryStorage');
const databaseStorage = require('./databaseStorage');

/**
* From time to time we need to clean up any expired tokens
* in the database
*/
setInterval(() => {
memoryStorage.accessTokens
databaseStorage.accessTokens
.removeExpired()
.catch((err) =>
console.error('Error trying to remove expired tokens:', err.stack)
Expand Down
4 changes: 2 additions & 2 deletions apps/auth-server/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const memoryStorage = require('./memoryStorage');
const databaseStorage = require('./databaseStorage');
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const { BasicStrategy } = require('passport-http');
Expand Down Expand Up @@ -237,7 +237,7 @@ passport.use(
*/
passport.use(
new BearerStrategy((accessToken, done) => {
memoryStorage.accessTokens
databaseStorage.accessTokens
.find(accessToken)
.then((token) => validate.token(token, accessToken))
.then((token) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/auth-server/controllers/admin/api/uniqueCode.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const Tasks = require('../../../memoryStorage/tasks');
const Tasks = require('../../../databaseStorage/tasks');

const outputUniqueCode = (req, res, next) => {
res.json(req.code);
Expand Down
12 changes: 6 additions & 6 deletions apps/auth-server/controllers/oauth/oauth2.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const passport = require('passport');
const URL = require('url').URL;
const db = require('../../db');
const config = require('../../config');
const memoryStorage = require('../../memoryStorage');
const databaseStorage = require('../../databaseStorage');
const utils = require('../../utils');
const validate = require('../../validate');

Expand Down Expand Up @@ -83,7 +83,7 @@ server.grant(
sub: user.id,
exp: config.codeToken.expiresIn,
});
memoryStorage.authorizationCodes
databaseStorage.authorizationCodes
.save(code, client.id, redirectURI, user.id, client.scope)
.then(() => done(null, code))
.catch((err) => done(err));
Expand All @@ -106,7 +106,7 @@ server.grant(
});
const expiration = config.token.calculateExpirationDate();

memoryStorage.accessTokens
databaseStorage.accessTokens
.save(token, expiration, user.id, client.id, client.scope)
.then(() => done(null, token, expiresIn))
.catch((err) => done(err));
Expand All @@ -123,7 +123,7 @@ server.grant(
*/
server.exchange(
oauth2orize.exchange.code((client, code, redirectURI, done) => {
memoryStorage.authorizationCodes
databaseStorage.authorizationCodes
.delete(code)
.then((authCode) =>
validate.authCode(code, authCode, client, redirectURI)
Expand Down Expand Up @@ -191,7 +191,7 @@ server.exchange(
const expiration = config.token.calculateExpirationDate();
// Pass in a null for user id since there is no user when using this grant type

memoryStorage.accessTokens
databaseStorage.accessTokens
.save(token, expiration, null, client.id, scope)
.then(() => done(null, token, null, expiresIn))
.catch((err) => done(err));
Expand All @@ -207,7 +207,7 @@ server.exchange(
*/
server.exchange(
oauth2orize.exchange.refreshToken((client, refreshToken, scope, done) => {
memoryStorage.refreshTokens
databaseStorage.refreshTokens
.find(refreshToken)
.then((foundRefreshToken) =>
validate.refreshToken(foundRefreshToken, refreshToken, client)
Expand Down
12 changes: 6 additions & 6 deletions apps/auth-server/controllers/oauth/token.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const memoryStorage = require('../../memoryStorage');
const databaseStorage = require('../../databaseStorage');
const db = require('../../db');
const validate = require('../../validate');

/**
Expand All @@ -27,11 +28,10 @@ const validate = require('../../validate');
exports.info = (req, res) =>
validate
.tokenForHttp(req.query.access_token)
.then(() => memoryStorage.accessTokens.find(req.query.access_token))
.then(() => databaseStorage.accessTokens.find(req.query.access_token))
.then((token) => validate.tokenExistsForHttp(token))
.then((token) =>
memoryStorage.clients
.find(token.clientID)
db.Client.findOne({ where: { id: token.clientID } })
.then((client) => validate.clientExistsForHttp(client))
.then((client) => ({ client, token }))
)
Expand Down Expand Up @@ -70,10 +70,10 @@ exports.info = (req, res) =>
exports.revoke = (req, res) =>
validate
.tokenForHttp(req.query.token)
.then(() => memoryStorage.accessTokens.delete(req.query.token))
.then(() => databaseStorage.accessTokens.delete(req.query.token))
.then((token) => {
if (token == null) {
return memoryStorage.refreshTokens.delete(req.query.token);
return databaseStorage.refreshTokens.delete(req.query.token);
}
return token;
})
Expand Down
110 changes: 110 additions & 0 deletions apps/auth-server/databaseStorage/accesstokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

const db = require('../db');
const utils = require('../utils');

/**
* Returns an access token if it finds one, otherwise returns undefined.
* @param {String} token - The token to decode to get the id of the access token to find.
* @returns {Promise} resolved with the token if found, otherwise resolved with undefined
*/
exports.find = (token) => {
let decoded;
try {
decoded = utils.verifyToken(token);
} catch (e) {
return Promise.resolve(undefined);
}

const id = decoded.jti;

return db.AccessToken.findOne({ where: { tokenId: id } })
.then((token) => token || undefined)
.catch((e) => {
console.warn('Error finding accesstoken: ', e);
return undefined;
});
};

/**
* Saves an access token, expiration date, user id, client id, and scope.
* @param {Object} token - The access token (required)
* @param {Date} expirationDate - The expiration of the access token (required)
* @param {String} userID - The user ID (required)
* @param {String} clientID - The client ID (required)
* @param {String} scope - The scope (optional)
* @returns {Promise} resolved with the saved token
*/
exports.save = (token, expirationDate, userID, clientID, scope) => {
let decoded;
try {
decoded = utils.verifyToken(token);
} catch (e) {
return Promise.resolve(undefined);
}

const id = decoded.jti;

return db.AccessToken.create({
tokenId: id,
userID,
expirationDate,
clientID,
scope,
})
.then((token) => token || undefined)
.catch((e) => {
console.warn('Error creating accesstoken: ', e);
return undefined;
});
};

/**
* Deletes/Revokes an access token by getting the ID and removing it from the storage.
* @param {String} token - The token to decode to get the id of the access token to delete.
* @returns {Promise} resolved with the deleted token
*/
exports.delete = (token) => {
let decoded;
try {
decoded = utils.verifyToken(token);
} catch (e) {
return Promise.resolve(undefined);
}

const id = decoded.jti;

return db.AccessToken.findOne({ where: { tokenId: id } })
.then((token) => {
if (!token) return undefined;
return token.destroy().then(() => token);
})
.catch((e) => {
console.warn('Error deleting accesstoken: ', e);
return undefined;
});
};

/**
* Removes expired access tokens.
* @returns {Promise} resolved when expired tokens are removed
*/
exports.removeExpired = () => {
return db.AccessToken.destroy({
where: {
expirationDate: {
[db.Sequelize.Op.lt]: new Date(),
},
},
}).catch((e) => {
console.warn('Error removing expired access tokens: ', e);
});
};

/**
* Removes all access tokens.
* @returns {Promise} resolved when all tokens are removed
*/
exports.removeAll = () => {
return db.AccessToken.destroy({ where: {} });
};
Loading
Loading