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
19 changes: 8 additions & 11 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2.1
jobs:
build:
docker:
- image: circleci/node:10
- image: cimg/node:24.20
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
Expand Down Expand Up @@ -33,26 +33,23 @@ jobs:
- checkout
- restore_cache:
keys:
- yarn-cache-{{ checksum "yarn.lock" }}
- yarn-cache-api-{{ checksum "api/yarn.lock" }}
- run:
name: Installing Dependencies
command: npm_config_build_from_source=true yarn --ignore-engines
name: Installing API Dependencies
command: yarn --cwd api --ignore-engines --ignore-scripts
- run:
name: Create empty .env
command: touch .env
- run:
name: Testing Compile
command: yarn run build-all
command: yarn --cwd api build
- save_cache:
key: yarn-cache-{{ checksum "yarn.lock" }}
key: yarn-cache-api-{{ checksum "api/yarn.lock" }}
paths:
- ~/.cache/yarn
- run:
name: Run linter
command: yarn run lint-ci
- run:
name: Run tests
command: yarn run test
name: Run API tests
command: yarn --cwd api test
- deploy:
name: Semantic Release
command: yarn run semantic-release
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10
24
5 changes: 4 additions & 1 deletion api/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
build/
build/
dist/
node_modules/
yarn-error.log
1 change: 1 addition & 0 deletions api/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
4 changes: 4 additions & 0 deletions api/.yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pkgcloud pulls in the legacy grpc native addon, which does not compile on Node 24.
# Skip install scripts so install succeeds; Rackspace/Amazon storage still work via JS.
ignore-engines true
ignore-scripts true
29 changes: 29 additions & 0 deletions api/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: { node: '24' },
modules: 'auto'
}
],
'@babel/preset-react',
'@babel/preset-flow'
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-export-default-from',
[
'module-resolver',
{
alias: {
api: './src',
lib: '../lib',
ui: '../ui/src',
worker: '../worker/src',
cli: '../cli/src'
}
}
]
]
};
72 changes: 38 additions & 34 deletions api/build.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import 'dotenv/config';
import path from 'path';
import run from '../lib/tools/run';
import bundle from '../lib/tools/bundle';
import newrelicConfig from '../lib/tools/newrelicConfig';
import getWebpackConfig from '../lib/tools/getWebpackConfig';
const webpack = require('webpack');
const getWebpackConfig = require('./webpack.config');

const isDebug = !process.argv.includes('--release');
const isVerbose = !!process.argv.includes('--verbose');
const watch = !!process.argv.includes('--watch');
const stats = !!process.argv.includes('--stats');
const isRelease = process.argv.includes('--release');
const watch = process.argv.includes('--watch');
const webpackConfig = getWebpackConfig({}, {
mode: isRelease ? 'production' : 'development'
});

const sourceDir = path.resolve(__dirname, 'src');
const outputDir = path.resolve(__dirname, 'dist', 'server');
const compiler = webpack(webpackConfig);

const bannerPrefix = newrelicConfig({ appType: 'API' });
const logThenComplete = (err, stats) => {
if (err) {
console.error(err);
process.exitCode = 1;
return;
}
if (stats.hasErrors()) {
console.error(stats.toString({ colors: true }));
process.exitCode = 1;
return;
}
console.log(stats.toString({ colors: true }));
};

const webpackConfig = getWebpackConfig({
isDebug,
isVerbose,
isClient: false,
sourceDir,
outputDir,
stats,
publicPath: '/',
entry: {
server: 'server.js',
},
bannerPrefix
});
const watchOptions = {
aggregateTimeout: 300,
poll: 500,
ignored: /node_modules/
};

