Skip to content

Commit 51ee456

Browse files
authored
Merge branch 'master' into beta-new
2 parents d5374d0 + b5ddc2a commit 51ee456

6 files changed

Lines changed: 137 additions & 6 deletions

File tree

functions/utilities/privacy/debugRouter.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ const {
1515
acceptedByKey,
1616
variantModeKey,
1717
latestTouchKey,
18+
ageSetKey
1819
} = require("./privacyKeys");
19-
20-
// /api/privacy/student/ultra-secret/view (GET)
21-
// /api/privacy/student/ultra-secret/reset (POST)
20+
const {
21+
getGuardianEmails
22+
} = require("../getTypeSafeUser");
2223

2324
const userDB = firebase.firestore().collection("users");
2425

@@ -39,6 +40,7 @@ const getViewData = (req) => {
3940
acceptedByKey,
4041
variantModeKey,
4142
latestTouchKey,
43+
ageSetKey,
4244
"birthMonth",
4345
"birthYear",
4446
];
@@ -49,6 +51,7 @@ const getViewData = (req) => {
4951
data[key] = req.user[key] || null;
5052
});
5153

54+
data.guardianEmails = getGuardianEmails(req.user) || [];
5255
data.privacyState = req.user.privacy || null;
5356
data.isUnderThirteen = isUnderThirteen(req.user);
5457

