Skip to content

Commit

Permalink
Merge pull request #483 from kipr/navzam/eula-support
Browse files Browse the repository at this point in the history
EULA support
  • Loading branch information
tcorbly authored Oct 29, 2024
2 parents 03f3dff + ae36572 commit 6517692
Show file tree
Hide file tree
Showing 30 changed files with 3,628 additions and 231 deletions.
21 changes: 21 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ try {
console.error(e);
}

const serviceAccountKeyString = process.env.FIREBASE_SERVICE_ACCOUNT_KEY_STRING;
const serviceAccountKeyFile = process.env.FIREBASE_SERVICE_ACCOUNT_KEY_FILE;

let serviceAccountKey;
if (serviceAccountKeyString) {
serviceAccountKey = JSON.parse(serviceAccountKeyString);
} else if (serviceAccountKeyFile) {
serviceAccountKey = JSON.parse(fs.readFileSync(serviceAccountKeyFile, 'utf8'));
} else {
throw new Error('FIREBASE_SERVICE_ACCOUNT_KEY_STRING or FIREBASE_SERVICE_ACCOUNT_KEY_FILE must be set');
}

module.exports = {
get: () => {
return {
Expand All @@ -23,6 +35,15 @@ module.exports = {
staticMaxAge: getEnvVarOrDefault('CACHING_STATIC_MAX_AGE', 60 * 60 * 1000),
},
dbUrl: getEnvVarOrDefault('API_URL', 'https://db-prerelease.botballacademy.org'),
firebase: {
// Firebase API keys are not secret, so the real value is okay to keep in code
apiKey: getEnvVarOrDefault('FIREBASE_API_KEY', 'AIzaSyBiVC6umtYRy-aQqDUBv8Nn1txWLssix04'),
serviceAccountKey,
},
mailgun: {
apiKey: getEnvVarOrDefault('MAILGUN_API_KEY', ''),
domain: getEnvVarOrDefault('MAILGUN_DOMAIN', ''),
},
};
},
};
Expand Down
4 changes: 3 additions & 1 deletion configs/webpack/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = {
app: './index.tsx',
login: './components/Login/index.tsx',
plugin: './lms/plugin/index.tsx',
parentalConsent: './components/ParentalConsent/index.tsx',
'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker.js',
},
Expand Down Expand Up @@ -137,9 +138,10 @@ module.exports = {
],
},
plugins: [
new HtmlWebpackPlugin({ template: 'index.html.ejs', excludeChunks: ['login', 'plugin'] }),
new HtmlWebpackPlugin({ template: 'index.html.ejs', excludeChunks: ['login', 'plugin', 'parentalConsent'] }),
new HtmlWebpackPlugin({ template: 'components/Login/login.html.ejs', filename: 'login.html', chunks: ['login'] }),
new HtmlWebpackPlugin({ template: 'lms/plugin/plugin.html.ejs', filename: 'plugin.html', chunks: ['plugin'] }),
new HtmlWebpackPlugin({ template: 'components/ParentalConsent/parental-consent.html.ejs', filename: 'parental-consent.html', chunks: ['parentalConsent'] }),
new DefinePlugin({
SIMULATOR_VERSION: JSON.stringify(require('../../package.json').version),
SIMULATOR_GIT_HASH: JSON.stringify(commitHash),
Expand Down
20 changes: 18 additions & 2 deletions express.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ const { get: getConfig } = require('./config');
const { WebhookClient } = require('discord.js');
const proxy = require('express-http-proxy');
const path = require('path');

const { FirebaseTokenManager } = require('./firebaseAuth');
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const createParentalConsentRouter = require('./parentalConsent');

let config;
try {
Expand All @@ -32,6 +35,14 @@ var limiter = RateLimit({
// apply rate limiter to all requests
app.use(limiter);

const mailgun = new Mailgun(formData);
const mailgunClient = mailgun.client({
username: 'api',
key: config.mailgun.apiKey,
});

const firebaseTokenManager = new FirebaseTokenManager(config.firebase.serviceAccountKey, config.firebase.apiKey);

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
Expand All @@ -41,6 +52,8 @@ app.use((req, res, next) => {
app.use(bodyParser.json());
app.use(morgan('combined'));

app.use('/api/parental-consent', createParentalConsentRouter(firebaseTokenManager, mailgunClient, config));

app.use('/api', proxy(config.dbUrl));

// If we have libkipr (C) artifacts and emsdk, we can compile.
Expand Down Expand Up @@ -260,11 +273,14 @@ app.get('/login', (req, res) => {
res.sendFile(`${__dirname}/${sourceDir}/login.html`);
});


app.get('/lms/plugin', (req, res) => {
res.sendFile(`${__dirname}/${sourceDir}/plugin.html`);
});

app.get('/parental-consent/*', (req, res) => {
res.sendFile(`${__dirname}/${sourceDir}/parental-consent.html`);
});

app.use('*', (req, res) => {
setCrossOriginIsolationHeaders(res);
res.sendFile(`${__dirname}/${sourceDir}/index.html`);
Expand Down
114 changes: 114 additions & 0 deletions firebaseAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-env node */

const { cert, initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const axios = require('axios').default;

class FirebaseTokenManager {
constructor(serviceAccountKey, firebaseApiKey) {
this.firebaseApiKey = firebaseApiKey;

const firebaseApp = initializeApp({
credential: cert(serviceAccountKey),
});

this.firebaseAuth = getAuth(firebaseApp);

this.idToken = null;
this.idTokenExp = null;

// Immediately schedule a refresh to get the initial token
this.refreshTimerId = setTimeout(this.refreshToken.bind(this), 0);
}

getToken() {
// May return an expired token if token refresh is failing
console.log('using firebase token', this.idToken);
return this.idToken;
}

refreshToken() {
console.log('REFRESHING FIREBASE TOKEN');
return this.getCustomToken()
.then(customToken => {
console.log('GOT CUSTOM TOKEN:', customToken);
return this.getIdTokenFromCustomToken(customToken);
})
.then(idToken => {
console.log('GOT ID TOKEN:', idToken);

if (!idToken) {
throw new Error('Failed to get ID token');
}

const base64Url = idToken.split('.')[1];
console.log('base64Url', base64Url);

const buff = Buffer.from(base64Url, 'base64url');
const raw = buff.toString('ascii');
console.log('raw', raw);

const parsed = JSON.parse(raw);
console.log('parsed', parsed);

const exp = parsed['exp'];
console.log('exp', exp);

this.idTokenExp = exp;
this.idToken = idToken;

// Schedule refresh 5 mins before expiration
const msUntilExpiration = (exp * 1000) - Date.now();
const refreshAt = msUntilExpiration - (5 * 60 * 1000);
if (refreshAt > 0) {
console.log('scheduling refresh in', refreshAt, 'ms');
this.refreshTimerId = setTimeout(this.refreshToken.bind(this), refreshAt);
} else {
console.log('GOT NEGATIVE REFRESH AT TIME');
}
})
.catch(e => {
// Try again in 1 minute
console.error('Token refresh failed, retrying in 1 min', e);
this.refreshTimerId = setTimeout(this.refreshToken.bind(this), 60 * 1000);
});
}

// Create a custom token with specific claims
// Used to exchange for an ID token from firebase
getCustomToken() {
if (!this.firebaseAuth) {
throw new Error('Firebase auth not initialized');
}

return this.firebaseAuth.createCustomToken('simulator', { 'sim_backend': true });
}

// Get an ID token from firebase using a previously created custom token
getIdTokenFromCustomToken(customToken) {
if (!this.firebaseAuth) {
throw new Error('Firebase auth not initialized');
}

// Send request to auth emulator if using
const urlPrefix = process.env.FIREBASE_AUTH_EMULATOR_HOST ? `http://${process.env.FIREBASE_AUTH_EMULATOR_HOST}/` : 'https://';
const url = `${urlPrefix}identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${this.firebaseApiKey}`;

return axios.post(url, {
token: customToken,
returnSecureToken: true,
})
.then(response => {
const responseBody = response.data;
return responseBody.idToken;
})
.catch(error => {
console.error('FAILED TO GET ID TOKEN', error?.response?.data?.error);
return null;
});
}
}

module.exports = {
FirebaseTokenManager,
};
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"typescript": "^4.9.0",
"webpack": "^5.36.2",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.10.3",
Expand All @@ -64,6 +64,7 @@
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.6",
"babylonjs-gltf2interface": "6.18.0",
"body-parser": "^1.19.0",
"colorjs.io": "^0.4.2",
Expand All @@ -75,6 +76,7 @@
"express-http-proxy": "^1.6.3",
"express-rate-limit": "^7.4.0",
"firebase": "^9.0.1",
"firebase-admin": "^12.0.0",
"form-data": "^4.0.0",
"history": "^4.7.2",
"image-loader": "^0.0.1",
Expand All @@ -83,7 +85,10 @@
"itch": "https://github.com/chrismbirmingham/itch#36",
"ivygate": "https://github.com/kipr/ivygate#v0.1.8",
"kipr-scratch": "file:dependencies/kipr-scratch/kipr-scratch",
"mailgun.js": "^10.2.1",
"morgan": "^1.10.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.4.168",
"prop-types": "^15.8.1",
"qs": "^6.11.0",
"react": "^17.0.1",
Expand Down
Loading

0 comments on commit 6517692

Please sign in to comment.