@olinfo/quizms-firebase 4.1.3 → 5.0.0

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 (55) hide show
  1. package/dist/cli.js +14 -14
  2. package/dist/cli.js.map +1 -1
  3. package/dist/package.json.js +1 -1
  4. package/dist/src/cli/definalize.js +17 -17
  5. package/dist/src/cli/definalize.js.map +1 -1
  6. package/dist/src/cli/export.js +42 -34
  7. package/dist/src/cli/export.js.map +1 -1
  8. package/dist/src/cli/files/firestore-indexes.json.js +1 -1
  9. package/dist/src/cli/files/firestore.rules.js +27 -27
  10. package/dist/src/cli/files/firestore.rules.js.map +1 -1
  11. package/dist/src/cli/files/functions-index.js +3 -3
  12. package/dist/src/cli/files/functions-index.js.map +1 -1
  13. package/dist/src/cli/files/storage.rules.js +3 -3
  14. package/dist/src/cli/files/storage.rules.js.map +1 -1
  15. package/dist/src/cli/import.js +65 -71
  16. package/dist/src/cli/import.js.map +1 -1
  17. package/dist/src/cli/update-scores.js +7 -7
  18. package/dist/src/cli/update-scores.js.map +1 -1
  19. package/dist/src/cli/utils/converters-admin.js +23 -22
  20. package/dist/src/cli/utils/converters-admin.js.map +1 -1
  21. package/dist/src/functions/api/index.js +18 -18
  22. package/dist/src/functions/api/index.js.map +1 -1
  23. package/dist/src/functions/api/queries.js +21 -21
  24. package/dist/src/functions/api/queries.js.map +1 -1
  25. package/dist/src/functions/api/student.js +46 -47
  26. package/dist/src/functions/api/student.js.map +1 -1
  27. package/dist/src/functions/api/teacher.js +92 -95
  28. package/dist/src/functions/api/teacher.js.map +1 -1
  29. package/dist/src/functions/submissions.js +11 -7
  30. package/dist/src/functions/submissions.js.map +1 -1
  31. package/dist/src/functions/update-scores.js +14 -14
  32. package/dist/src/functions/update-scores.js.map +1 -1
  33. package/dist/src/models/submission.js +13 -0
  34. package/dist/src/models/submission.js.map +1 -0
  35. package/dist/src/web/admin/announcements.js +45 -42
  36. package/dist/src/web/admin/announcements.js.map +1 -1
  37. package/dist/src/web/admin/dashboard.js +43 -43
  38. package/dist/src/web/admin/dashboard.js.map +1 -1
  39. package/dist/src/web/admin/export.js +56 -52
  40. package/dist/src/web/admin/export.js.map +1 -1
  41. package/dist/src/web/admin/provider.js +28 -28
  42. package/dist/src/web/admin/provider.js.map +1 -1
  43. package/dist/src/web/admin/school-table.js +20 -24
  44. package/dist/src/web/admin/school-table.js.map +1 -1
  45. package/dist/src/web/common/api.js +25 -25
  46. package/dist/src/web/common/api.js.map +1 -1
  47. package/dist/src/web/common/converters.js +38 -39
  48. package/dist/src/web/common/converters.js.map +1 -1
  49. package/dist/src/web/entry/student-statement.js +1 -1
  50. package/dist/src/web/entry/student-statement.js.map +1 -1
  51. package/dist/src/web/entry/student.js +80 -75
  52. package/dist/src/web/entry/student.js.map +1 -1
  53. package/dist/src/web/entry/teacher.js +63 -62
  54. package/dist/src/web/entry/teacher.js.map +1 -1
  55. package/package.json +13 -13
