@olinfo/quizms-firebase 4.0.0-canary.20251130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/bin/bin.js +4 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +36 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/entry.d.ts +4 -0
  6. package/dist/entry.js +19 -0
  7. package/dist/entry.js.map +1 -0
  8. package/dist/functions.d.ts +14 -0
  9. package/dist/functions.js +104 -0
  10. package/dist/functions.js.map +1 -0
  11. package/dist/package.json.js +5 -0
  12. package/dist/package.json.js.map +1 -0
  13. package/dist/src/cli/definalize.js +23 -0
  14. package/dist/src/cli/definalize.js.map +1 -0
  15. package/dist/src/cli/export.js +54 -0
  16. package/dist/src/cli/export.js.map +1 -0
  17. package/dist/src/cli/files/firestore-indexes.json.js +10 -0
  18. package/dist/src/cli/files/firestore-indexes.json.js.map +1 -0
  19. package/dist/src/cli/files/firestore.rules.js +191 -0
  20. package/dist/src/cli/files/firestore.rules.js.map +1 -0
  21. package/dist/src/cli/files/storage.rules.js +35 -0
  22. package/dist/src/cli/files/storage.rules.js.map +1 -0
  23. package/dist/src/cli/import.js +140 -0
  24. package/dist/src/cli/import.js.map +1 -0
  25. package/dist/src/cli/init.js +140 -0
  26. package/dist/src/cli/init.js.map +1 -0
  27. package/dist/src/cli/update-scores.js +37 -0
  28. package/dist/src/cli/update-scores.js.map +1 -0
  29. package/dist/src/cli/utils/collection.js +55 -0
  30. package/dist/src/cli/utils/collection.js.map +1 -0
  31. package/dist/src/cli/utils/converters-admin.js +41 -0
  32. package/dist/src/cli/utils/converters-admin.js.map +1 -0
  33. package/dist/src/cli/utils/initialize.js +31 -0
  34. package/dist/src/cli/utils/initialize.js.map +1 -0
  35. package/dist/src/cli/utils/rest-api.js +23 -0
  36. package/dist/src/cli/utils/rest-api.js.map +1 -0
  37. package/dist/src/cli/utils/storage.js +41 -0
  38. package/dist/src/cli/utils/storage.js.map +1 -0
  39. package/dist/src/cli/utils/users.js +30 -0
  40. package/dist/src/cli/utils/users.js.map +1 -0
  41. package/dist/src/models/user.js +12 -0
  42. package/dist/src/models/user.js.map +1 -0
  43. package/dist/src/vite/entry.js +33 -0
  44. package/dist/src/vite/entry.js.map +1 -0
  45. package/dist/src/vite/preset.js +30 -0
  46. package/dist/src/vite/preset.js.map +1 -0
  47. package/dist/src/web/admin/announcements.js +85 -0
  48. package/dist/src/web/admin/announcements.js.map +1 -0
  49. package/dist/src/web/admin/contest-settings.js +16 -0
  50. package/dist/src/web/admin/contest-settings.js.map +1 -0
  51. package/dist/src/web/admin/context.js +11 -0
  52. package/dist/src/web/admin/context.js.map +1 -0
  53. package/dist/src/web/admin/dashboard.js +85 -0
  54. package/dist/src/web/admin/dashboard.js.map +1 -0
  55. package/dist/src/web/admin/export.js +59 -0
  56. package/dist/src/web/admin/export.js.map +1 -0
  57. package/dist/src/web/admin/layout.js +38 -0
  58. package/dist/src/web/admin/layout.js.map +1 -0
  59. package/dist/src/web/admin/provider.js +30 -0
  60. package/dist/src/web/admin/provider.js.map +1 -0
  61. package/dist/src/web/admin/school-table.js +79 -0
  62. package/dist/src/web/admin/school-table.js.map +1 -0
  63. package/dist/src/web/common/api.js +43 -0
  64. package/dist/src/web/common/api.js.map +1 -0
  65. package/dist/src/web/common/base-login.js +26 -0
  66. package/dist/src/web/common/base-login.js.map +1 -0
  67. package/dist/src/web/common/converters.js +57 -0
  68. package/dist/src/web/common/converters.js.map +1 -0
  69. package/dist/src/web/common/query.js +16 -0
  70. package/dist/src/web/common/query.js.map +1 -0
  71. package/dist/src/web/common/token-login.js +40 -0
  72. package/dist/src/web/common/token-login.js.map +1 -0
  73. package/dist/src/web/entry/admin.js +27 -0
  74. package/dist/src/web/entry/admin.js.map +1 -0
  75. package/dist/src/web/entry/student-statement.js +28 -0
  76. package/dist/src/web/entry/student-statement.js.map +1 -0
  77. package/dist/src/web/entry/student.js +126 -0
  78. package/dist/src/web/entry/student.js.map +1 -0
  79. package/dist/src/web/entry/teacher.js +88 -0
  80. package/dist/src/web/entry/teacher.js.map +1 -0
  81. package/dist/src/web/hooks/auth.js +26 -0
  82. package/dist/src/web/hooks/auth.js.map +1 -0
  83. package/dist/src/web/hooks/collection.js +68 -0
  84. package/dist/src/web/hooks/collection.js.map +1 -0
  85. package/dist/src/web/hooks/count.js +22 -0
  86. package/dist/src/web/hooks/count.js.map +1 -0
  87. package/dist/src/web/hooks/document.js +61 -0
  88. package/dist/src/web/hooks/document.js.map +1 -0
  89. package/dist/src/web/hooks/subscription.js +42 -0
  90. package/dist/src/web/hooks/subscription.js.map +1 -0
  91. package/dist/vite.d.ts +4 -0
  92. package/dist/vite.js +7 -0
  93. package/dist/vite.js.map +1 -0
  94. package/package.json +83 -0