/**
* Uses webpack to compile the API server
* into a single file executable by node
*/
async function build() {
await run(bundle, { webpackConfig, watch });
if (watch) {
compiler.watch(watchOptions, logThenComplete);
} else {
compiler.run((err, stats) => {
logThenComplete(err, stats);
compiler.close((closeErr) => {
if (closeErr) {
console.error(closeErr);
process.exitCode = 1;
}
});
});
}

export default build();
97 changes: 97 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"name": "@learninglocker/api",
"description": "Learning Locker API server",
"license": "GPL-3.0",
"version": "2.0.0",
"private": true,
"engines": {
"node": ">=24.0.0"
},
"scripts": {
"dev": "node build.js",
"dev:watch": "node build.js --watch",
"build": "node build.js --release",
"start": "node dist/server",
"test": "mocha --exit --timeout 10000 --require ./test-register.js \"src/**/*-test.js\"",
"test-lib": "mocha --exit --timeout 10000 --require ./test-register.js \"../lib/**/*-test.js\""
},
"dependencies": {
"@azure/service-bus": "^1.1.10",
"@azure/storage-blob": "^10.5.0",
"@babel/runtime": "^7.29.7",
"@google-cloud/pubsub": "^6.0.1",
"@google-cloud/storage": "^8.0.1",
"@learninglocker/persona-service": "^1.7.1",
"@learninglocker/xapi-validation": "^2.1.10",
"async": "^3.2.6",
"aws-sdk": "^2.1693.0",
"azure-sb": "^0.11.2",
"bcryptjs": "^3.0.3",
"bluebird": "^3.7.2",
"body-parser": "^1.20.6",
"boolean": "0.2.0",
"bull": "^3.29.3",
"clamscan": "^2.4.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"dotenv": "^16.6.1",
"express": "^4.22.2",
"express-restify-mongoose": "^6.1.2",
"fast-csv": "^5.0.7",
"git-rev": "^0.2.1",
"highland": "^2.13.5",
"html-to-text": "^10.0.1",
"http-status": "^1.8.1",
"immutable": "^3.8.2",
"ioredis": "^5.11.1",
"jscommons": "^2.3.0",
"jsonwebtoken": "^9.0.3",
"lodash": "^4.18.1",
"moment": "^2.30.1",
"moment-timezone": "^0.6.3",
"mongodb": "^3.7.4",
"mongoose": "^5.13.23",
"mongoose-findorcreate": "^3.0.0",
"mongoose-timestamp": "^0.6.0",
"ms": "^2.1.3",
"multer": "^1.4.5-lts.2",
"multiparty": "^4.3.0",
"newrelic": "^12.25.1",
"nodemailer": "^9.1.1",
"passport": "^0.7.0",
"passport-custom": "^1.2.1",
"passport-google-oauth": "^2.0.0",
"passport-http": "^0.3.0",
"passport-http-bearer": "^1.0.1",
"pkgcloud": "^1.7.0",
"rulr": "^4.0.2",
"sha1": "^1.1.1",
"sift": "^5.1.0",
"source-map-support": "^0.5.21",
"sqs-consumer": "^5.8.0",
"uuid": "^11.1.1",
"validator": "^13.15.35",
"winston": "^2.4.7",
"winston-aws-cloudwatch": "^1.6.0"
},
"devDependencies": {
"@babel/core": "^7.29.7",
"@babel/plugin-proposal-export-default-from": "^7.29.7",
"@babel/plugin-transform-runtime": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-flow": "7.29.7",
"@babel/preset-react": "7.29.7",
"@babel/register": "^7.29.7",
"@faker-js/faker": "^10.6.0",
"babel-loader": "^10.1.1",
"babel-plugin-module-resolver": "^5.0.3",
"chai": "^4.5.0",
"chai-as-promised": "^7.1.2",
"chai-fs": "^2.0.0",
"mocha": "^10.8.2",
"supertest": "^7.2.2",
"webpack": "^5.110.3",
"webpack-cli": "^7.2.3",
"webpack-node-externals": "^3.0.0"
}
}
4 changes: 3 additions & 1 deletion api/src/controllers/RequestAppAccessController.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import getAuthFromRequest from 'lib/helpers/getAuthFromRequest';
import logger from 'lib/logger';
import catchErrors from './utils/catchErrors';

const htmlToPlain = htmlToText.convert || htmlToText.fromString.bind(htmlToText);

