Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EULA support #483

Merged
merged 48 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
998bb38
Initial work for EULA support
navzam Jan 25, 2024
b02bbe4
Partial progress commit
navzam May 14, 2024
825d62b
Add confirmation to parental consent page + cleanup
navzam Jun 11, 2024
88159e3
Persist values when going back in parental consent forms
navzam Jun 11, 2024
98ecbcb
Send confirmation email after parental consent
navzam Jun 12, 2024
6a35fe1
Allow logout when waiting for consent
navzam Jun 12, 2024
65793b1
Move parental consent API to sub-router
navzam Jun 20, 2024
0e93777
Improve naming in parental consent subroute
navzam Jun 21, 2024
c2be194
Move firebase API key into config
navzam Jun 21, 2024
e47a132
On parent consent page, auto-preview the PDF
navzam Jun 21, 2024
6694d30
Hide toolbars in PDF preview (at least on Chrome)
navzam Jun 21, 2024
c726d98
Incorporate real terms and privacy docs
navzam Jul 3, 2024
1413b31
Use unique token to protect parent consent API
navzam Jul 3, 2024
2ea66a3
In parent consent page, ensure link is valid
navzam Jul 13, 2024
f565af8
In parent consent page, change button icon/text while submitting
navzam Jul 13, 2024
28366df
Fix bug in date validation
navzam Jul 14, 2024
b1ae8fa
Allow download of parent consent PDF after submission
navzam Jul 14, 2024
8410597
Fix bug where DOB persisted between users
navzam Jul 14, 2024
f728dcd
Send additional headers for big store API
navzam Jul 14, 2024
aa0121a
Fix parent consent page getting cut off on mobile
navzam Jul 14, 2024
94b2783
In parent consent page, show PDF on right
navzam Jul 14, 2024
85d3d14
In login page, capitalize headers
navzam Jul 17, 2024
6f7c6a7
Generate parent form on server and use pdf.js to render on client (pa…
navzam Jul 21, 2024
2f31d30
Properly scale PDFs depending on container size
navzam Jul 26, 2024
da6e14c
Use pdf.js to render user consent PDFs
navzam Aug 11, 2024
3c06acc
Use pdf.js viewer instead of raw canvas drawing
navzam Aug 12, 2024
1895006
In parent consent form, pre-fill email, dob, and today's date
navzam Aug 13, 2024
cc29bd5
Don't allow parent email to match user email
navzam Aug 13, 2024
fc93362
Warn user that parent has limited time to consent
navzam Aug 13, 2024
da32c39
Update template name and subject for consent emails
navzam Aug 14, 2024
c1c23fc
Change "parent" to "parent/guardian"
navzam Aug 14, 2024
009c93a
Centralize parent token auth in APIs
navzam Aug 31, 2024
e133ecf
Add user token auth to API for starting parent consent
navzam Aug 31, 2024
04cf529
Add server-side validation for parental consent form values
navzam Sep 3, 2024
3532d74
Add global rate limiting for parental consent APIs
navzam Sep 10, 2024
ff9e9d4
Improve expiration and re-consent logic
navzam Oct 23, 2024
d7cb384
Merge branch 'master' into navzam/eula-support
navzam Oct 24, 2024
1ae519e
Fix minor issues and linter errors
navzam Oct 24, 2024
6c199d3
Upgrade typescript to ^4.9.0
navzam Oct 24, 2024
a463be3
Don't use class fields in FirebaseTokenManager
navzam Oct 25, 2024
143b8fb
Check if acquiring ID token fails
navzam Oct 25, 2024
c79283e
Improve parent UX if consent request expires
navzam Oct 25, 2024
5b9e705
Use PATCH instead of POST for consent updates
navzam Oct 28, 2024
db399d6
Validate user ID to prevent malicious input
navzam Oct 29, 2024
3641c25
Remove sample PDF from repo
navzam Oct 29, 2024
f519128
Fix typo in firebaseAuth.js
tcorbly Oct 29, 2024
03c7473
Fix typo in firebaseAuth.js
tcorbly Oct 29, 2024
ae36572
Linting parentalConsent.js
tcorbly Oct 29, 2024
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
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`);
});
Fixed Show fixed Hide fixed

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
Loading