package/bin/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env -S node --no-deprecation --experimental-import-meta-resolve
2
+ "use strict";
3
+
4
+ void import("../dist/cli.js");
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,36 @@
1
+ import { existsSync as n } from "node:fs";
2
+ import { homedir as a } from "node:os";
3
+ import e from "node:path";
4
+ import { exit as c, cwd as m, chdir as d } from "node:process";
5
+ import { fatal as f } from "@olinfo/quizms/utils-node";
6
+ import { program as t, Option as r } from "commander";
7
+ import { version as s } from "./package.json.js";
8
+ import l from "./src/cli/definalize.js";
9
+ import h from "./src/cli/export.js";
10
+ import u from "./src/cli/import.js";
11
+ import x from "./src/cli/init.js";
12
+ import g from "./src/cli/update-scores.js";
13
+ function v() {
14
+ const p = a();
15
+ let o = m();
16
+ for (; ; ) {
17
+ if (n(e.join(o, "package.json")) && n(e.join(o, "src"))) {
18
+ d(o);
19
+ return;
20
+ }
21
+ const i = e.dirname(o);
22
+ (i === o || i === p) && f("Invalid directory. Make sure you're inside a QuizMS project."), o = i;
23
+ }
24
+ }
25
+ async function I() {
26
+ t.addHelpText("beforeAll", `QuizMS-firebase cli v${s}
27
+ `), t.name("quizms-firebase"), t.version(s), t.command("init").description("Initialize the Firebase project.").option("--force", "Overwrite existing files.").action(x), t.command("export").description("Export the contests data.").option("--contests", "Export the contests.").option("--participations", "Export the participations.").option("--students", "Export the students.").option("--variants", "Export the variants.").action(h), t.command("import").description("Import the contests data.").option("-c, --config <config>", "The contests config file.").option("--admins", "Import the admins.").option("--contests", "Import the contests.").option("--schools", "Import the schools.").option("--statements", "Import the statements.").option("--students", "Import the students.").option("--teachers", "Import the teachers.").option("--variants", "Import the variants.").option("-d, --delete", "Delete existing collections.").addOption(new r("-s, --skip-existing", "Skip existing files.").conflicts(["--delete"])).addOption(
28
+ new r("-f, --force", "Overwrite existing documents.").conflicts([
29
+ "--delete",
30
+ "--skip-existing"
31
+ ])
32
+ ).action(u), t.command("definalize").description("Definalize all participations.").action(l), t.command("update-scores").description("Update student scores.").action(g), v(), await t.parseAsync();
33
+ }
34
+ await I();
35
+ c(0);
36
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sources":["../src/cli/index.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { chdir, cwd, exit } from \"node:process\";\n\nimport { fatal } from \"@olinfo/quizms/utils-node\";\nimport { Option, program } from \"commander\";\nimport { version } from \"package.json\";\n\nimport definalize from \"./definalize\";\nimport exportData from \"./export\";\nimport importData from \"./import\";\nimport init from \"./init\";\nimport updateScores from \"./update-scores\";\n\nfunction findProjectDirectory() {\n const home = homedir();\n let root = cwd();\n for (;;) {\n if (existsSync(path.join(root, \"package.json\")) && existsSync(path.join(root, \"src\"))) {\n chdir(root);\n return;\n }\n const parent = path.dirname(root);\n if (parent === root || parent === home) {\n fatal(\"Invalid directory. Make sure you're inside a QuizMS project.\");\n }\n root = parent;\n }\n}\n\nasync function main() {\n program.addHelpText(\"beforeAll\", `QuizMS-firebase cli v${version}\\n`);\n program.name(\"quizms-firebase\");\n program.version(version);\n\n program\n .command(\"init\")\n .description(\"Initialize the Firebase project.\")\n .option(\"--force\", \"Overwrite existing files.\")\n .action(init);\n\n program\n .command(\"export\")\n .description(\"Export the contests data.\")\n .option(\"--contests\", \"Export the contests.\")\n .option(\"--participations\", \"Export the participations.\")\n .option(\"--students\", \"Export the students.\")\n .option(\"--variants\", \"Export the variants.\")\n .action(exportData);\n\n program\n .command(\"import\")\n .description(\"Import the contests data.\")\n .option(\"-c, --config <config>\", \"The contests config file.\")\n .option(\"--admins\", \"Import the admins.\")\n .option(\"--contests\", \"Import the contests.\")\n .option(\"--schools\", \"Import the schools.\")\n .option(\"--statements\", \"Import the statements.\")\n .option(\"--students\", \"Import the students.\")\n .option(\"--teachers\", \"Import the teachers.\")\n .option(\"--variants\", \"Import the variants.\")\n .option(\"-d, --delete\", \"Delete existing collections.\")\n .addOption(new Option(\"-s, --skip-existing\", \"Skip existing files.\").conflicts([\"--delete\"]))\n .addOption(\n new Option(\"-f, --force\", \"Overwrite existing documents.\").conflicts([\n \"--delete\",\n \"--skip-existing\",\n ]),\n )\n .action(importData);\n\n program.command(\"definalize\").description(\"Definalize all participations.\").action(definalize);\n program.command(\"update-scores\").description(\"Update student scores.\").action(updateScores);\n\n findProjectDirectory();\n await program.parseAsync();\n}\n\nawait main();\nexit(0);\n"],"names":["findProjectDirectory","home","homedir","root","cwd","existsSync","path","chdir","parent","fatal","main","program","version","init","exportData","Option","importData","definalize","updateScores","exit"],"mappings":";;;;;;;;;;;;AAeA,SAASA,IAAuB;AAC9B,QAAMC,IAAOC,EAAA;AACb,MAAIC,IAAOC,EAAA;AACX,aAAS;AACP,QAAIC,EAAWC,EAAK,KAAKH,GAAM,cAAc,CAAC,KAAKE,EAAWC,EAAK,KAAKH,GAAM,KAAK,CAAC,GAAG;AACrF,MAAAI,EAAMJ,CAAI;AACV;AAAA,IACF;AACA,UAAMK,IAASF,EAAK,QAAQH,CAAI;AAChC,KAAIK,MAAWL,KAAQK,MAAWP,MAChCQ,EAAM,8DAA8D,GAEtEN,IAAOK;AAAA,EACT;AACF;AAEA,eAAeE,IAAO;AACpB,EAAAC,EAAQ,YAAY,aAAa,wBAAwBC,CAAO;AAAA,CAAI,GACpED,EAAQ,KAAK,iBAAiB,GAC9BA,EAAQ,QAAQC,CAAO,GAEvBD,EACG,QAAQ,MAAM,EACd,YAAY,kCAAkC,EAC9C,OAAO,WAAW,2BAA2B,EAC7C,OAAOE,CAAI,GAEdF,EACG,QAAQ,QAAQ,EAChB,YAAY,2BAA2B,EACvC,OAAO,cAAc,sBAAsB,EAC3C,OAAO,oBAAoB,4BAA4B,EACvD,OAAO,cAAc,sBAAsB,EAC3C,OAAO,cAAc,sBAAsB,EAC3C,OAAOG,CAAU,GAEpBH,EACG,QAAQ,QAAQ,EAChB,YAAY,2BAA2B,EACvC,OAAO,yBAAyB,2BAA2B,EAC3D,OAAO,YAAY,oBAAoB,EACvC,OAAO,cAAc,sBAAsB,EAC3C,OAAO,aAAa,qBAAqB,EACzC,OAAO,gBAAgB,wBAAwB,EAC/C,OAAO,cAAc,sBAAsB,EAC3C,OAAO,cAAc,sBAAsB,EAC3C,OAAO,cAAc,sBAAsB,EAC3C,OAAO,gBAAgB,8BAA8B,EACrD,UAAU,IAAII,EAAO,uBAAuB,sBAAsB,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAC3F;AAAA,IACC,IAAIA,EAAO,eAAe,+BAA+B,EAAE,UAAU;AAAA,MACnE;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EAAA,EAEF,OAAOC,CAAU,GAEpBL,EAAQ,QAAQ,YAAY,EAAE,YAAY,gCAAgC,EAAE,OAAOM,CAAU,GAC7FN,EAAQ,QAAQ,eAAe,EAAE,YAAY,wBAAwB,EAAE,OAAOO,CAAY,GAE1FlB,EAAA,GACA,MAAMW,EAAQ,WAAA;AAChB;AAEA,MAAMD,EAAA;AACNS,EAAK,CAAC;"}
@@ -0,0 +1,4 @@
1
+ declare function createFirebaseEntry(): void;
2
+ export default createFirebaseEntry;
3
+
4
+ export { }
package/dist/entry.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsx as r, jsxs as n } from "react/jsx-runtime";
2
+ import { lazy as t } from "react";
3
+ import { createApp as o } from "@olinfo/quizms/entry";
4
+ import { Router as i, Switch as c, Route as e } from "wouter";
5
+ import { FirebaseLogin as m } from "./src/web/common/base-login.js";
6
+ const p = t(() => import("./src/web/entry/admin.js")), a = t(() => import("./src/web/entry/teacher.js")), s = t(() => import("./src/web/entry/student.js"));
7
+ function E() {
8
+ return o(
9
+ /* @__PURE__ */ r(i, { base: process.env.NODE_ENV === "development" ? "/firebase" : "", children: /* @__PURE__ */ r(m, { children: /* @__PURE__ */ n(c, { children: [
10
+ /* @__PURE__ */ r(e, { path: "/admin", nest: !0, children: /* @__PURE__ */ r(p, {}) }),
11
+ /* @__PURE__ */ r(e, { path: "/teacher", nest: !0, children: /* @__PURE__ */ r(a, {}) }),
12
+ /* @__PURE__ */ r(e, { path: "/", children: /* @__PURE__ */ r(s, {}) })
13
+ ] }) }) })
14
+ );
15
+ }
16
+ export {
17
+ E as default
18
+ };
19
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sources":["../src/web/entry/index.tsx"],"sourcesContent":["import { lazy } from \"react\";\n\nimport { createApp } from \"@olinfo/quizms/entry\";\nimport { Route, Router, Switch } from \"wouter\";\n\nimport { FirebaseLogin } from \"~/web/common/base-login\";\n\nconst AdminEntry = lazy(() => import(\"./admin\"));\nconst TeacherEntry = lazy(() => import(\"./teacher\"));\nconst StudentEntry = lazy(() => import(\"./student\"));\n\nexport default function createFirebaseEntry() {\n return createApp(\n <Router base={process.env.NODE_ENV === \"development\" ? \"/firebase\" : \"\"}>\n <FirebaseLogin>\n <Switch>\n <Route path=\"/admin\" nest>\n <AdminEntry />\n </Route>\n <Route path=\"/teacher\" nest>\n <TeacherEntry />\n </Route>\n <Route path=\"/\">\n <StudentEntry />\n </Route>\n </Switch>\n </FirebaseLogin>\n </Router>,\n );\n}\n"],"names":["AdminEntry","lazy","TeacherEntry","StudentEntry","createFirebaseEntry","createApp","jsx","Router","FirebaseLogin","jsxs","Switch","Route"],"mappings":";;;;;AAOA,MAAMA,IAAaC,EAAK,MAAM,OAAO,0BAAS,CAAC,GACzCC,IAAeD,EAAK,MAAM,OAAO,4BAAW,CAAC,GAC7CE,IAAeF,EAAK,MAAM,OAAO,4BAAW,CAAC;AAEnD,SAAwBG,IAAsB;AAC5C,SAAOC;AAAA,IACL,gBAAAC,EAACC,GAAA,EAAO,MAAM,QAAQ,IAAI,aAAa,gBAAgB,cAAc,IACnE,UAAA,gBAAAD,EAACE,GAAA,EACC,UAAA,gBAAAC,EAACC,GAAA,EACC,UAAA;AAAA,MAAA,gBAAAJ,EAACK,KAAM,MAAK,UAAS,MAAI,IACvB,UAAA,gBAAAL,EAACN,KAAW,EAAA,CACd;AAAA,MACA,gBAAAM,EAACK,KAAM,MAAK,YAAW,MAAI,IACzB,UAAA,gBAAAL,EAACJ,KAAa,EAAA,CAChB;AAAA,wBACCS,GAAA,EAAM,MAAK,KACV,UAAA,gBAAAL,EAACH,KAAa,EAAA,CAChB;AAAA,IAAA,EAAA,CACF,GACF,EAAA,CACF;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,14 @@
1
+ import { CallableFunction as CallableFunction_2 } from 'firebase-functions/https';
2
+
3
+ export declare const api: CallableFunction_2<any, Promise<EndpointSuccess<any> | {
4
+ success: boolean;
5
+ errorCode: string;
6
+ error: string;
7
+ }>, unknown>;
8
+
9
+ declare type EndpointSuccess<Res> = {
10
+ success: true;
11
+ data: Res;
12
+ };
13
+
14
+ export { }
@@ -0,0 +1,104 @@
1
+ import * as p from "node:crypto";
2
+ import { randomToken as u } from "@olinfo/quizms/utils";
3
+ import { roundToNearestMinutes as f, addSeconds as l, addMinutes as h, isFuture as m, isPast as T } from "date-fns";
4
+ import { initializeApp as w } from "firebase-admin/app";
5
+ import { getAuth as C } from "firebase-admin/auth";
6
+ import { getFirestore as N } from "firebase-admin/firestore";
7
+ import { onCall as g } from "firebase-functions/https";
8
+ import * as n from "firebase-functions/logger";
9
+ import { participationConverter as I, contestConverter as O } from "./src/cli/utils/converters-admin.js";
10
+ w();
11
+ const i = N(), E = C(), S = (t) => (n.info("Student login request received", { data: t.data }), { success: !1, errorCode: "NOT_IMPLEMENTED", error: "Not implemented yet" }), D = async (t) => {
12
+ n.info("Teacher login request received", { data: t.data });
13
+ const r = await i.collection("users").where("username", "==", t.data.username).get();
14
+ if (r.empty)
15
+ return { success: !1, errorCode: "USER_NOT_FOUND", error: "User not found" };
16
+ const e = r.docs[0], a = e.data();
17
+ return a.role !== "teacher" ? { success: !1, errorCode: "INVALID_ROLE", error: "User is not a teacher" } : a.password !== t.data.password ? { success: !1, errorCode: "INVALID_PASSWORD", error: "Wrong password" } : { success: !0, data: { token: await E.createCustomToken(p.randomUUID(), {
18
+ role: "teacher",
19
+ schoolId: e.id
20
+ }) } };
21
+ };
22
+ async function c(t, r) {
23
+ const e = t.data.participationId, a = await i.doc(`participations/${e}`).withConverter(I).get();
24
+ if (!a.exists)
25
+ return [
26
+ { success: !1, errorCode: "PARTICIPATION_NOT_FOUND", error: "Participation not found" }
27
+ ];
28
+ const o = a.data();
29
+ if (o.schoolId !== t.auth?.token.schoolId)
30
+ return [
31
+ {
32
+ success: !1,
33
+ errorCode: "FORBIDDEN",
34
+ error: "You don't have permission to access this participation"
35
+ }
36
+ ];
37
+ const s = (await i.doc(`contests/${o.contestId}`).withConverter(O).get()).data();
38
+ if (r) {
39
+ if (!s.hasOnline)
40
+ return [{ success: !1, errorCode: "CONTEST_OFFLINE", error: "Contest is not online" }];
41
+ if (m(s.contestWindowStart))
42
+ return [
43
+ { success: !1, errorCode: "CONTEST_NOT_STARTED", error: "Contest has not started yet" }
44
+ ];
45
+ if (T(s.contestWindowEnd))
46
+ return [{ success: !1, errorCode: "CONTEST_ENDED", error: "Contest has already ended" }];
47
+ }
48
+ return [null, o, s];
49
+ }
50
+ const P = async (t) => {
51
+ n.info("Teacher start participation received", { data: t.data });
52
+ const [r, e, a] = await c(t, !0);
53
+ if (r)
54
+ return r;
55
+ const o = f(l(Date.now(), 3.5 * 60)), d = h(o, a.hasOnline ? a.duration : 0);
56
+ return await i.doc(`participations/${e.id}`).update({
57
+ token: await u(),
58
+ // TODO: controllare duplicati
59
+ startingTime: o,
60
+ endingTime: d
61
+ }), { success: !0, data: {} };
62
+ }, _ = async (t) => {
63
+ n.info("Teacher stop participation received", { data: t.data });
64
+ const [r, e] = await c(t, !0);
65
+ return r || (await i.doc(`participations/${e.id}`).update({
66
+ token: null,
67
+ startingTime: null,
68
+ endingTime: null
69
+ }), { success: !0, data: {} });
70
+ }, y = async (t) => {
71
+ n.info("Teacher finalize participation received", { data: t.data });
72
+ const [r, e] = await c(t, !1);
73
+ return r || (await i.doc(`participations/${e.id}`).update({ finalized: !0 }), { success: !0, data: {} });
74
+ }, $ = g(
75
+ {
76
+ region: "europe-west6",
77
+ cors: ["http://localhost:1234"]
78
+ },
79
+ async (t) => {
80
+ n.info(`Received request for action: ${t.data.action}`);
81
+ try {
82
+ switch (t.data.action) {
83
+ case "studentLogin":
84
+ return S(t);
85
+ case "teacherLogin":
86
+ return await D(t);
87
+ case "teacherStartParticipation":
88
+ return await P(t);
89
+ case "teacherStopParticipation":
90
+ return await _(t);
91
+ case "teacherFinalizeParticipation":
92
+ return y(t);
93
+ default:
94
+ return { success: !1, errorCode: "UNKNOWN_ACTION", error: "Unknown action" };
95
+ }
96
+ } catch (r) {
97
+ return n.error("Error processing request", r), { success: !1, errorCode: "INTERNAL_ERROR", error: "An internal error occurred" };
98
+ }
99
+ }
100
+ );
101
+ export {
102
+ $ as api
103
+ };
104
+ //# sourceMappingURL=functions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functions.js","sources":["../src/functions/index.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\n\nimport type { Contest, Participation } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport { initializeApp } from \"firebase-admin/app\";\nimport { getAuth } from \"firebase-admin/auth\";\nimport { getFirestore } from \"firebase-admin/firestore\";\nimport { type CallableRequest, onCall } from \"firebase-functions/https\";\nimport * as logger from \"firebase-functions/logger\";\n\nimport { contestConverter, participationConverter } from \"~/cli/utils/converters-admin\";\n\ninitializeApp();\nconst db = getFirestore();\nconst auth = getAuth();\n\ntype MaybePromise<T> = T | Promise<T>;\n\ntype EndpointSuccess<Res> = { success: true; data: Res };\ntype EndpointError = { success: false; errorCode: string; error: string };\n\ntype Endpoint<Req = any, Res = any> = (\n request: CallableRequest<Req>,\n) => MaybePromise<EndpointSuccess<Res> | EndpointError>;\n\nconst studentLogin: Endpoint = (request) => {\n logger.info(\"Student login request received\", { data: request.data });\n return { success: false, errorCode: \"NOT_IMPLEMENTED\", error: \"Not implemented yet\" };\n};\n\nconst teacherLogin: Endpoint = async (request) => {\n logger.info(\"Teacher login request received\", { data: request.data });\n const snapshot = await db\n .collection(\"users\")\n .where(\"username\", \"==\", request.data.username)\n .get();\n\n if (snapshot.empty) {\n return { success: false, errorCode: \"USER_NOT_FOUND\", error: \"User not found\" };\n }\n\n const doc = snapshot.docs[0];\n const user = doc.data();\n\n if (user.role !== \"teacher\") {\n return { success: false, errorCode: \"INVALID_ROLE\", error: \"User is not a teacher\" };\n }\n if (user.password !== request.data.password) {\n return { success: false, errorCode: \"INVALID_PASSWORD\", error: \"Wrong password\" };\n }\n const token = await auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: doc.id,\n });\n return { success: true, data: { token } };\n};\n\nasync function getParticipation(\n request: CallableRequest,\n onlineOnly: boolean,\n): Promise<[EndpointError] | [null, Participation, Contest]> {\n const participationId = request.data.participationId;\n\n const snapshot = await db\n .doc(`participations/${participationId}`)\n .withConverter(participationConverter)\n .get();\n if (!snapshot.exists) {\n return [\n { success: false, errorCode: \"PARTICIPATION_NOT_FOUND\", error: \"Participation not found\" },\n ];\n }\n\n const participation = snapshot.data()!;\n if (participation.schoolId !== request.auth?.token.schoolId) {\n return [\n {\n success: false,\n errorCode: \"FORBIDDEN\",\n error: \"You don't have permission to access this participation\",\n },\n ];\n }\n\n const contestSnapshot = await db\n .doc(`contests/${participation.contestId}`)\n .withConverter(contestConverter)\n .get();\n const contest = contestSnapshot.data()!;\n if (onlineOnly) {\n if (!contest.hasOnline) {\n return [{ success: false, errorCode: \"CONTEST_OFFLINE\", error: \"Contest is not online\" }];\n }\n if (isFuture(contest.contestWindowStart)) {\n return [\n { success: false, errorCode: \"CONTEST_NOT_STARTED\", error: \"Contest has not started yet\" },\n ];\n }\n if (isPast(contest.contestWindowEnd)) {\n return [{ success: false, errorCode: \"CONTEST_ENDED\", error: \"Contest has already ended\" }];\n }\n }\n\n return [null, participation, contest];\n}\n\nconst teacherStartParticipation: Endpoint = async (request) => {\n logger.info(\"Teacher start participation received\", { data: request.data });\n\n const [error, participation, contest] = await getParticipation(request, true);\n if (error) {\n return error;\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n await db.doc(`participations/${participation.id}`).update({\n token: await randomToken(), // TODO: controllare duplicati\n startingTime: startingTime,\n endingTime: endingTime,\n });\n\n return { success: true, data: {} };\n};\n\nconst teacherStopParticipation: Endpoint = async (request) => {\n logger.info(\"Teacher stop participation received\", { data: request.data });\n\n const [error, participation] = await getParticipation(request, true);\n if (error) {\n return error;\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n\n return { success: true, data: {} };\n};\n\nconst teacherFinalizeParticipation: Endpoint = async (request) => {\n logger.info(\"Teacher finalize participation received\", { data: request.data });\n\n const [error, participation] = await getParticipation(request, false);\n if (error) {\n return error;\n }\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n\n return { success: true, data: {} };\n};\n\nexport const api = onCall(\n {\n region: \"europe-west6\",\n cors: [\"http://localhost:1234\"],\n },\n async (request) => {\n logger.info(`Received request for action: ${request.data.action}`);\n try {\n switch (request.data.action) {\n case \"studentLogin\":\n return studentLogin(request);\n case \"teacherLogin\":\n return await teacherLogin(request);\n case \"teacherStartParticipation\":\n return await teacherStartParticipation(request);\n case \"teacherStopParticipation\":\n return await teacherStopParticipation(request);\n case \"teacherFinalizeParticipation\":\n return teacherFinalizeParticipation(request);\n default:\n return { success: false, errorCode: \"UNKNOWN_ACTION\", error: \"Unknown action\" };\n }\n } catch (error) {\n logger.error(\"Error processing request\", error);\n return { success: false, errorCode: \"INTERNAL_ERROR\", error: \"An internal error occurred\" };\n }\n },\n);\n"],"names":["initializeApp","db","getFirestore","auth","getAuth","studentLogin","request","logger","teacherLogin","snapshot","doc","user","crypto","getParticipation","onlineOnly","participationId","participationConverter","participation","contest","contestConverter","isFuture","isPast","teacherStartParticipation","error","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","randomToken","teacherStopParticipation","teacherFinalizeParticipation","api","onCall"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ const n = "4.0.0-canary.20251130";
2
+ export {
3
+ n as version
4
+ };
5
+ //# sourceMappingURL=package.json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,23 @@
1
+ import { info as r, success as l } from "@olinfo/quizms/utils-node";
2
+ import { sumBy as p } from "lodash-es";
3
+ import { participationConverter as c } from "./utils/converters-admin.js";
4
+ import { initializeFirebase as f } from "./utils/initialize.js";
5
+ async function h() {
6
+ const { db: a } = await f();
7
+ r("Definalizing all participations.");
8
+ const e = a.collection("participations").where("finalized", "==", !1).withConverter(c).limit(1e3);
9
+ let i = await e.get(), t = 0;
10
+ for (; !i.empty; ) {
11
+ const o = await Promise.all(
12
+ i.docs.map(async (n) => n.data().finalized ? (await a.doc(n.ref.path).update({ finalized: !1 }), 1) : 0)
13
+ );
14
+ t += p(o), r(`Definalized ${t} participations.`);
15
+ const s = i.docs.at(-1);
16
+ i = await e.startAfter(s).get();
17
+ }
18
+ l(`${t} participations were successfully definalized.`);
19
+ }
20
+ export {
21
+ h as default
22
+ };
23
+ //# sourceMappingURL=definalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definalize.js","sources":["../../../src/cli/definalize.ts"],"sourcesContent":["import { info, success } from \"@olinfo/quizms/utils-node\";\nimport { sumBy } from \"lodash-es\";\n\nimport { participationConverter } from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\n\nexport default async function definalize() {\n const { db } = await initializeFirebase();\n\n info(\"Definalizing all participations.\");\n\n const ref = db\n .collection(\"participations\")\n .where(\"finalized\", \"==\", false)\n .withConverter(participationConverter)\n .limit(1000);\n\n let snapshot = await ref.get();\n\n let sum = 0;\n while (!snapshot.empty) {\n const finalized = await Promise.all(\n snapshot.docs.map(async (doc) => {\n const participation = doc.data();\n if (participation.finalized) {\n await db.doc(doc.ref.path).update({ finalized: false });\n return 1;\n }\n return 0;\n }),\n );\n\n sum += sumBy(finalized);\n info(`Definalized ${sum} participations.`);\n\n const last = snapshot.docs.at(-1);\n snapshot = await ref.startAfter(last).get();\n }\n\n success(`${sum} participations were successfully definalized.`);\n}\n"],"names":["definalize","db","initializeFirebase","info","ref","participationConverter","snapshot","sum","finalized","doc","sumBy","last","success"],"mappings":";;;;AAMA,eAA8BA,IAAa;AACzC,QAAM,EAAE,IAAAC,MAAO,MAAMC,EAAA;AAErB,EAAAC,EAAK,kCAAkC;AAEvC,QAAMC,IAAMH,EACT,WAAW,gBAAgB,EAC3B,MAAM,aAAa,MAAM,EAAK,EAC9B,cAAcI,CAAsB,EACpC,MAAM,GAAI;AAEb,MAAIC,IAAW,MAAMF,EAAI,IAAA,GAErBG,IAAM;AACV,SAAO,CAACD,EAAS,SAAO;AACtB,UAAME,IAAY,MAAM,QAAQ;AAAA,MAC9BF,EAAS,KAAK,IAAI,OAAOG,MACDA,EAAI,KAAA,EACR,aAChB,MAAMR,EAAG,IAAIQ,EAAI,IAAI,IAAI,EAAE,OAAO,EAAE,WAAW,IAAO,GAC/C,KAEF,CACR;AAAA,IAAA;AAGH,IAAAF,KAAOG,EAAMF,CAAS,GACtBL,EAAK,eAAeI,CAAG,kBAAkB;AAEzC,UAAMI,IAAOL,EAAS,KAAK,GAAG,EAAE;AAChC,IAAAA,IAAW,MAAMF,EAAI,WAAWO,CAAI,EAAE,IAAA;AAAA,EACxC;AAEA,EAAAC,EAAQ,GAAGL,CAAG,gDAAgD;AAChE;"}
@@ -0,0 +1,54 @@
1
+ import { createWriteStream as p } from "node:fs";
2
+ import { mkdir as m } from "node:fs/promises";
3
+ import s from "node:path";
4
+ import { pipeline as f } from "node:stream/promises";
5
+ import { info as l, success as u } from "@olinfo/quizms/utils-node";
6
+ import { SingleBar as w } from "cli-progress";
7
+ import { formatDistanceStrict as d } from "date-fns";
8
+ import { deleteApp as v } from "firebase-admin/app";
9
+ import { capitalize as C } from "lodash-es";
10
+ import { studentConverter as h, participationConverter as x, variantConverter as b, contestConverter as g } from "./utils/converters-admin.js";
11
+ import { initializeFirebase as S } from "./utils/initialize.js";
12
+ async function F(e) {
13
+ const { app: a, db: o } = await S(), n = (/* @__PURE__ */ new Date()).toISOString(), r = s.join("export", n);
14
+ if (await m(r, { recursive: !0 }), e.students) {
15
+ const t = o.collectionGroup("students").withConverter(h);
16
+ await i(t, "students", r);
17
+ }
18
+ if (e.participations) {
19
+ const t = o.collection("participations").withConverter(x);
20
+ await i(t, "participations", r);
21
+ }
22
+ if (e.variants) {
23
+ const t = o.collection("variants").withConverter(b);
24
+ await i(t, "variants", r);
25
+ }
26
+ if (e.contests) {
27
+ const t = o.collection("contests").withConverter(g);
28
+ await i(t, "contests", r);
29
+ }
30
+ await v(a);
31
+ }
32
+ async function i(e, a, o) {
33
+ l(`Exporting ${a}.`);
34
+ const n = await e.count().get(), r = new w({
35
+ format: " {bar} {percentage}% | ETA: {eta_formatted} | {value}/{total}",
36
+ barCompleteChar: "█",
37
+ barIncompleteChar: "▂",
38
+ etaBuffer: 1e4,
39
+ formatTime: (t) => d(0, t * 1e3)
40
+ });
41
+ r.start(n.data().count, 0), await f(
42
+ e.stream(),
43
+ async function* (t) {
44
+ for await (const c of t)
45
+ r.increment(), yield `${JSON.stringify(c.data())}
46
+ `;
47
+ },
48
+ p(s.format({ dir: o, name: a, ext: ".jsonl" }))
49
+ ), r.stop(), u(`${C(a)} export completed.`);
50
+ }
51
+ export {
52
+ F as default
53
+ };
54
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sources":["../../../src/cli/export.ts"],"sourcesContent":["import { createWriteStream } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport { info, success } from \"@olinfo/quizms/utils-node\";\nimport { SingleBar } from \"cli-progress\";\nimport { formatDistanceStrict } from \"date-fns\";\nimport { deleteApp } from \"firebase-admin/app\";\nimport type { Query, QueryDocumentSnapshot } from \"firebase-admin/firestore\";\nimport { capitalize } from \"lodash-es\";\n\nimport {\n contestConverter,\n participationConverter,\n studentConverter,\n variantConverter,\n} from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\n\ntype ExportOptions = {\n contests?: true;\n participations?: true;\n students?: true;\n variants?: true;\n};\n\nexport default async function exportContests(options: ExportOptions) {\n const { app, db } = await initializeFirebase();\n\n const timestamp = new Date().toISOString();\n const outDir = path.join(\"export\", timestamp);\n await mkdir(outDir, { recursive: true });\n\n if (options.students) {\n const ref = db.collectionGroup(\"students\").withConverter(studentConverter);\n await exportCollection(ref, \"students\", outDir);\n }\n if (options.participations) {\n const ref = db.collection(\"participations\").withConverter(participationConverter);\n await exportCollection(ref, \"participations\", outDir);\n }\n if (options.variants) {\n const ref = db.collection(\"variants\").withConverter(variantConverter);\n await exportCollection(ref, \"variants\", outDir);\n }\n if (options.contests) {\n const ref = db.collection(\"contests\").withConverter(contestConverter);\n await exportCollection(ref, \"contests\", outDir);\n }\n\n await deleteApp(app);\n}\n\nasync function exportCollection(ref: Query, collection: string, dir: string) {\n info(`Exporting ${collection}.`);\n\n const count = await ref.count().get();\n const bar = new SingleBar({\n format: \" {bar} {percentage}% | ETA: {eta_formatted} | {value}/{total}\",\n barCompleteChar: \"\\u2588\",\n barIncompleteChar: \"\\u2582\",\n etaBuffer: 10000,\n formatTime: (t) => formatDistanceStrict(0, t * 1000),\n });\n bar.start(count.data().count, 0);\n\n await pipeline(\n ref.stream(),\n async function* (stream) {\n for await (const data of stream) {\n bar.increment();\n const snapshot = data as unknown as QueryDocumentSnapshot;\n yield `${JSON.stringify(snapshot.data())}\\n`;\n }\n },\n createWriteStream(path.format({ dir, name: collection, ext: \".jsonl\" })),\n );\n\n bar.stop();\n success(`${capitalize(collection)} export completed.`);\n}\n"],"names":["exportContests","options","app","db","initializeFirebase","timestamp","outDir","path","mkdir","ref","studentConverter","exportCollection","participationConverter","variantConverter","contestConverter","deleteApp","collection","dir","info","count","bar","SingleBar","formatDistanceStrict","pipeline","stream","data","createWriteStream","success","capitalize"],"mappings":";;;;;;;;;;;AA2BA,eAA8BA,EAAeC,GAAwB;AACnE,QAAM,EAAE,KAAAC,GAAK,IAAAC,EAAA,IAAO,MAAMC,EAAA,GAEpBC,KAAY,oBAAI,KAAA,GAAO,YAAA,GACvBC,IAASC,EAAK,KAAK,UAAUF,CAAS;AAG5C,MAFA,MAAMG,EAAMF,GAAQ,EAAE,WAAW,IAAM,GAEnCL,EAAQ,UAAU;AACpB,UAAMQ,IAAMN,EAAG,gBAAgB,UAAU,EAAE,cAAcO,CAAgB;AACzE,UAAMC,EAAiBF,GAAK,YAAYH,CAAM;AAAA,EAChD;AACA,MAAIL,EAAQ,gBAAgB;AAC1B,UAAMQ,IAAMN,EAAG,WAAW,gBAAgB,EAAE,cAAcS,CAAsB;AAChF,UAAMD,EAAiBF,GAAK,kBAAkBH,CAAM;AAAA,EACtD;AACA,MAAIL,EAAQ,UAAU;AACpB,UAAMQ,IAAMN,EAAG,WAAW,UAAU,EAAE,cAAcU,CAAgB;AACpE,UAAMF,EAAiBF,GAAK,YAAYH,CAAM;AAAA,EAChD;AACA,MAAIL,EAAQ,UAAU;AACpB,UAAMQ,IAAMN,EAAG,WAAW,UAAU,EAAE,cAAcW,CAAgB;AACpE,UAAMH,EAAiBF,GAAK,YAAYH,CAAM;AAAA,EAChD;AAEA,QAAMS,EAAUb,CAAG;AACrB;AAEA,eAAeS,EAAiBF,GAAYO,GAAoBC,GAAa;AAC3E,EAAAC,EAAK,aAAaF,CAAU,GAAG;AAE/B,QAAMG,IAAQ,MAAMV,EAAI,MAAA,EAAQ,IAAA,GAC1BW,IAAM,IAAIC,EAAU;AAAA,IACxB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,WAAW;AAAA,IACX,YAAY,CAAC,MAAMC,EAAqB,GAAG,IAAI,GAAI;AAAA,EAAA,CACpD;AACD,EAAAF,EAAI,MAAMD,EAAM,KAAA,EAAO,OAAO,CAAC,GAE/B,MAAMI;AAAA,IACJd,EAAI,OAAA;AAAA,IACJ,iBAAiBe,GAAQ;AACvB,uBAAiBC,KAAQD;AACvB,QAAAJ,EAAI,UAAA,GAEJ,MAAM,GAAG,KAAK,UADGK,EACgB,KAAA,CAAM,CAAC;AAAA;AAAA,IAE5C;AAAA,IACAC,EAAkBnB,EAAK,OAAO,EAAE,KAAAU,GAAK,MAAMD,GAAY,KAAK,UAAU,CAAC;AAAA,EAAA,GAGzEI,EAAI,KAAA,GACJO,EAAQ,GAAGC,EAAWZ,CAAU,CAAC,oBAAoB;AACvD;"}
@@ -0,0 +1,10 @@
1
+ const e = [{ collectionGroup: "announcements", queryScope: "COLLECTION", fields: [{ fieldPath: "contestIds", arrayConfig: "CONTAINS" }, { fieldPath: "createdAt", order: "DESCENDING" }] }, { collectionGroup: "students", queryScope: "COLLECTION_GROUP", fields: [{ fieldPath: "contestId", order: "ASCENDING" }, { fieldPath: "disabled", order: "ASCENDING" }] }], t = [{ collectionGroup: "students", fieldPath: "answers", indexes: [] }, { collectionGroup: "students", fieldPath: "updatedAt", indexes: [] }, { collectionGroup: "students", fieldPath: "extraData", indexes: [] }, { collectionGroup: "submissions", fieldPath: "student", indexes: [] }, { collectionGroup: "submissions", fieldPath: "submittedAt", indexes: [] }], s = {
2
+ indexes: e,
3
+ fieldOverrides: t
4
+ };
5
+ export {
6
+ s as default,
7
+ t as fieldOverrides,
8
+ e as indexes
9
+ };
10
+ //# sourceMappingURL=firestore-indexes.json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore-indexes.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,191 @@
1
+ const t = `rules_version = '2';
2
+ service cloud.firestore {
3
+ match /databases/{database}/documents {
4
+ function hasTimestamp(field) {
5
+ return request.resource.data[field] == request.time;
6
+ }
7
+
8
+ function isAuthenticated(id) {
9
+ return request.auth != null && request.auth.uid == id;
10
+ }
11
+
12
+ function affectedKeys() {
13
+ return request.resource.data.diff(resource.data).affectedKeys();
14
+ }
15
+
16
+ function isTeacher() {
17
+ return request.auth != null && request.auth.token.isTeacher == true;
18
+ }
19
+
20
+ function getStudentById(participationId, studentId) {
21
+ return getAfter(/databases/$(database)/documents/participations/$(participationId)/students/$(studentId));
22
+ }
23
+
24
+ function getStudent(uid) {
25
+ let data = get(/databases/$(database)/documents/studentMappingUid/$(uid)).data;
26
+ return get(/databases/$(database)/documents/participations/$(data.participationId)/students/$(data.studentId));
27
+ }
28
+
29
+ function getParticipation(id) {
30
+ return get(/databases/$(database)/documents/participations/$(id));
31
+ }
32
+
33
+ function isTeacherParticipation(participationId) {
34
+ let participation = getParticipation(participationId);
35
+ return participation != null && isAuthenticated(participation.data.teacher);
36
+ }
37
+
38
+ match /announcements/{announcement} {
39
+ allow read: if isTeacher();
40
+ }
41
+
42
+ match /contests/{contest} {
43
+ allow read: if request.auth != null;
44
+ }
45
+
46
+ match /participations/{participationId} {
47
+ function isStudentParticipation() {
48
+ let student = getStudent(request.auth.uid);
49
+ return student != null
50
+ && isAuthenticated(student.data.uid)
51
+ && student.data.participationId == participationId
52
+ && student.data.token == resource.data.token;
53
+ }
54
+
55
+ allow read: if isAuthenticated(resource.data.teacher) || isStudentParticipation();
56
+ allow update: if isAuthenticated(resource.data.teacher) && affectedKeys().hasOnly(['token', 'startingTime', 'endingTime', 'finalized']);
57
+ }
58
+
59
+ match /participationMapping/{token} {
60
+ function checkToken(participationId) {
61
+ let participation = getAfter(/databases/$(database)/documents/participations/$(participationId));
62
+ return participation != null
63
+ && participation.data.token == token
64
+ && participation.data.startingTime == request.resource.data.startingTime
65
+ && participation.data.endingTime == request.resource.data.endingTime
66
+ && participation.data.contestId == request.resource.data.contestId;
67
+ }
68
+
69
+ allow get: if request.auth != null;
70
+ allow create: if isTeacherParticipation(request.resource.data.participationId) && checkToken(request.resource.data.participationId);
71
+ }
72
+
73
+ function studentHash(student) {
74
+ let data = student.userData;
75
+ let joined = [data.name, data.surname, data.classYear, data.classSection, student.token].join('$').lower();
76
+ return hashing.sha256(joined).toHexString()[0: 24];
77
+ }
78
+
79
+ match /participations/{participationId}/students/{student} {
80
+ function checkToken() {
81
+ let participation = getParticipation(participationId);
82
+ return participation != null
83
+ && participation.data.token != null
84
+ && participation.data.token == request.resource.data.token
85
+ && participation.data.startingTime == request.resource.data.startedAt
86
+ && participation.data.endingTime >= request.resource.data.finishedAt
87
+ && participation.data.contestId == request.resource.data.contestId
88
+ && request.time <= resource.data.finishedAt + duration.value(10, 's');
89
+ }
90
+
91
+ function checkVariant(data) {
92
+ let hash = data.contestId + '-' + studentHash(data)[0: 3];
93
+ let mapping = get(/databases/$(database)/documents/variantMappings/$(hash));
94
+ return mapping != null
95
+ && mapping.data.variant == data.variant;
96
+ }
97
+
98
+ function canStudentCreate() {
99
+ return isAuthenticated(request.resource.data.uid)
100
+ && request.resource.data.userData != null
101
+ && request.resource.data.contestId != null
102
+ && checkToken()
103
+ && checkVariant(request.resource.data);
104
+ }
105
+
106
+ function canStudentUpdate() {
107
+ let keys = affectedKeys();
108
+ return isAuthenticated(resource.data.uid)
109
+ && checkToken()
110
+ && keys.hasOnly(['answers', 'updatedAt', 'finishedAt', 'extraData'])
111
+ && hasTimestamp('updatedAt');
112
+ }
113
+
114
+ function canTeacherUpdate() {
115
+ let keys = affectedKeys();
116
+ return isTeacherParticipation(participationId)
117
+ && !keys.hasAny(['participationId', 'createdAt'])
118
+ && hasTimestamp('updatedAt');
119
+ }
120
+
121
+ allow read: if isAuthenticated(resource.data.uid) || isTeacherParticipation(participationId);
122
+ allow create: if (canStudentCreate() || isTeacherParticipation(participationId)) && request.resource.data.participationId == participationId /* TODO && hasTimestamp('createdAt') */;
123
+ allow update: if canStudentUpdate() || canTeacherUpdate();
124
+ allow delete: if isTeacherParticipation(participationId);
125
+ }
126
+
127
+ match /participations/{participationId}/studentMappingHash/{id} {
128
+ function canCreateStudent() {
129
+ let student = getStudentById(participationId, request.resource.data.studentId);
130
+ return student != null
131
+ && isAuthenticated(student.data.uid)
132
+ && student.data.userData != null
133
+ && studentHash(student.data) == id;
134
+ }
135
+
136
+ allow get: if request.auth != null;
137
+ allow create, update: if canCreateStudent() || isTeacherParticipation(participationId);
138
+ allow delete: if isTeacherParticipation(participationId);
139
+ }
140
+
141
+ match /participations/{participationId}/studentRestore/{uid} {
142
+ function canStudentCreate() {
143
+ let participation = getParticipation(participationId);
144
+ return isAuthenticated(uid)
145
+ && participationId == request.resource.data.participationId
146
+ && participation != null
147
+ && participation.data.token == request.resource.data.token;
148
+ }
149
+
150
+ allow read: if isTeacherParticipation(participationId);
151
+ allow create, update: if canStudentCreate();
152
+ allow delete: if isTeacherParticipation(participationId);
153
+ }
154
+
155
+ match /studentMappingUid/{id} {
156
+ function canCreateStudent() {
157
+ let student = getStudentById(request.resource.data.participationId, request.resource.data.studentId);
158
+ return student != null
159
+ && isAuthenticated(student.data.uid)
160
+ && isAuthenticated(id);
161
+ }
162
+
163
+ allow get: if request.auth != null;
164
+ allow list: if isTeacherParticipation(resource.data.participationId);
165
+ allow create, update: if canCreateStudent() || isTeacherParticipation(request.resource.data.participationId);
166
+ allow delete: if isTeacherParticipation(resource.data.participationId);
167
+ }
168
+
169
+ match /submissions/{submission} {
170
+ allow get: if isAuthenticated(resource.data.uid);
171
+ allow create: if isAuthenticated(request.resource.data.uid) && hasTimestamp('submittedAt');
172
+ }
173
+
174
+ match /variants/{variant} {
175
+ allow read: if isTeacher();
176
+ }
177
+
178
+ match /variantMappings/{hash} {
179
+ allow read: if request.auth != null;
180
+ }
181
+
182
+ match /{document=**} {
183
+ allow read, write: if request.auth.token.isAdmin == true;
184
+ }
185
+ }
186
+ }
187
+ `;
188
+ export {
189
+ t as default
190
+ };
191
+ //# sourceMappingURL=firestore.rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.rules.js","sources":["../../../../src/cli/files/firestore.rules?raw"],"sourcesContent":["export default \"rules_version = '2';\\nservice cloud.firestore {\\n match /databases/{database}/documents {\\n function hasTimestamp(field) {\\n return request.resource.data[field] == request.time;\\n }\\n\\n function isAuthenticated(id) {\\n return request.auth != null && request.auth.uid == id;\\n }\\n\\n function affectedKeys() {\\n return request.resource.data.diff(resource.data).affectedKeys();\\n }\\n\\n function isTeacher() {\\n return request.auth != null && request.auth.token.isTeacher == true;\\n }\\n\\n function getStudentById(participationId, studentId) {\\n return getAfter(/databases/$(database)/documents/participations/$(participationId)/students/$(studentId));\\n }\\n\\n function getStudent(uid) {\\n let data = get(/databases/$(database)/documents/studentMappingUid/$(uid)).data;\\n return get(/databases/$(database)/documents/participations/$(data.participationId)/students/$(data.studentId));\\n }\\n\\n function getParticipation(id) {\\n return get(/databases/$(database)/documents/participations/$(id));\\n }\\n\\n function isTeacherParticipation(participationId) {\\n let participation = getParticipation(participationId);\\n return participation != null && isAuthenticated(participation.data.teacher);\\n }\\n\\n match /announcements/{announcement} {\\n allow read: if isTeacher();\\n }\\n\\n match /contests/{contest} {\\n allow read: if request.auth != null;\\n }\\n\\n match /participations/{participationId} {\\n function isStudentParticipation() {\\n let student = getStudent(request.auth.uid);\\n return student != null\\n && isAuthenticated(student.data.uid)\\n && student.data.participationId == participationId\\n && student.data.token == resource.data.token;\\n }\\n\\n allow read: if isAuthenticated(resource.data.teacher) || isStudentParticipation();\\n allow update: if isAuthenticated(resource.data.teacher) && affectedKeys().hasOnly(['token', 'startingTime', 'endingTime', 'finalized']);\\n }\\n\\n match /participationMapping/{token} {\\n function checkToken(participationId) {\\n let participation = getAfter(/databases/$(database)/documents/participations/$(participationId));\\n return participation != null\\n && participation.data.token == token\\n && participation.data.startingTime == request.resource.data.startingTime\\n && participation.data.endingTime == request.resource.data.endingTime\\n && participation.data.contestId == request.resource.data.contestId;\\n }\\n\\n allow get: if request.auth != null;\\n allow create: if isTeacherParticipation(request.resource.data.participationId) && checkToken(request.resource.data.participationId);\\n }\\n\\n function studentHash(student) {\\n let data = student.userData;\\n let joined = [data.name, data.surname, data.classYear, data.classSection, student.token].join('$').lower();\\n return hashing.sha256(joined).toHexString()[0: 24];\\n }\\n\\n match /participations/{participationId}/students/{student} {\\n function checkToken() {\\n let participation = getParticipation(participationId);\\n return participation != null\\n && participation.data.token != null\\n && participation.data.token == request.resource.data.token\\n && participation.data.startingTime == request.resource.data.startedAt\\n && participation.data.endingTime >= request.resource.data.finishedAt\\n && participation.data.contestId == request.resource.data.contestId\\n && request.time <= resource.data.finishedAt + duration.value(10, 's');\\n }\\n\\n function checkVariant(data) {\\n let hash = data.contestId + '-' + studentHash(data)[0: 3];\\n let mapping = get(/databases/$(database)/documents/variantMappings/$(hash));\\n return mapping != null\\n && mapping.data.variant == data.variant;\\n }\\n\\n function canStudentCreate() {\\n return isAuthenticated(request.resource.data.uid)\\n && request.resource.data.userData != null\\n && request.resource.data.contestId != null\\n && checkToken()\\n && checkVariant(request.resource.data);\\n }\\n\\n function canStudentUpdate() {\\n let keys = affectedKeys();\\n return isAuthenticated(resource.data.uid)\\n && checkToken()\\n && keys.hasOnly(['answers', 'updatedAt', 'finishedAt', 'extraData'])\\n && hasTimestamp('updatedAt');\\n }\\n\\n function canTeacherUpdate() {\\n let keys = affectedKeys();\\n return isTeacherParticipation(participationId)\\n && !keys.hasAny(['participationId', 'createdAt'])\\n && hasTimestamp('updatedAt');\\n }\\n\\n allow read: if isAuthenticated(resource.data.uid) || isTeacherParticipation(participationId);\\n allow create: if (canStudentCreate() || isTeacherParticipation(participationId)) && request.resource.data.participationId == participationId /* TODO && hasTimestamp('createdAt') */;\\n allow update: if canStudentUpdate() || canTeacherUpdate();\\n allow delete: if isTeacherParticipation(participationId);\\n }\\n\\n match /participations/{participationId}/studentMappingHash/{id} {\\n function canCreateStudent() {\\n let student = getStudentById(participationId, request.resource.data.studentId);\\n return student != null\\n && isAuthenticated(student.data.uid)\\n && student.data.userData != null\\n && studentHash(student.data) == id;\\n }\\n\\n allow get: if request.auth != null;\\n allow create, update: if canCreateStudent() || isTeacherParticipation(participationId);\\n allow delete: if isTeacherParticipation(participationId);\\n }\\n\\n match /participations/{participationId}/studentRestore/{uid} {\\n function canStudentCreate() {\\n let participation = getParticipation(participationId);\\n return isAuthenticated(uid)\\n && participationId == request.resource.data.participationId\\n && participation != null\\n && participation.data.token == request.resource.data.token;\\n }\\n\\n allow read: if isTeacherParticipation(participationId);\\n allow create, update: if canStudentCreate();\\n allow delete: if isTeacherParticipation(participationId);\\n }\\n\\n match /studentMappingUid/{id} {\\n function canCreateStudent() {\\n let student = getStudentById(request.resource.data.participationId, request.resource.data.studentId);\\n return student != null\\n && isAuthenticated(student.data.uid)\\n && isAuthenticated(id);\\n }\\n\\n allow get: if request.auth != null;\\n allow list: if isTeacherParticipation(resource.data.participationId);\\n allow create, update: if canCreateStudent() || isTeacherParticipation(request.resource.data.participationId);\\n allow delete: if isTeacherParticipation(resource.data.participationId);\\n }\\n\\n match /submissions/{submission} {\\n allow get: if isAuthenticated(resource.data.uid);\\n allow create: if isAuthenticated(request.resource.data.uid) && hasTimestamp('submittedAt');\\n }\\n\\n match /variants/{variant} {\\n allow read: if isTeacher();\\n }\\n\\n match /variantMappings/{hash} {\\n allow read: if request.auth != null;\\n }\\n\\n match /{document=**} {\\n allow read, write: if request.auth.token.isAdmin == true;\\n }\\n }\\n}\\n\""],"names":["firestoreRules"],"mappings":"AAAA,MAAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;"}
@@ -0,0 +1,35 @@
1
+ const t = `rules_version = '2';
2
+ service firebase.storage {
3
+ match /b/{bucket}/o {
4
+ function isAuthenticated(id) {
5
+ return request.auth != null && request.auth.uid == id;
6
+ }
7
+
8
+ function isTeacher() {
9
+ return request.auth != null && request.auth.token.isTeacher == true;
10
+ }
11
+
12
+ function getStudent(uid) {
13
+ let data = firestore.get(/databases/(default)/documents/studentMappingUid/$(uid)).data;
14
+ return firestore.get(/databases/(default)/documents/participations/$(data.participationId)/students/$(data.studentId));
15
+ }
16
+
17
+ match /statements/{contestId}/{variantId}/{filename} {
18
+ function canStudentReadVariant() {
19
+ let student = getStudent(request.auth.uid);
20
+ return student != null
21
+ && isAuthenticated(student.data.uid)
22
+ && student.data.contestId == contestId
23
+ && student.data.variant == variantId
24
+ && student.data.startedAt <= request.time;
25
+ }
26
+
27
+ allow read: if canStudentReadVariant() || isTeacher();
28
+ }
29
+ }
30
+ }
31
+ `;
32
+ export {
33
+ t as default
34
+ };
35
+ //# sourceMappingURL=storage.rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.rules.js","sources":["../../../../src/cli/files/storage.rules?raw"],"sourcesContent":["export default \"rules_version = '2';\\nservice firebase.storage {\\n match /b/{bucket}/o {\\n function isAuthenticated(id) {\\n return request.auth != null && request.auth.uid == id;\\n }\\n\\n function isTeacher() {\\n return request.auth != null && request.auth.token.isTeacher == true;\\n }\\n\\n function getStudent(uid) {\\n let data = firestore.get(/databases/(default)/documents/studentMappingUid/$(uid)).data;\\n return firestore.get(/databases/(default)/documents/participations/$(data.participationId)/students/$(data.studentId));\\n }\\n\\n match /statements/{contestId}/{variantId}/{filename} {\\n function canStudentReadVariant() {\\n let student = getStudent(request.auth.uid);\\n return student != null\\n && isAuthenticated(student.data.uid)\\n && student.data.contestId == contestId\\n && student.data.variant == variantId\\n && student.data.startedAt <= request.time;\\n }\\n\\n allow read: if canStudentReadVariant() || isTeacher();\\n }\\n }\\n}\\n\""],"names":["storageRules"],"mappings":"AAAA,MAAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;"}