Skip to content

Commit ab842bc

Browse files
committed
Symlink submission-created across courses
Otherwise the Piscine doesn't get register events handled
1 parent 6573e0d commit ab842bc

File tree

3 files changed

+110
-108
lines changed

3 files changed

+110
-108
lines changed

org-cyf-itp/tooling/netlify/functions/submission-created.js

Lines changed: 0 additions & 108 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../tooling/netlify/functions/submission-created.js
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../tooling/netlify/functions/submission-created.js
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// This function handles forwards netlify form submissions to our register spreadsheets.
2+
// Note that errors are reported in the netlify console, but are _not_ exposed to users who submit the form - they will see successful submission even if this function fails.
3+
//
4+
// This function is automatically called for all form submissions, because it is named exactly submission-create.
5+
// It cannot be manually called by users, and is only callable by Netlify itself.
6+
// If we end up adding multiple forms to the site, we'll need to demux them here in some way and filter out other forms.
7+
//
8+
// The underlying form data is in the body (which is a JSON object) under .payload.data.
9+
10+
import {Auth, google} from "googleapis";
11+
12+
// Each sheet listed here was manually crated, and its spreadsheet ID is taken from its URL.
13+
// Each spreadsheet is expected to contain one sheet per module, named for the module.
14+
// Each module sheet is expected to contain the following columns, in order:
15+
// Given Name | Family Name | Email | Timestamp | Course | Module | Day | Build Time
16+
// Each spreadsheet must also give write access to the email listed below in CREDENTIALS.
17+
const COURSE_TO_SPREADSHEET_ID = {
18+
"cyf-itp": "1YHKPCCN55PJD-o1jg4wbVKI3kbhB-ULiwB5hhG17DcA",
19+
"cyf-piscine": "1XabWuYqvOUiY7HpUra0Vdic4pSxmXmNRZHMR72I1bjk",
20+
};
21+
22+
const CREDENTIALS = {
23+
// This was generated by:
24+
// 1. Visit https://console.cloud.google.com/apis/credentials?project=cyf-syllabus
25+
// 2. Generate or find the service account.
26+
// 3. Download its credentials as a JSON file.
27+
// 4. Fetch the keys listed below - the non-env-var ones are not really secret.
28+
// 5. For private_key, this is read from an env var on netlify, and env vars cannot contain newlines, so we put the string "\n" where we need newlines, accordingly, we undo this below.
29+
// The env var was constructed by running: `jq '.private_key' <cyf-syllabus-7ca5140fd0c6.json | tr -d '"'`.
30+
"private_key": process.env["GOOGLEAPI_REGISTER_PRIVATE_KEY"].replaceAll("\\n", "\n"),
31+
"client_email": "register@cyf-syllabus.iam.gserviceaccount.com",
32+
"client_id": "113977060196146055874",
33+
};
34+
35+
const handler = async (event, context) => {
36+
console.log("Got request with body", event.body);
37+
let body;
38+
try {
39+
// TODO: Check for structure
40+
body = JSON.parse(event.body);
41+
} catch (error) {
42+
console.error(`Failed to parse request as valid JSON: ${event.body}: ${error}`);
43+
return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")};
44+
}
45+
if (!("payload" in body) || !("data" in body.payload)) {
46+
console.error(`Failed to parse request - missing .payload.data: ${event.body}`);
47+
return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")};
48+
}
49+
const request = body.payload.data;
50+
51+
for (const requiredField of ["course", "module", "name", "email", "day", "location", "buildTime"]) {
52+
if (!(requiredField in request)) {
53+
const error = `Request missing field ${requiredField}`;
54+
console.error(error, request);
55+
return {statusCode: 400, body: JSON.stringify(error)};
56+
}
57+
const type = typeof request[requiredField];
58+
if (type !== "string") {
59+
const error = `Field ${requiredField} must be of type string but was ${type}`;
60+
console.error(error, request);
61+
return {statusCode: 400, body: JSON.stringify(error)};
62+
}
63+
}
64+
65+
if (!(request.course in COURSE_TO_SPREADSHEET_ID)) {
66+
console.error(`Didn't recognise course ${request.course}`);
67+
return {statusCode: 404, body: JSON.stringify(`Unknown course: ${request.course}`)};
68+
}
69+
70+
const auth = new Auth.GoogleAuth({
71+
credentials: CREDENTIALS,
72+
scopes: "https://www.googleapis.com/auth/spreadsheets",
73+
});
74+
75+
const sheets = google.sheets({version: "v4", auth});
76+
77+
try {
78+
const response = await sheets.spreadsheets.values.append({
79+
spreadsheetId: COURSE_TO_SPREADSHEET_ID[request.course],
80+
// Trust that the user-supplied module exists.
81+
// If it's wrong, we'll get a 400 from the API and pass it on to the user without much useful detail.
82+
range: request.module,
83+
valueInputOption: "RAW",
84+
insertDataOption: "INSERT_ROWS",
85+
resource: {
86+
values: [
87+
[request.name, request.email, new Date().toISOString(), request.course, request.module, request.day, request.location, request.buildTime],
88+
],
89+
},
90+
});
91+
} catch (error) {
92+
console.error("Error from Google API", error);
93+
let message = "An error occurred signing the register";
94+
if (error.errors) {
95+
if ("message" in error.errors[0]) {
96+
message += ": " + error.errors[0].message;
97+
}
98+
}
99+
return {statusCode: 400, body: JSON.stringify(message)};
100+
}
101+
102+
return {
103+
statusCode: 200,
104+
body: JSON.stringify("You have successfully signed the register"),
105+
};
106+
};
107+
108+
export { handler };

0 commit comments

Comments
 (0)