export const requestAppAccess = catchErrors(async (request, response) => {
const authInfo = getAuthFromRequest(request);

Expand All @@ -28,7 +30,7 @@ export const requestAppAccess = catchErrors(async (request, response) => {
});

const htmlEmail = compiledWrapper({ title: subject, body: content, siteUrl: process.env.SITE_URL });
const text = htmlToText.fromString(content);
const text = htmlToPlain(content);

const mailOptions = _.defaults({
to: recipient,
Expand Down
6 changes: 3 additions & 3 deletions api/src/routes/userOrganisationSettings/router-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'assert';
import mongoose from 'mongoose';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import { ALL, SITE_ADMIN } from 'lib/constants/scopes';
import * as orgScopes from 'lib/constants/orgScopes';
import Organisation from 'lib/models/organisation';
Expand Down Expand Up @@ -54,7 +54,7 @@ describe('userOrganisationSettings.router', () => {
const createLoginUser = async (scopes, organisationSettings = []) => {
const loginUser = await User.create({
name: 'login user',
email: `login-user-${uuid.v4()}@example.com`,
email: `login-user-${uuid()}@example.com`,
scopes,
organisations: [org1Id, org2Id],
organisationSettings,
Expand All @@ -69,7 +69,7 @@ describe('userOrganisationSettings.router', () => {
const createTargetUser = async (organisationSettings = []) => {
const anotherUser = await User.create({
name: 'target user',
email: `target-user-${uuid.v4()}@example.com`,
email: `target-user-${uuid()}@example.com`,
scopes: [],
organisations: [org1Id, org2Id],
organisationSettings,
Expand Down
6 changes: 3 additions & 3 deletions api/src/routes/userOrganisations/router-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'assert';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import { ALL, SITE_ADMIN } from 'lib/constants/scopes';
import * as orgScopes from 'lib/constants/orgScopes';
import Organisation from 'lib/models/organisation';
Expand Down Expand Up @@ -35,7 +35,7 @@ describe('userOrganisations.router', () => {
const createLoginUser = async (scopes, organisationSettings = []) => {
const loginUser = await User.create({
name: 'login user',
email: `login-user-${uuid.v4()}@example.com`,
email: `login-user-${uuid()}@example.com`,
scopes,
organisations: [org1Id, org2Id],
organisationSettings,
Expand All @@ -50,7 +50,7 @@ describe('userOrganisations.router', () => {
const createTargetUser = async (organisations = []) => {
const anotherUser = await User.create({
name: 'target user',
email: `target-user-${uuid.v4()}@example.com`,
email: `target-user-${uuid()}@example.com`,
scopes: [],
organisations,
organisationSettings: [],
Expand Down
22 changes: 22 additions & 0 deletions api/test-register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const path = require('path');
const dotenv = require('dotenv');

dotenv.config({ path: path.resolve(__dirname, '../.env') });
dotenv.config({ path: path.resolve(__dirname, '.env') });

process.env.MONGODB_PATH = process.env.MONGODB_TEST_PATH;
process.env.API_PORT = process.env.TEST_API_PORT;
process.env.SMTP_PASS = process.env.TEST_SMTP_PASS;
process.env.LOG_MIN_LEVEL = process.env.TEST_LOG_MIN_LEVEL;
process.env.TESTING = true;

require('@babel/register')({
extensions: ['.js'],
configFile: path.resolve(__dirname, 'babel.config.js'),
ignore: [/node_modules/],
only: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, '../lib'),
path.resolve(__dirname, '../ui/src')
]
});
32 changes: 2 additions & 30 deletions api/testConfig.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
require('dotenv/config');
var path = require('path');
var getWebpackConfig = require('../lib/tools/getWebpackConfig');
require('./test-register');

var isDebug = !process.argv.includes('--release');
var isVerbose = !!process.argv.includes('--verbose');
var stats = !!process.argv.includes('--stats');

var sourceDir = path.resolve(__dirname, 'src');
var outputDir = path.resolve(__dirname, 'dist', 'server');

process.env.MONGODB_PATH = process.env.MONGODB_TEST_PATH;
process.env.API_PORT = process.env.TEST_API_PORT;
process.env.SMTP_PASS = process.env.TEST_SMTP_PASS;
process.env.LOG_MIN_LEVEL = process.env.TEST_LOG_MIN_LEVEL;
process.env.TESTING = true;

var webpackConfig = getWebpackConfig({
isDebug: isDebug,
isVerbose: isVerbose,
isClient: false,
sourceDir: sourceDir,
outputDir: outputDir,
stats: stats,
publicPath: '/',
entry: {
server: 'server.js',
},
});

module.exports = webpackConfig;
module.exports = require('./webpack.config')({}, { mode: 'development' });
Loading