@@ -1,75 +1,74 @@
1
- import D, { hash as I } from "node:crypto";
2
- import { studentSchema as p, getNormalizedUserData as w } from "@olinfo/quizms/models";
3
- import { TRPCError as i } from "@trpc/server";
4
- import { Timestamp as r } from "firebase-admin/firestore";
5
- import * as h from "firebase-functions/logger";
6
- import s from "zod";
7
- import { db as u, auth as m } from "../common.js";
8
- import { getParticipationByToken as l, getContest as k, getStudentByHash as v, getRandomVariant as T } from "./queries.js";
9
- import { publicProcedure as C } from "./trpc.js";
10
- function R(o, t) {
11
- return I("sha256", w(o, { userData: t }));
1
+ import D, { hash as f } from "node:crypto";
2
+ import { studentSchema as c, getNormalizedUserData as v } from "@olinfo/quizms/models";
3
+ import { TRPCError as r } from "@trpc/server";
4
+ import { Timestamp as s } from "firebase-admin/firestore";
5
+ import * as g from "firebase-functions/logger";
6
+ import d from "zod";
7
+ import { db as m, auth as p } from "../common.js";
8
+ import { getVenueByToken as h, getContest as l, getStudentByHash as k, getRandomVariant as C } from "./queries.js";
9
+ import { publicProcedure as R } from "./trpc.js";
10
+ function T(o, t) {
11
+ return f("sha256", v(o, { userData: t }));
12
12
  }
13
- const U = C.input(
14
- s.strictObject({
15
- contestId: s.string(),
16
- token: s.string(),
17
- userData: p.shape.userData.unwrap().required(),
18
- extraData: p.shape.extraData.unwrap()
13
+ const U = R.input(
14
+ d.strictObject({
15
+ contestId: d.string(),
16
+ token: d.string(),
17
+ userData: c.shape.userData.unwrap().required(),
18
+ extraData: c.shape.extraData.unwrap()
19
19
  })
20
20
  ).mutation(async (o) => {
21
21
  const { input: t } = o;
22
- h.info("Student login request received", { data: t });
23
- const a = await l(t.token);
24
- if (!a)
25
- throw new i({ code: "BAD_REQUEST", message: "Codice prova non valido" });
26
- if (a.contestId !== t.contestId)
27
- throw new i({ code: "FORBIDDEN", message: "Codice prova non valido" });
28
- const f = await k(a.contestId), n = D.randomUUID(), d = R(f, t.userData), e = await v(a.id, d);
29
- if (e) {
30
- if (e.token !== t.token)
31
- throw new i({
22
+ g.info("Student login request received", { data: t });
23
+ const e = await h(t.token);
24
+ if (!e)
25
+ throw new r({ code: "BAD_REQUEST", message: "Codice prova non valido" });
26
+ if (e.contestId !== t.contestId)
27
+ throw new r({ code: "FORBIDDEN", message: "Codice prova non valido" });
28
+ const I = await l(e.contestId), n = D.randomUUID(), i = T(I, t.userData), a = await k(e.id, i);
29
+ if (a) {
30
+ if (a.token !== t.token)
31
+ throw new r({
32
32
  code: "FORBIDDEN",
33
33
  message: "Hai già partecipato a questa gara"
34
34
  });
35
- return await u.doc(`studentRestores/${n}`).set({
36
- studentId: e.id,
37
- participationId: e.participationId,
35
+ return await m.doc(`studentRestores/${n}`).set({
36
+ studentId: a.id,
37
+ venueId: a.venueId,
38
38
  token: t.token,
39
39
  name: t.userData.name,
40
40
  surname: t.userData.surname,
41
41
  approvalCode: Math.round(Math.random() * 1e3),
42
42
  status: "pending",
43
- createdAt: r.now()
44
- }), m.createCustomToken(n, {
43
+ createdAt: s.now()
44
+ }), p.createCustomToken(n, {
45
45
  role: "student",
46
- studentId: e.id,
47
- participationId: e.participationId,
48
- variant: e.variant
46
+ studentId: a.id,
47
+ venueId: a.venueId,
48
+ variantId: a.variantId
49
49
  });
50
50
  }
51
- const c = await T(t.contestId), g = await u.collection(`participations/${a.id}/students`).add({
51
+ const u = await C(t.contestId), w = await m.collection(`venues/${e.id}/students`).add({
52
52
  uid: n,
53
53
  userData: t.userData,
54
- userDataHash: d,
54
+ userDataHash: i,
55
55
  absent: !1,
56
56
  disabled: !1,
57
- participationId: a.id,
57
+ venueId: e.id,
58
58
  contestId: t.contestId,
59
59
  token: t.token,
60
- startedAt: a.startingTime,
61
- finishedAt: a.endingTime,
62
- variant: c.id,
60
+ participationWindow: e.participationWindow,
61
+ variant: u.id,
63
62
  answers: {},
64
63
  extraData: t.extraData,
65
- createdAt: r.now(),
66
- updatedAt: r.now()
64
+ createdAt: s.now(),
65
+ updatedAt: s.now()
67
66
  });
68
- return m.createCustomToken(n, {
67
+ return p.createCustomToken(n, {
69
68
  role: "student",
70
- studentId: g.id,
71
- participationId: a.id,
72
- variant: c
69
+ studentId: w.id,
70
+ venueId: e.id,
71
+ variant: u
73
72
  });
74
73
  });
75
74
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"student.js","sources":["../../../../src/functions/api/student.ts"],"sourcesContent":["import crypto, { hash } from \"node:crypto\";\n\nimport {\n type Contest,\n getNormalizedUserData,\n type Student,\n studentSchema,\n} from \"@olinfo/quizms/models\";\nimport { TRPCError } from \"@trpc/server\";\nimport { Timestamp } from \"firebase-admin/firestore\";\nimport * as logger from \"firebase-functions/logger\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport { getContest, getParticipationByToken, getRandomVariant, getStudentByHash } from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nfunction studentHash(contest: Contest, userData: NonNullable<Student[\"userData\"]>) {\n return hash(\"sha256\", getNormalizedUserData(contest, { userData }));\n}\n\nexport const studentLogin = publicProcedure\n .input(\n z.strictObject({\n contestId: z.string(),\n token: z.string(),\n userData: studentSchema.shape.userData.unwrap().required(),\n extraData: studentSchema.shape.extraData.unwrap(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Student login request received\", { data });\n\n const participation = await getParticipationByToken(data.token);\n if (!participation) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Codice prova non valido\" });\n }\n if (participation.contestId !== data.contestId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Codice prova non valido\" });\n }\n\n const contest = await getContest(participation.contestId);\n\n const uid = crypto.randomUUID();\n const userDataHash = studentHash(contest, data.userData);\n\n const duplicated = await getStudentByHash(participation.id, userDataHash);\n if (duplicated) {\n if (duplicated.token !== data.token) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Hai già partecipato a questa gara\",\n });\n }\n\n await db.doc(`studentRestores/${uid}`).set({\n studentId: duplicated.id,\n participationId: duplicated.participationId,\n token: data.token,\n name: data.userData.name,\n surname: data.userData.surname,\n approvalCode: Math.round(Math.random() * 1000),\n status: \"pending\",\n createdAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: duplicated.id,\n participationId: duplicated.participationId,\n variant: duplicated.variant,\n });\n }\n\n const variant = await getRandomVariant(data.contestId);\n\n const studentRef = await db.collection(`participations/${participation.id}/students`).add({\n uid,\n userData: data.userData,\n userDataHash,\n absent: false,\n disabled: false,\n participationId: participation.id,\n contestId: data.contestId,\n token: data.token,\n startedAt: participation.startingTime,\n finishedAt: participation.endingTime,\n variant: variant.id,\n answers: {},\n extraData: data.extraData,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: studentRef.id,\n participationId: participation.id,\n variant,\n });\n });\n"],"names":["studentHash","contest","userData","hash","getNormalizedUserData","studentLogin","publicProcedure","z","studentSchema","opts","data","logger","participation","getParticipationByToken","TRPCError","getContest","uid","crypto","userDataHash","duplicated","getStudentByHash","db","Timestamp","auth","variant","getRandomVariant","studentRef"],"mappings":";;;;;;;;;AAiBA,SAASA,EAAYC,GAAkBC,GAA4C;AACjF,SAAOC,EAAK,UAAUC,EAAsBH,GAAS,EAAE,UAAAC,EAAA,CAAU,CAAC;AACpE;AAEO,MAAMG,IAAeC,EACzB;AAAA,EACCC,EAAE,aAAa;AAAA,IACb,WAAWA,EAAE,OAAA;AAAA,IACb,OAAOA,EAAE,OAAA;AAAA,IACT,UAAUC,EAAc,MAAM,SAAS,OAAA,EAAS,SAAA;AAAA,IAChD,WAAWA,EAAc,MAAM,UAAU,OAAA;AAAA,EAAO,CACjD;AACH,EACC,SAAS,OAAOC,MAAS;AACxB,QAAM,EAAE,OAAOC,EAAA,IAASD;AACxB,EAAAE,EAAO,KAAK,kCAAkC,EAAE,MAAAD,EAAA,CAAM;AAEtD,QAAME,IAAgB,MAAMC,EAAwBH,EAAK,KAAK;AAC9D,MAAI,CAACE;AACH,UAAM,IAAIE,EAAU,EAAE,MAAM,eAAe,SAAS,2BAA2B;AAEjF,MAAIF,EAAc,cAAcF,EAAK;AACnC,UAAM,IAAII,EAAU,EAAE,MAAM,aAAa,SAAS,2BAA2B;AAG/E,QAAMb,IAAU,MAAMc,EAAWH,EAAc,SAAS,GAElDI,IAAMC,EAAO,WAAA,GACbC,IAAelB,EAAYC,GAASS,EAAK,QAAQ,GAEjDS,IAAa,MAAMC,EAAiBR,EAAc,IAAIM,CAAY;AACxE,MAAIC,GAAY;AACd,QAAIA,EAAW,UAAUT,EAAK;AAC5B,YAAM,IAAII,EAAU;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAGH,iBAAMO,EAAG,IAAI,mBAAmBL,CAAG,EAAE,EAAE,IAAI;AAAA,MACzC,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,OAAOT,EAAK;AAAA,MACZ,MAAMA,EAAK,SAAS;AAAA,MACpB,SAASA,EAAK,SAAS;AAAA,MACvB,cAAc,KAAK,MAAM,KAAK,OAAA,IAAW,GAAI;AAAA,MAC7C,QAAQ;AAAA,MACR,WAAWY,EAAU,IAAA;AAAA,IAAI,CAC1B,GAEMC,EAAK,kBAAkBP,GAAK;AAAA,MACjC,MAAM;AAAA,MACN,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,SAASA,EAAW;AAAA,IAAA,CACrB;AAAA,EACH;AAEA,QAAMK,IAAU,MAAMC,EAAiBf,EAAK,SAAS,GAE/CgB,IAAa,MAAML,EAAG,WAAW,kBAAkBT,EAAc,EAAE,WAAW,EAAE,IAAI;AAAA,IACxF,KAAAI;AAAA,IACA,UAAUN,EAAK;AAAA,IACf,cAAAQ;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiBN,EAAc;AAAA,IAC/B,WAAWF,EAAK;AAAA,IAChB,OAAOA,EAAK;AAAA,IACZ,WAAWE,EAAc;AAAA,IACzB,YAAYA,EAAc;AAAA,IAC1B,SAASY,EAAQ;AAAA,IACjB,SAAS,CAAA;AAAA,IACT,WAAWd,EAAK;AAAA,IAChB,WAAWY,EAAU,IAAA;AAAA,IACrB,WAAWA,EAAU,IAAA;AAAA,EAAI,CAC1B;AAED,SAAOC,EAAK,kBAAkBP,GAAK;AAAA,IACjC,MAAM;AAAA,IACN,WAAWU,EAAW;AAAA,IACtB,iBAAiBd,EAAc;AAAA,IAC/B,SAAAY;AAAA,EAAA,CACD;AACH,CAAC;"}
1
+ {"version":3,"file":"student.js","sources":["../../../../src/functions/api/student.ts"],"sourcesContent":["import crypto, { hash } from \"node:crypto\";\n\nimport {\n type Contest,\n getNormalizedUserData,\n type Student,\n studentSchema,\n} from \"@olinfo/quizms/models\";\nimport { TRPCError } from \"@trpc/server\";\nimport { Timestamp } from \"firebase-admin/firestore\";\nimport * as logger from \"firebase-functions/logger\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport { getContest, getRandomVariant, getStudentByHash, getVenueByToken } from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nfunction studentHash(contest: Contest, userData: NonNullable<Student[\"userData\"]>) {\n return hash(\"sha256\", getNormalizedUserData(contest, { userData }));\n}\n\nexport const studentLogin = publicProcedure\n .input(\n z.strictObject({\n contestId: z.string(),\n token: z.string(),\n userData: studentSchema.shape.userData.unwrap().required(),\n extraData: studentSchema.shape.extraData.unwrap(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Student login request received\", { data });\n\n const venue = await getVenueByToken(data.token);\n if (!venue) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Codice prova non valido\" });\n }\n if (venue.contestId !== data.contestId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Codice prova non valido\" });\n }\n\n const contest = await getContest(venue.contestId);\n\n const uid = crypto.randomUUID();\n const userDataHash = studentHash(contest, data.userData);\n\n const duplicated = await getStudentByHash(venue.id, userDataHash);\n if (duplicated) {\n if (duplicated.token !== data.token) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Hai già partecipato a questa gara\",\n });\n }\n\n await db.doc(`studentRestores/${uid}`).set({\n studentId: duplicated.id,\n venueId: duplicated.venueId,\n token: data.token,\n name: data.userData.name,\n surname: data.userData.surname,\n approvalCode: Math.round(Math.random() * 1000),\n status: \"pending\",\n createdAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: duplicated.id,\n venueId: duplicated.venueId,\n variantId: duplicated.variantId,\n });\n }\n\n const variant = await getRandomVariant(data.contestId);\n\n const studentRef = await db.collection(`venues/${venue.id}/students`).add({\n uid,\n userData: data.userData,\n userDataHash,\n absent: false,\n disabled: false,\n venueId: venue.id,\n contestId: data.contestId,\n token: data.token,\n participationWindow: venue.participationWindow,\n variant: variant.id,\n answers: {},\n extraData: data.extraData,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: studentRef.id,\n venueId: venue.id,\n variant,\n });\n });\n"],"names":["studentHash","contest","userData","hash","getNormalizedUserData","studentLogin","publicProcedure","z","studentSchema","opts","data","logger","venue","getVenueByToken","TRPCError","getContest","uid","crypto","userDataHash","duplicated","getStudentByHash","db","Timestamp","auth","variant","getRandomVariant","studentRef"],"mappings":";;;;;;;;;AAiBA,SAASA,EAAYC,GAAkBC,GAA4C;AACjF,SAAOC,EAAK,UAAUC,EAAsBH,GAAS,EAAE,UAAAC,EAAA,CAAU,CAAC;AACpE;AAEO,MAAMG,IAAeC,EACzB;AAAA,EACCC,EAAE,aAAa;AAAA,IACb,WAAWA,EAAE,OAAA;AAAA,IACb,OAAOA,EAAE,OAAA;AAAA,IACT,UAAUC,EAAc,MAAM,SAAS,OAAA,EAAS,SAAA;AAAA,IAChD,WAAWA,EAAc,MAAM,UAAU,OAAA;AAAA,EAAO,CACjD;AACH,EACC,SAAS,OAAOC,MAAS;AACxB,QAAM,EAAE,OAAOC,EAAA,IAASD;AACxB,EAAAE,EAAO,KAAK,kCAAkC,EAAE,MAAAD,EAAA,CAAM;AAEtD,QAAME,IAAQ,MAAMC,EAAgBH,EAAK,KAAK;AAC9C,MAAI,CAACE;AACH,UAAM,IAAIE,EAAU,EAAE,MAAM,eAAe,SAAS,2BAA2B;AAEjF,MAAIF,EAAM,cAAcF,EAAK;AAC3B,UAAM,IAAII,EAAU,EAAE,MAAM,aAAa,SAAS,2BAA2B;AAG/E,QAAMb,IAAU,MAAMc,EAAWH,EAAM,SAAS,GAE1CI,IAAMC,EAAO,WAAA,GACbC,IAAelB,EAAYC,GAASS,EAAK,QAAQ,GAEjDS,IAAa,MAAMC,EAAiBR,EAAM,IAAIM,CAAY;AAChE,MAAIC,GAAY;AACd,QAAIA,EAAW,UAAUT,EAAK;AAC5B,YAAM,IAAII,EAAU;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAGH,iBAAMO,EAAG,IAAI,mBAAmBL,CAAG,EAAE,EAAE,IAAI;AAAA,MACzC,WAAWG,EAAW;AAAA,MACtB,SAASA,EAAW;AAAA,MACpB,OAAOT,EAAK;AAAA,MACZ,MAAMA,EAAK,SAAS;AAAA,MACpB,SAASA,EAAK,SAAS;AAAA,MACvB,cAAc,KAAK,MAAM,KAAK,OAAA,IAAW,GAAI;AAAA,MAC7C,QAAQ;AAAA,MACR,WAAWY,EAAU,IAAA;AAAA,IAAI,CAC1B,GAEMC,EAAK,kBAAkBP,GAAK;AAAA,MACjC,MAAM;AAAA,MACN,WAAWG,EAAW;AAAA,MACtB,SAASA,EAAW;AAAA,MACpB,WAAWA,EAAW;AAAA,IAAA,CACvB;AAAA,EACH;AAEA,QAAMK,IAAU,MAAMC,EAAiBf,EAAK,SAAS,GAE/CgB,IAAa,MAAML,EAAG,WAAW,UAAUT,EAAM,EAAE,WAAW,EAAE,IAAI;AAAA,IACxE,KAAAI;AAAA,IACA,UAAUN,EAAK;AAAA,IACf,cAAAQ;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAASN,EAAM;AAAA,IACf,WAAWF,EAAK;AAAA,IAChB,OAAOA,EAAK;AAAA,IACZ,qBAAqBE,EAAM;AAAA,IAC3B,SAASY,EAAQ;AAAA,IACjB,SAAS,CAAA;AAAA,IACT,WAAWd,EAAK;AAAA,IAChB,WAAWY,EAAU,IAAA;AAAA,IACrB,WAAWA,EAAU,IAAA;AAAA,EAAI,CAC1B;AAED,SAAOC,EAAK,kBAAkBP,GAAK;AAAA,IACjC,MAAM;AAAA,IACN,WAAWU,EAAW;AAAA,IACtB,SAASd,EAAM;AAAA,IACf,SAAAY;AAAA,EAAA,CACD;AACH,CAAC;"}
@@ -1,129 +1,126 @@
1
1
  import D from "node:crypto";
2
- import { randomToken as E } from "@olinfo/quizms/utils";
3
- import { TRPCError as o } from "@trpc/server";
4
- import { isFuture as g, roundToNearestMinutes as O, addSeconds as l, addMinutes as R, isPast as h } from "date-fns";
5
- import { getFunctions as B } from "firebase-admin/functions";
2
+ import { randomToken as T } from "@olinfo/quizms/utils";
3
+ import { TRPCError as a } from "@trpc/server";
4
+ import { isFuture as g, roundToNearestMinutes as S, addSeconds as l, addMinutes as E, isPast as h } from "date-fns";
5
+ import { getFunctions as R } from "firebase-admin/functions";
6
6
  import * as d from "firebase-functions/logger";
7
- import { defineString as F } from "firebase-functions/params";
8
- import { chunk as P } from "lodash-es";
9
- import r from "zod";
10
- import { auth as T, db as c } from "../common.js";
11
- import { getUser as S, getParticipationStudents as I, getParticipation as y, getContest as b, getParticipationByToken as v } from "./queries.js";
12
- import { publicProcedure as w } from "./trpc.js";
13
- const N = F("QUIZMS_REGION"), W = w.input(
14
- r.strictObject({
15
- username: r.string(),
16
- password: r.string()
7
+ import { defineString as O } from "firebase-functions/params";
8
+ import { chunk as B } from "lodash-es";
9
+ import s from "zod";
10
+ import { auth as v, db as c } from "../common.js";
11
+ import { getUser as F, getVenueStudents as I, getVenue as y, getContest as b, getVenueByToken as N } from "./queries.js";
12
+ import { publicProcedure as p } from "./trpc.js";
13
+ const W = O("QUIZMS_REGION"), A = p.input(
14
+ s.strictObject({
15
+ username: s.string(),
16
+ password: s.string()
17
17
  })
18
- ).mutation(async (a) => {
19
- const { input: i } = a;
20
- d.info("Teacher login request received", { data: i });
21
- const e = await S(i.username);
22
- if (!e)
23
- throw new o({ code: "BAD_REQUEST", message: "Username non esistente" });
24
- if (e.password !== i.password)
25
- throw new o({ code: "BAD_REQUEST", message: "Password non corretta" });
26
- if (e.role !== "teacher")
27
- throw new o({ code: "FORBIDDEN", message: "Utenza non abilitata" });
28
- return T.createCustomToken(D.randomUUID(), {
18
+ ).mutation(async (n) => {
19
+ const { input: t } = n;
20
+ d.info("Teacher login request received", { data: t });
21
+ const o = await F(t.username);
22
+ if (!o)
23
+ throw new a({ code: "BAD_REQUEST", message: "Username non esistente" });
24
+ if (o.password !== t.password)
25
+ throw new a({ code: "BAD_REQUEST", message: "Password non corretta" });
26
+ if (o.role !== "teacher")
27
+ throw new a({ code: "FORBIDDEN", message: "Utenza non abilitata" });
28
+ return v.createCustomToken(D.randomUUID(), {
29
29
  role: "teacher",
30
- schoolId: e.id
30
+ schoolId: o.id
31
31
  });
32
32
  });
33
- async function f(a, i, e) {
34
- const t = await y(a);
35
- if (!t)
36
- throw new o({ code: "BAD_REQUEST", message: "Gara non valida" });
37
- if (t.schoolId !== i?.schoolId)
38
- throw new o({ code: "FORBIDDEN", message: "Gara non valida" });
39
- const n = await b(t.contestId);
40
- if (e) {
41
- if (!n.hasOnline)
42
- throw new o({ code: "FORBIDDEN", message: "Gara non abilitata per prove online" });
43
- if (g(n.contestWindowStart))
44
- throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
45
- if (h(n.contestWindowEnd))
46
- throw new o({ code: "FORBIDDEN", message: "Gara già terminata" });
33
+ async function f(n, t, o) {
34
+ const e = await y(n);
35
+ if (!e)
36
+ throw new a({ code: "BAD_REQUEST", message: "Gara non valida" });
37
+ if (e.schoolId !== t?.schoolId)
38
+ throw new a({ code: "FORBIDDEN", message: "Gara non valida" });
39
+ const i = await b(e.contestId);
40
+ if (o) {
41
+ if (!i.onlineSettings)
42
+ throw new a({ code: "FORBIDDEN", message: "Gara non abilitata per prove online" });
43
+ if (g(i.onlineSettings.contestWindow.start))
44
+ throw new a({ code: "FORBIDDEN", message: "Gara non iniziata" });
45
+ if (h(i.onlineSettings.contestWindow.end))
46
+ throw new a({ code: "FORBIDDEN", message: "Gara già terminata" });
47
47
  }
48
- return [t, n];
48
+ return [e, i];
49
49
  }
50
50
  async function z() {
51
51
  for (; ; ) {
52
- const a = await E();
53
- if (!await v(a))
54
- return a;
52
+ const n = await T();
53
+ if (!await N(n))
54
+ return n;
55
55
  }
56
56
  }
57
- const L = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
58
- const { input: i, ctx: e } = a;
59
- d.info("Teacher start participation received", { data: i });
60
- const [t, n] = await f(
61
- i.participationId,
62
- e.claims,
63
- !0
64
- );
65
- if (t.endingTime != null && n.hasOnline && (g(t.endingTime) || !n.allowRestart))
66
- throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
67
- if (t.token) {
68
- const m = await I(t.id, t.token);
57
+ const M = p.input(s.strictObject({ venueId: s.string() })).mutation(async (n) => {
58
+ const { input: t, ctx: o } = n;
59
+ d.info("Teacher start venue received", { data: t });
60
+ const [e, i] = await f(t.venueId, o.claims, !0);
61
+ if (e.participationWindow && i.onlineSettings && (g(e.participationWindow.end) || !i.onlineSettings.allowRestart))
62
+ throw new a({ code: "FORBIDDEN", message: "Gara già iniziata" });
63
+ if (e.token) {
64
+ const m = await I(e.id, e.token);
69
65
  await k(m);
70
66
  }
71
- const p = O(l(Date.now(), 3.5 * 60)), s = R(p, n.hasOnline ? n.duration : 0), u = await z();
72
- await c.doc(`participations/${t.id}`).update({
73
- token: u,
74
- startingTime: p,
75
- endingTime: s
67
+ const u = S(l(Date.now(), 3.5 * 60)), r = E(u, i.onlineSettings?.duration ?? 0), w = await z();
68
+ await c.doc(`venues/${e.id}`).update({
69
+ token: w,
70
+ participationWindow: {
71
+ start: u,
72
+ end: r
73
+ }
76
74
  });
77
75
  try {
78
- await B().taskQueue(
79
- `locations/${N.value()}/functions/updateScores`
76
+ await R().taskQueue(
77
+ `locations/${W.value()}/functions/updateScores`
80
78
  ).enqueue(
81
- { participationId: t.id, token: u },
82
- { scheduleTime: l(s, 5) }
79
+ { venueId: e.id, token: w },
80
+ { scheduleTime: l(r, 5) }
83
81
  );
84
82
  } catch (m) {
85
83
  d.error("Failed to queue updateScores task", { error: m });
86
84
  }
87
- }), Z = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
88
- const { input: i, ctx: e } = a;
89
- d.info("Teacher stop participation received", { data: i });
90
- const [t] = await f(i.participationId, e.claims, !0);
91
- if (t.startingTime == null)
92
- throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
93
- if (h(t.startingTime))
94
- throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
95
- const n = await I(t.id, t.token);
96
- await k(n);
97
- for (const p of P(n, 300)) {
98
- const s = c.batch();
99
- for (const u of p)
100
- s.delete(c.doc(`participations/${t.id}/students/${u.id}`));
101
- await s.commit();
85
+ }), L = p.input(s.strictObject({ venueId: s.string() })).mutation(async (n) => {
86
+ const { input: t, ctx: o } = n;
87
+ d.info("Teacher stop venue received", { data: t });
88
+ const [e] = await f(t.venueId, o.claims, !0);
89
+ if (e.participationWindow == null)
90
+ throw new a({ code: "FORBIDDEN", message: "Gara non iniziata" });
91
+ if (h(e.participationWindow.start))
92
+ throw new a({ code: "FORBIDDEN", message: "Gara già iniziata" });
93
+ const i = await I(e.id, e.token);
94
+ await k(i);
95
+ for (const u of B(i, 300)) {
96
+ const r = c.batch();
97
+ for (const w of u)
98
+ r.delete(c.doc(`venues/${e.id}/students/${w.id}`));
99
+ await r.commit();
102
100
  }
103
- await c.doc(`participations/${t.id}`).update({
101
+ await c.doc(`venues/${e.id}`).update({
104
102
  token: null,
105
- startingTime: null,
106
- endingTime: null
103
+ participationWindow: null
107
104
  });
108
105
  });
109
- async function k(a) {
106
+ async function k(n) {
110
107
  await Promise.all(
111
- a.map(async (i) => {
112
- i.uid && await T.revokeRefreshTokens(i.uid).catch(() => {
108
+ n.map(async (t) => {
109
+ t.uid && await v.revokeRefreshTokens(t.uid).catch(() => {
113
110
  });
114
111
  })
115
112
  );
116
113
  }
117
- const H = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
118
- const { input: i, ctx: e } = a;
119
- d.info("Teacher finalize participation received", { data: i });
120
- const [t] = await f(i.participationId, e.claims, !1);
121
- await c.doc(`participations/${t.id}`).update({ finalized: !0 });
114
+ const Z = p.input(s.strictObject({ venueId: s.string() })).mutation(async (n) => {
115
+ const { input: t, ctx: o } = n;
116
+ d.info("Teacher finalize venue received", { data: t });
117
+ const [e] = await f(t.venueId, o.claims, !1);
118
+ await c.doc(`venues/${e.id}`).update({ finalized: !0 });
122
119
  });
123
120
  export {
124
- H as teacherFinalizeParticipation,
125
- W as teacherLogin,
126
- L as teacherStartParticipation,
127
- Z as teacherStopParticipation
121
+ Z as teacherFinalizeVenue,
122
+ A as teacherLogin,
123
+ M as teacherStartContestWindow,
124
+ L as teacherStopContestWindow
128
125
  };
129
126
  //# sourceMappingURL=teacher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Participation, Student } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { TRPCError } from \"@trpc/server\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport type { DecodedIdToken } from \"firebase-admin/auth\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport {\n getContest,\n getParticipation,\n getParticipationByToken,\n getParticipationStudents,\n getUser,\n} from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin = publicProcedure\n .input(\n z.strictObject({\n username: z.string(),\n password: z.string(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Teacher login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Username non esistente\" });\n }\n\n if (user.password !== data.password) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Password non corretta\" });\n }\n if (user.role !== \"teacher\") {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Utenza non abilitata\" });\n }\n\n return auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n });\n\nasync function checkParticipation(\n participationId: string,\n claims: DecodedIdToken | undefined,\n onlineOnly: boolean,\n): Promise<[Participation, Contest]> {\n const participation = await getParticipation(participationId);\n if (!participation) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Gara non valida\" });\n }\n\n if (participation.schoolId !== claims?.schoolId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non valida\" });\n }\n\n const contest = await getContest(participation.contestId);\n if (onlineOnly) {\n if (!contest.hasOnline) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non abilitata per prove online\" });\n }\n if (isFuture(contest.contestWindowStart)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n if (isPast(contest.contestWindowEnd)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già terminata\" });\n }\n }\n\n return [participation, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getParticipationByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher start participation received\", { data });\n\n const [participation, contest] = await checkParticipation(\n data.participationId,\n ctx.claims,\n true,\n );\n\n if (\n participation.endingTime != null &&\n contest.hasOnline &&\n (isFuture(participation.endingTime) || !contest.allowRestart)\n ) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n if (participation.token) {\n const students = await getParticipationStudents(participation.id, participation.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n const token = await generateToken();\n\n await db.doc(`participations/${participation.id}`).update({\n token,\n startingTime,\n endingTime,\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { participationId: participation.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n });\n\nexport const teacherStopParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher stop participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, true);\n\n if (participation.startingTime == null) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n\n if (isPast(participation.startingTime)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n const students = await getParticipationStudents(participation.id, participation.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`participations/${participation.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n });\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher finalize participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, false);\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n });\n"],"names":["region","defineString","teacherLogin","publicProcedure","z","opts","data","logger","user","getUser","TRPCError","auth","crypto","checkParticipation","participationId","claims","onlineOnly","participation","getParticipation","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getParticipationByToken","teacherStartParticipation","ctx","students","getParticipationStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","error","teacherStopParticipation","studentChunk","chunk","batch","student","teacherFinalizeParticipation"],"mappings":"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"}
1
+ {"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Student, Venue } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { TRPCError } from \"@trpc/server\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport type { DecodedIdToken } from \"firebase-admin/auth\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport { getContest, getUser, getVenue, getVenueByToken, getVenueStudents } from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin = publicProcedure\n .input(\n z.strictObject({\n username: z.string(),\n password: z.string(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Teacher login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Username non esistente\" });\n }\n\n if (user.password !== data.password) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Password non corretta\" });\n }\n if (user.role !== \"teacher\") {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Utenza non abilitata\" });\n }\n\n return auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n });\n\nasync function checkVenue(\n venueId: string,\n claims: DecodedIdToken | undefined,\n onlineOnly: boolean,\n): Promise<[Venue, Contest]> {\n const venue = await getVenue(venueId);\n if (!venue) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Gara non valida\" });\n }\n\n if (venue.schoolId !== claims?.schoolId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non valida\" });\n }\n\n const contest = await getContest(venue.contestId);\n if (onlineOnly) {\n if (!contest.onlineSettings) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non abilitata per prove online\" });\n }\n if (isFuture(contest.onlineSettings.contestWindow.start)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n if (isPast(contest.onlineSettings.contestWindow.end)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già terminata\" });\n }\n }\n\n return [venue, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getVenueByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartContestWindow = publicProcedure\n .input(z.strictObject({ venueId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher start venue received\", { data });\n\n const [venue, contest] = await checkVenue(data.venueId, ctx.claims, true);\n\n if (\n venue.participationWindow &&\n contest.onlineSettings &&\n (isFuture(venue.participationWindow.end) || !contest.onlineSettings.allowRestart)\n ) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n if (venue.token) {\n const students = await getVenueStudents(venue.id, venue.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.onlineSettings?.duration ?? 0);\n\n const token = await generateToken();\n\n await db.doc(`venues/${venue.id}`).update({\n token,\n participationWindow: {\n start: startingTime,\n end: endingTime,\n },\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { venueId: venue.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n });\n\nexport const teacherStopContestWindow = publicProcedure\n .input(z.strictObject({ venueId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher stop venue received\", { data });\n\n const [venue] = await checkVenue(data.venueId, ctx.claims, true);\n\n if (venue.participationWindow == null) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n\n if (isPast(venue.participationWindow.start)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n const students = await getVenueStudents(venue.id, venue.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`venues/${venue.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`venues/${venue.id}`).update({\n token: null,\n participationWindow: null,\n });\n });\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeVenue = publicProcedure\n .input(z.strictObject({ venueId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher finalize venue received\", { data });\n\n const [venue] = await checkVenue(data.venueId, ctx.claims, false);\n\n await db.doc(`venues/${venue.id}`).update({ finalized: true });\n });\n"],"names":["region","defineString","teacherLogin","publicProcedure","z","opts","data","logger","user","getUser","TRPCError","auth","crypto","checkVenue","venueId","claims","onlineOnly","venue","getVenue","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getVenueByToken","teacherStartContestWindow","ctx","students","getVenueStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","error","teacherStopContestWindow","studentChunk","chunk","batch","student","teacherFinalizeVenue"],"mappings":"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"}
@@ -1,15 +1,19 @@
1
- import { db as d } from "./common.js";
2
- async function i(t) {
3
- const a = t.data?.after;
4
- a?.exists && await d.collection("submissions").add({
1
+ import { studentSchema as s, calcScore as i } from "@olinfo/quizms/models";
2
+ import { parseSnapshot as n, variantConverter as d, submissionConverter as c } from "../cli/utils/converters-admin.js";
3
+ import { db as e } from "./common.js";
4
+ async function p(t) {
5
+ const r = t.data?.after;
6
+ if (!r?.exists) return;
7
+ const a = n(s, r), o = await e.doc(`variants/${a.variantId}`).withConverter(d).get();
8
+ a.score = i(a, o.data()?.schema), await e.collection("submissions").withConverter(c).add({
5
9
  authType: t.authType,
6
10
  authId: t.authId,
7
- studentId: a.id,
8
- student: a.data(),
11
+ studentId: r.id,
12
+ student: a,
9
13
  submittedAt: t.time
10
14
  });
11
15
  }
12
16
  export {
13
- i as studentUpdatedHandler
17
+ p as studentUpdatedHandler
14
18
  };
15
19
  //# sourceMappingURL=submissions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data?.exists) return;\n\n await db.collection(\"submissions\").add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student: data.data(),\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","db"],"mappings":";AAKA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,EAAKC,GAAM,UAEX,MAAMC,EAAG,WAAW,aAAa,EAAE,IAAI;AAAA,IACrC,UAAUF,EAAM;AAAA,IAChB,QAAQA,EAAM;AAAA,IACd,WAAWC,EAAK;AAAA,IAChB,SAASA,EAAK,KAAA;AAAA,IACd,aAAaD,EAAM;AAAA,EAAA,CACpB;AACH;"}
1
+ {"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import { calcScore, studentSchema } from \"@olinfo/quizms/models\";\nimport type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { parseSnapshot, submissionConverter, variantConverter } from \"~/cli/utils/converters-admin\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data?.exists) return;\n\n const student = parseSnapshot(studentSchema, data);\n\n const variant = await db\n .doc(`variants/${student.variantId}`)\n .withConverter(variantConverter)\n .get();\n student.score = calcScore(student, variant.data()?.schema);\n\n await db.collection(\"submissions\").withConverter(submissionConverter).add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student,\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","student","parseSnapshot","studentSchema","variant","db","variantConverter","calcScore","submissionConverter"],"mappings":";;;AAQA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,MAAI,CAACC,GAAM,OAAQ;AAEnB,QAAMC,IAAUC,EAAcC,GAAeH,CAAI,GAE3CI,IAAU,MAAMC,EACnB,IAAI,YAAYJ,EAAQ,SAAS,EAAE,EACnC,cAAcK,CAAgB,EAC9B,IAAA;AACH,EAAAL,EAAQ,QAAQM,EAAUN,GAASG,EAAQ,KAAA,GAAQ,MAAM,GAEzD,MAAMC,EAAG,WAAW,aAAa,EAAE,cAAcG,CAAmB,EAAE,IAAI;AAAA,IACxE,UAAUT,EAAM;AAAA,IAChB,QAAQA,EAAM;AAAA,IACd,WAAWC,EAAK;AAAA,IAChB,SAAAC;AAAA,IACA,aAAaF,EAAM;AAAA,EAAA,CACpB;AACH;"}
@@ -1,22 +1,22 @@
1
1
  import { pipeline as u } from "node:stream/promises";
2
- import { calcScore as v } from "@olinfo/quizms/models";
3
- import { variantConverter as l, studentConverter as w } from "../cli/utils/converters-admin.js";
4
- import { db as n } from "./common.js";
5
- async function R(i) {
6
- const { participationId: s, token: c } = i.data, p = await n.collection("variants").withConverter(l).get(), f = Object.fromEntries(p.docs.map((t) => [t.id, t.data()])), d = n.collection(`participations/${s}/students`).where("token", "==", c).withConverter(w);
7
- await u(d.stream(), async function* (t) {
8
- for await (const m of t) {
9
- const o = m.ref;
10
- await n.runTransaction(async (r) => {
11
- const a = (await r.get(o)).data();
12
- if (!a?.variant) return;
13
- const e = v(a, f[a.variant]?.schema) ?? null;
14
- e !== a.score && r.update(o, { score: e });
2
+ import { calcScore as m } from "@olinfo/quizms/models";
3
+ import { variantConverter as w, studentConverter as l } from "../cli/utils/converters-admin.js";
4
+ import { db as a } from "./common.js";
5
+ async function k(s) {
6
+ const { venueId: i, token: c } = s.data, d = await a.collection("variants").withConverter(w).get(), f = Object.fromEntries(d.docs.map((t) => [t.id, t.data()])), p = a.collection(`venues/${i}/students`).where("token", "==", c).withConverter(l);
7
+ await u(p.stream(), async function* (t) {
8
+ for await (const v of t) {
9
+ const e = v.ref;
10
+ await a.runTransaction(async (o) => {
11
+ const n = (await o.get(e)).data();
12
+ if (!n?.variantId) return;
13
+ const r = m(n, f[n.variantId]?.schema);
14
+ r !== n.score && o.update(e, { score: r });
15
15
  });
16
16
  }
17
17
  });
18
18
  }
19
19
  export {
20
- R as updateScoresHandler
20
+ k as updateScoresHandler
21
21
  };
22
22
  //# sourceMappingURL=update-scores.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-scores.js","sources":["../../../src/functions/update-scores.ts"],"sourcesContent":["import { pipeline } from \"node:stream/promises\";\n\nimport { calcScore, type Student } from \"@olinfo/quizms/models\";\nimport type { QueryDocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Request } from \"firebase-functions/tasks\";\n\nimport { studentConverter, variantConverter } from \"~/cli/utils/converters-admin\";\n\nimport { db } from \"./common\";\n\nexport async function updateScoresHandler(request: Request) {\n const { participationId, token } = request.data;\n\n const variantRef = db.collection(\"variants\").withConverter(variantConverter);\n const variantSnap = await variantRef.get();\n const variants = Object.fromEntries(variantSnap.docs.map((doc) => [doc.id, doc.data()]));\n\n const ref = db\n .collection(`participations/${participationId}/students`)\n .where(\"token\", \"==\", token)\n .withConverter(studentConverter);\n\n await pipeline(ref.stream(), async function* (stream) {\n for await (const data of stream) {\n const snapshot = data as unknown as QueryDocumentSnapshot<Student>;\n const ref = snapshot.ref;\n await db.runTransaction(async (t) => {\n const studentSnap = await t.get(ref);\n const student = studentSnap.data();\n if (!student?.variant) return;\n const score = calcScore(student, variants[student.variant]?.schema) ?? null;\n if (score !== student.score) {\n t.update(ref, { score });\n }\n });\n }\n });\n}\n"],"names":["updateScoresHandler","request","participationId","token","variantSnap","db","variantConverter","variants","doc","ref","studentConverter","pipeline","stream","data","t","student","score","calcScore"],"mappings":";;;;AAUA,eAAsBA,EAAoBC,GAAkB;AAC1D,QAAM,EAAE,iBAAAC,GAAiB,OAAAC,EAAA,IAAUF,EAAQ,MAGrCG,IAAc,MADDC,EAAG,WAAW,UAAU,EAAE,cAAcC,CAAgB,EACtC,IAAA,GAC/BC,IAAW,OAAO,YAAYH,EAAY,KAAK,IAAI,CAACI,MAAQ,CAACA,EAAI,IAAIA,EAAI,KAAA,CAAM,CAAC,CAAC,GAEjFC,IAAMJ,EACT,WAAW,kBAAkBH,CAAe,WAAW,EACvD,MAAM,SAAS,MAAMC,CAAK,EAC1B,cAAcO,CAAgB;AAEjC,QAAMC,EAASF,EAAI,OAAA,GAAU,iBAAiBG,GAAQ;AACpD,qBAAiBC,KAAQD,GAAQ;AAE/B,YAAMH,IADWI,EACI;AACrB,YAAMR,EAAG,eAAe,OAAOS,MAAM;AAEnC,cAAMC,KADc,MAAMD,EAAE,IAAIL,CAAG,GACP,KAAA;AAC5B,YAAI,CAACM,GAAS,QAAS;AACvB,cAAMC,IAAQC,EAAUF,GAASR,EAASQ,EAAQ,OAAO,GAAG,MAAM,KAAK;AACvE,QAAIC,MAAUD,EAAQ,SACpBD,EAAE,OAAOL,GAAK,EAAE,OAAAO,EAAA,CAAO;AAAA,MAE3B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;"}
1
+ {"version":3,"file":"update-scores.js","sources":["../../../src/functions/update-scores.ts"],"sourcesContent":["import { pipeline } from \"node:stream/promises\";\n\nimport { calcScore, type Student } from \"@olinfo/quizms/models\";\nimport type { QueryDocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Request } from \"firebase-functions/tasks\";\n\nimport { studentConverter, variantConverter } from \"~/cli/utils/converters-admin\";\n\nimport { db } from \"./common\";\n\nexport async function updateScoresHandler(request: Request) {\n const { venueId, token } = request.data;\n\n const variantRef = db.collection(\"variants\").withConverter(variantConverter);\n const variantSnap = await variantRef.get();\n const variants = Object.fromEntries(variantSnap.docs.map((doc) => [doc.id, doc.data()]));\n\n const ref = db\n .collection(`venues/${venueId}/students`)\n .where(\"token\", \"==\", token)\n .withConverter(studentConverter);\n\n await pipeline(ref.stream(), async function* (stream) {\n for await (const data of stream) {\n const snapshot = data as unknown as QueryDocumentSnapshot<Student>;\n const ref = snapshot.ref;\n await db.runTransaction(async (t) => {\n const studentSnap = await t.get(ref);\n const student = studentSnap.data();\n if (!student?.variantId) return;\n const score = calcScore(student, variants[student.variantId]?.schema);\n if (score !== student.score) {\n t.update(ref, { score });\n }\n });\n }\n });\n}\n"],"names":["updateScoresHandler","request","venueId","token","variantSnap","db","variantConverter","variants","doc","ref","studentConverter","pipeline","stream","data","t","student","score","calcScore"],"mappings":";;;;AAUA,eAAsBA,EAAoBC,GAAkB;AAC1D,QAAM,EAAE,SAAAC,GAAS,OAAAC,EAAA,IAAUF,EAAQ,MAG7BG,IAAc,MADDC,EAAG,WAAW,UAAU,EAAE,cAAcC,CAAgB,EACtC,IAAA,GAC/BC,IAAW,OAAO,YAAYH,EAAY,KAAK,IAAI,CAACI,MAAQ,CAACA,EAAI,IAAIA,EAAI,KAAA,CAAM,CAAC,CAAC,GAEjFC,IAAMJ,EACT,WAAW,UAAUH,CAAO,WAAW,EACvC,MAAM,SAAS,MAAMC,CAAK,EAC1B,cAAcO,CAAgB;AAEjC,QAAMC,EAASF,EAAI,OAAA,GAAU,iBAAiBG,GAAQ;AACpD,qBAAiBC,KAAQD,GAAQ;AAE/B,YAAMH,IADWI,EACI;AACrB,YAAMR,EAAG,eAAe,OAAOS,MAAM;AAEnC,cAAMC,KADc,MAAMD,EAAE,IAAIL,CAAG,GACP,KAAA;AAC5B,YAAI,CAACM,GAAS,UAAW;AACzB,cAAMC,IAAQC,EAAUF,GAASR,EAASQ,EAAQ,SAAS,GAAG,MAAM;AACpE,QAAIC,MAAUD,EAAQ,SACpBD,EAAE,OAAOL,GAAK,EAAE,OAAAO,EAAA,CAAO;AAAA,MAE3B,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;"}
@@ -0,0 +1,13 @@
1
+ import { studentSchema as s } from "@olinfo/quizms/models";
2
+ import t from "zod";
3
+ const o = t.strictObject({
4
+ authType: t.string(),
5
+ authId: t.string().optional(),
6
+ studentId: t.string(),
7
+ student: s,
8
+ submittedAt: t.iso.datetime()
9
+ });
10
+ export {
11
+ o as submissionSchema
12
+ };
13
+ //# sourceMappingURL=submission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.js","sources":["../../../src/models/submission.ts"],"sourcesContent":["import { studentSchema } from \"@olinfo/quizms/models\";\nimport z from \"zod\";\n\nexport const submissionSchema = z.strictObject({\n authType: z.string(),\n authId: z.string().optional(),\n studentId: z.string(),\n student: studentSchema,\n submittedAt: z.iso.datetime(),\n});\n"],"names":["submissionSchema","z","studentSchema"],"mappings":";;AAGO,MAAMA,IAAmBC,EAAE,aAAa;AAAA,EAC7C,UAAUA,EAAE,OAAA;AAAA,EACZ,QAAQA,EAAE,OAAA,EAAS,SAAA;AAAA,EACnB,WAAWA,EAAE,OAAA;AAAA,EACb,SAASC;AAAA,EACT,aAAaD,EAAE,IAAI,SAAA;AACrB,CAAC;"}