@@ -79,8 +82,8 @@ router.post("/reset", studentMiddleware, async (req, res) => {
7982
[acceptedByKey]: req.body.acceptedByKey || null,
8083
[variantModeKey]: req.body.variantMode || null,
8184
[latestTouchKey]: req.body.latestTouch || null,
82-
birthMonth: req.body.birthMonth || "1",
83-
birthYear: req.body.birthYear || "2025",
85+
birthMonth: req.body.birthMonth || null,
86+
birthYear: req.body.birthYear || null,
8487
};
8588

8689
if (req.body.emails) {

functions/utilities/privacy/privacyKeys.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const guardianPrivacyAuthTokenKey = "guardianPrivacyAuthToken";
1616
const acceptedKey = `${latestPrivacyVersion}-accepted`;
1717
const acceptedByKey = `${latestPrivacyVersion}-acceptedBy`;
1818
const variantModeKey = `${latestPrivacyVersion}-variant`;
19+
const ageSetKey = `${latestPrivacyVersion}-ageSet`;
1920

2021
module.exports = {
2122
firstSeenKey,
@@ -26,4 +27,5 @@ module.exports = {
2627
acceptedKey,
2728
acceptedByKey,
2829
variantModeKey,
30+
ageSetKey
2931
};

functions/utilities/privacy/router.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const {
1212
guardianPrivacyAuthTokenKey,
1313
acceptedKey,
1414
acceptedByKey,
15-
latestTouchKey
15+
latestTouchKey,
16+
ageSetKey
1617
} = require("./privacyKeys");
1718
const debugRouter = require("./debugRouter");
1819
const {
@@ -175,6 +176,47 @@ router.post("/student/update", studentMiddleware, async (req, res) => {
175176
}
176177
});
177178

179+
router.post("/student/update-age", studentMiddleware, async (req, res) => {
180+
try {
181+
const month = req.body.month;
182+
const year = req.body.year;
183+
184+
if (
185+
typeof month !== "number" ||
186+
month < 1 ||
187+
month > 12 ||
188+
typeof year !== "number" ||
189+
year < 1900 ||
190+
year > new Date().getFullYear()
191+
) {
192+
return res.status(200).send({
193+
success: false,
194+
error: "Invalid month or year",
195+
data: null,
196+
});
197+
}
198+
199+
const updateBody = {
200+
birthMonth: String(month),
201+
birthYear: String(year),
202+
[ageSetKey]: Date.now(),
203+
};
204+
205+
await userDB.doc(req.user.uid).update(updateBody);
206+
207+
return res
208+
.status(200)
209+
.send({ success: true, data: updateBody, error: null });
210+
} catch (e) {
211+
console.error("Failed to update age", req.user.uid, e);
212+
return res.status(200).send({
213+
success: false,
214+
error: "Failed to update age",
215+
data: null,
216+
});
217+
}
218+
});
219+
178220
router.get("/student/accept", studentMiddleware, async (req, res) => {
179221
try {
180222
const updateBody = {

functions/utilities/privacy/utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,42 @@ const shouldRetryPopup = (lastTouched, isUnder13, guardianEmails) => {
7575
}
7676
}
7777

78+
const hasValidAge = (email, user) => {
79+
try {
80+
if (!email.includes("@mcmill.co.uk")) return true; // only enforce for test accounts for now
81+
if (user.code) return true; // assume teachers are valid
82+
83+
const birthYear = getCleanNumber(user?.birthYear);
84+
const birthMonth = getCleanNumber(user?.birthMonth);
85+
86+
if (!birthYear || !birthMonth) return false;
87+
if (birthYear < 1900 || birthYear > new Date().getFullYear()) return false;
88+
if (birthMonth < 1 || birthMonth > 12) return false;
89+
90+
return true;
91+
} catch {
92+
return true; // fail safe
93+
}
94+
}
95+
7896
const getPrivacyState = (email, user) => {
7997
const isUnder13 = isUnderThirteen(user);
8098
const variant = getPrivacyVariant(user);
8199
const guardianEmails = getGuardianEmails(user);
100+
const validAge = hasValidAge(email, user);
82101

83102
const useUnder13 = isUnder13 && variant !== 'year8webinar'
84103

104+
if (!validAge) {
105+
// User has invalid age data
106+
return {
107+
debug: 1,
108+
visible: true,
109+
mode: "update-age",
110+
variant
111+
};
112+
}
113+
85114
if (user[acceptedKey]) {
86115
// User has already accepted
87116
return {

scripts/privacy-reset-bulk.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const fb = require('firebase-admin');
4+
const downloadUsers = require('./utils/downloadUsers');
5+
const {
6+
firstSeenKey,
7+
latestTouchKey,
8+
dueByKey,
9+
userNeedsGuardianTouchKey,
10+
guardianPrivacyAuthTokenKey,
11+
acceptedKey,
12+
acceptedByKey,
13+
variantModeKey,
14+
ageSetKey
15+
} = require('../functions/utilities/privacy/privacyKeys');
16+
const { getGuardianEmails } = require('../functions/utilities/getTypeSafeUser');
17+
18+
const userDb = fb.firestore().collection('users');
19+
20+
const dry = true;
21+
22+
const run = async () => {
23+
let users = await downloadUsers();
24+
const file = path.join(__dirname, `../private/tmp-users.json`);
25+
const accounts = JSON.parse(fs.readFileSync(file)).users;
26+
27+
for (let a of accounts) {
28+
const u = users[a.localId];
29+
if (!u) continue;
30+
31+
const emails = getGuardianEmails(u);
32+
33+
if (u[userNeedsGuardianTouchKey] && !emails.length) {
34+
console.log('Resetting privacy touch for user without guardian email:', a.localId);
35+
36+
if (!dry) {
37+
await userDb.doc(a.localId).set({
38+
[firstSeenKey]: fb.firestore.FieldValue.delete(),
39+
[latestTouchKey]: fb.firestore.FieldValue.delete(),
40+
[dueByKey]: fb.firestore.FieldValue.delete(),
41+
[userNeedsGuardianTouchKey]: fb.firestore.FieldValue.delete(),
42+
[guardianPrivacyAuthTokenKey]: fb.firestore.FieldValue.delete(),
43+
[acceptedKey]: fb.firestore.FieldValue.delete(),
44+
[acceptedByKey]: fb.firestore.FieldValue.delete(),
45+
[variantModeKey]: fb.firestore.FieldValue.delete(),
46+
[ageSetKey]: fb.firestore.FieldValue.delete(),
47+
}, {merge: true});
48+
}
49+
}
50+
}
51+
}
52+
53+
run();

scripts/privacy-reset.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
acceptedKey,
1212
acceptedByKey,
1313
variantModeKey,
14+
ageSetKey
1415
} = require('../functions/utilities/privacy/privacyKeys');
1516

1617
// ---------------------
@@ -51,6 +52,7 @@ const run = async () => {
5152
[acceptedKey]: fb.firestore.FieldValue.delete(),
5253
[acceptedByKey]: fb.firestore.FieldValue.delete(),
5354
[variantModeKey]: fb.firestore.FieldValue.delete(),
55+
[ageSetKey]: fb.firestore.FieldValue.delete(),
5456
}, {merge: true});
5557

5658
console.log(`User "${email}" privacy data deleted`);

0 commit comments

Comments
 (0)