@olinfo/quizms-firebase 4.0.1 → 4.1.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 (65) hide show
  1. package/dist/functions.d.ts +2 -11
  2. package/dist/functions.js +2 -2
  3. package/dist/package.json.js +1 -1
  4. package/dist/src/cli/files/functions-index.js +4 -4
  5. package/dist/src/cli/files/functions-index.js.map +1 -1
  6. package/dist/src/cli/init.js +8 -8
  7. package/dist/src/cli/init.js.map +1 -1
  8. package/dist/src/cli/update-scores.js +21 -32
  9. package/dist/src/cli/update-scores.js.map +1 -1
  10. package/dist/src/cli/utils/collection.js +20 -18
  11. package/dist/src/cli/utils/collection.js.map +1 -1
  12. package/dist/src/cli/utils/converters-admin.js +31 -32
  13. package/dist/src/cli/utils/converters-admin.js.map +1 -1
  14. package/dist/src/cli/utils/region-finder.js +6 -5
  15. package/dist/src/cli/utils/region-finder.js.map +1 -1
  16. package/dist/src/cli/utils/storage.js +26 -35
  17. package/dist/src/cli/utils/storage.js.map +1 -1
  18. package/dist/src/cli/utils/users.js +13 -12
  19. package/dist/src/cli/utils/users.js.map +1 -1
  20. package/dist/src/functions/api/admin.js +25 -10
  21. package/dist/src/functions/api/admin.js.map +1 -1
  22. package/dist/src/functions/api/index.js +26 -37
  23. package/dist/src/functions/api/index.js.map +1 -1
  24. package/dist/src/functions/api/queries.js +5 -5
  25. package/dist/src/functions/api/student.js +62 -72
  26. package/dist/src/functions/api/student.js.map +1 -1
  27. package/dist/src/functions/api/teacher.js +104 -111
  28. package/dist/src/functions/api/teacher.js.map +1 -1
  29. package/dist/src/functions/api/trpc.js +18 -0
  30. package/dist/src/functions/api/trpc.js.map +1 -0
  31. package/dist/src/functions/submissions.js +1 -1
  32. package/dist/src/functions/submissions.js.map +1 -1
  33. package/dist/src/models/user.js +2 -2
  34. package/dist/src/models/user.js.map +1 -1
  35. package/dist/src/models/website.js +1 -1
  36. package/dist/src/models/website.js.map +1 -1
  37. package/dist/src/vite/preset.js +3 -4
  38. package/dist/src/vite/preset.js.map +1 -1
  39. package/dist/src/web/admin/announcements.js +9 -10
  40. package/dist/src/web/admin/announcements.js.map +1 -1
  41. package/dist/src/web/admin/school-table.js +7 -8
  42. package/dist/src/web/admin/school-table.js.map +1 -1
  43. package/dist/src/web/common/api.js +32 -35
  44. package/dist/src/web/common/api.js.map +1 -1
  45. package/dist/src/web/common/converters.js +35 -36
  46. package/dist/src/web/common/converters.js.map +1 -1
  47. package/dist/src/web/common/token-login.js +11 -11
  48. package/dist/src/web/common/token-login.js.map +1 -1
  49. package/dist/src/web/entry/admin.js +5 -7
  50. package/dist/src/web/entry/admin.js.map +1 -1
  51. package/dist/src/web/entry/student-restoring.js +18 -20
  52. package/dist/src/web/entry/student-restoring.js.map +1 -1
  53. package/dist/src/web/entry/student.js +70 -76
  54. package/dist/src/web/entry/student.js.map +1 -1
  55. package/dist/src/web/entry/teacher.js +68 -68
  56. package/dist/src/web/entry/teacher.js.map +1 -1
  57. package/dist/src/web/hooks/collection.js +29 -30
  58. package/dist/src/web/hooks/collection.js.map +1 -1
  59. package/dist/src/web/hooks/document.js +35 -36
  60. package/dist/src/web/hooks/document.js.map +1 -1
  61. package/dist/src/web/hooks/subscription.js +26 -22
  62. package/dist/src/web/hooks/subscription.js.map +1 -1
  63. package/package.json +23 -19
  64. package/dist/src/functions/api/schema.js +0 -42
  65. package/dist/src/functions/api/schema.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sources":["../../../../src/cli/utils/storage.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm } from \"@inquirer/prompts\";\nimport { AsyncPool } from \"@olinfo/quizms/utils\";\nimport { fatal, success } from \"@olinfo/quizms/utils-node\";\nimport { SingleBar } from \"cli-progress\";\nimport { partition } from \"lodash-es\";\nimport pc from \"picocolors\";\n\ntype ImportOptions = {\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importStorage(\n bucket: Bucket,\n collection: string,\n files: [string, string][],\n options: ImportOptions,\n) {\n const bar = new SingleBar({\n format: \" {bar} {percentage}% | {value}/{total}\",\n barCompleteChar: \"\\u2588\",\n barIncompleteChar: \"\\u2582\",\n });\n\n const existingFiles = new Set<string>();\n\n bar.start(files.length, 0);\n await Promise.all(\n files.map(async ([local, remote]) => {\n if (!existsSync(local)) {\n fatal(\n `Cannot find ${collection}. Make sure to generate them first, use \\`quizms --help\\` for usage.`,\n );\n }\n\n const file = bucket.file(remote);\n const [exists] = await file.exists().catch(() => [false]);\n if (exists) {\n existingFiles.add(remote);\n }\n bar.increment();\n }),\n );\n bar.stop();\n\n const [existing, nonExisting] = partition(files, ([_, remote]) => existingFiles.has(remote));\n if (existing.length > 0 && !options.skipExisting && !options.force) {\n fatal(\n `Files already exist in \\`${collection}\\`. Use \\`--force\\` to overwrite or \\`--skip-existing\\` to ignore.`,\n );\n }\n\n const filesToImport = options.skipExisting ? nonExisting : [...existing, ...nonExisting];\n\n if (options.force && existing.length > 0) {\n const confirmed = await confirm({\n message: `You are about to import the ${pc.bold(\n collection,\n )}. ${existing.length} files will be overwritten, the previous data will be lost. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n }\n\n bar.start(filesToImport.length, 0);\n\n const pool = new AsyncPool(8);\n for (const [local, remote] of filesToImport) {\n void pool.run(async () => {\n await bucket.upload(local, { destination: remote });\n bar.increment();\n });\n }\n await pool.wait();\n\n bar.stop();\n\n success(`${filesToImport.length} ${collection} imported!`);\n}\n"],"names":["importStorage","bucket","collection","files","options","bar","SingleBar","existingFiles","local","remote","existsSync","fatal","file","exists","existing","nonExisting","partition","_","filesToImport","confirm","pc","pool","AsyncPool","success"],"mappings":";;;;;;;AAeA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACA;AACA,QAAMC,IAAM,IAAIC,EAAU;AAAA,IACxB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,EAAA,CACpB,GAEKC,wBAAoB,IAAA;AAE1B,EAAAF,EAAI,MAAMF,EAAM,QAAQ,CAAC,GACzB,MAAM,QAAQ;AAAA,IACZA,EAAM,IAAI,OAAO,CAACK,GAAOC,CAAM,MAAM;AACnC,MAAKC,EAAWF,CAAK,KACnBG;AAAA,QACE,eAAeT,CAAU;AAAA,MAAA;AAI7B,YAAMU,IAAOX,EAAO,KAAKQ,CAAM,GACzB,CAACI,CAAM,IAAI,MAAMD,EAAK,OAAA,EAAS,MAAM,MAAM,CAAC,EAAK,CAAC;AACxD,MAAIC,KACFN,EAAc,IAAIE,CAAM,GAE1BJ,EAAI,UAAA;AAAA,IACN,CAAC;AAAA,EAAA,GAEHA,EAAI,KAAA;AAEJ,QAAM,CAACS,GAAUC,CAAW,IAAIC,EAAUb,GAAO,CAAC,CAACc,GAAGR,CAAM,MAAMF,EAAc,IAAIE,CAAM,CAAC;AAC3F,EAAIK,EAAS,SAAS,KAAK,CAACV,EAAQ,gBAAgB,CAACA,EAAQ,SAC3DO;AAAA,IACE,4BAA4BT,CAAU;AAAA,EAAA;AAI1C,QAAMgB,IAAgBd,EAAQ,eAAeW,IAAc,CAAC,GAAGD,GAAU,GAAGC,CAAW;AAEvF,EAAIX,EAAQ,SAASU,EAAS,SAAS,MACnB,MAAMK,EAAQ;AAAA,IAC9B,SAAS,+BAA+BC,EAAG;AAAA,MACzClB;AAAA,IAAA,CACD,KAAKY,EAAS,MAAM;AAAA,IACrB,SAAS;AAAA,EAAA,CACV,KAECH,EAAM,kBAAkB,IAI5BN,EAAI,MAAMa,EAAc,QAAQ,CAAC;AAEjC,QAAMG,IAAO,IAAIC,EAAU,CAAC;AAC5B,aAAW,CAACd,GAAOC,CAAM,KAAKS;AAC5B,IAAKG,EAAK,IAAI,YAAY;AACxB,YAAMpB,EAAO,OAAOO,GAAO,EAAE,aAAaC,GAAQ,GAClDJ,EAAI,UAAA;AAAA,IACN,CAAC;AAEH,QAAMgB,EAAK,KAAA,GAEXhB,EAAI,KAAA,GAEJkB,EAAQ,GAAGL,EAAc,MAAM,IAAIhB,CAAU,YAAY;AAC3D;"}
1
+ {"version":3,"file":"storage.js","sources":["../../../../src/cli/utils/storage.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { styleText } from \"node:util\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm } from \"@inquirer/prompts\";\nimport { fatal, success, withProgress } from \"@olinfo/quizms/utils-node\";\nimport { partition } from \"lodash-es\";\n\ntype ImportOptions = {\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importStorage(\n bucket: Bucket,\n collection: string,\n files: [string, string][],\n options: ImportOptions,\n) {\n const existingFiles = new Set<string>();\n\n await withProgress(files, files.length, async ([local, remote]) => {\n if (!existsSync(local)) {\n fatal(\n `Cannot find ${collection}. Make sure to generate them first, use \\`quizms --help\\` for usage.`,\n );\n }\n\n const file = bucket.file(remote);\n const [exists] = await file.exists().catch(() => [false]);\n if (exists) {\n existingFiles.add(remote);\n }\n });\n\n const [existing, nonExisting] = partition(files, ([_, remote]) => existingFiles.has(remote));\n if (existing.length > 0 && !options.skipExisting && !options.force) {\n fatal(\n `Files already exist in \\`${collection}\\`. Use \\`--force\\` to overwrite or \\`--skip-existing\\` to ignore.`,\n );\n }\n\n const filesToImport = options.skipExisting ? nonExisting : [...existing, ...nonExisting];\n\n if (options.force && existing.length > 0) {\n const confirmed = await confirm({\n message: `You are about to import the ${styleText(\n \"bold\",\n collection,\n )}. ${existing.length} files will be overwritten, the previous data will be lost. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n }\n\n await withProgress(filesToImport, filesToImport.length, ([local, remote]) =>\n bucket.upload(local, { destination: remote }),\n );\n\n success(`${filesToImport.length} ${collection} imported!`);\n}\n"],"names":["importStorage","bucket","collection","files","options","existingFiles","withProgress","local","remote","existsSync","fatal","file","exists","existing","nonExisting","partition","_","filesToImport","confirm","styleText","success"],"mappings":";;;;;AAaA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACA;AACA,QAAMC,wBAAoB,IAAA;AAE1B,QAAMC,EAAaH,GAAOA,EAAM,QAAQ,OAAO,CAACI,GAAOC,CAAM,MAAM;AACjE,IAAKC,EAAWF,CAAK,KACnBG;AAAA,MACE,eAAeR,CAAU;AAAA,IAAA;AAI7B,UAAMS,IAAOV,EAAO,KAAKO,CAAM,GACzB,CAACI,CAAM,IAAI,MAAMD,EAAK,OAAA,EAAS,MAAM,MAAM,CAAC,EAAK,CAAC;AACxD,IAAIC,KACFP,EAAc,IAAIG,CAAM;AAAA,EAE5B,CAAC;AAED,QAAM,CAACK,GAAUC,CAAW,IAAIC,EAAUZ,GAAO,CAAC,CAACa,GAAGR,CAAM,MAAMH,EAAc,IAAIG,CAAM,CAAC;AAC3F,EAAIK,EAAS,SAAS,KAAK,CAACT,EAAQ,gBAAgB,CAACA,EAAQ,SAC3DM;AAAA,IACE,4BAA4BR,CAAU;AAAA,EAAA;AAI1C,QAAMe,IAAgBb,EAAQ,eAAeU,IAAc,CAAC,GAAGD,GAAU,GAAGC,CAAW;AAEvF,EAAIV,EAAQ,SAASS,EAAS,SAAS,MACnB,MAAMK,EAAQ;AAAA,IAC9B,SAAS,+BAA+BC;AAAA,MACtC;AAAA,MACAjB;AAAA,IAAA,CACD,KAAKW,EAAS,MAAM;AAAA,IACrB,SAAS;AAAA,EAAA,CACV,KAECH,EAAM,kBAAkB,IAI5B,MAAMJ;AAAA,IAAaW;AAAA,IAAeA,EAAc;AAAA,IAAQ,CAAC,CAACV,GAAOC,CAAM,MACrEP,EAAO,OAAOM,GAAO,EAAE,aAAaC,EAAA,CAAQ;AAAA,EAAA,GAG9CY,EAAQ,GAAGH,EAAc,MAAM,IAAIf,CAAU,YAAY;AAC3D;"}
@@ -1,30 +1,31 @@
1
- import { confirm as m } from "@inquirer/prompts";
2
- import { fatal as s } from "@olinfo/quizms/utils-node";
1
+ import { styleText as m } from "node:util";
2
+ import { confirm as s } from "@inquirer/prompts";
3
+ import { fatal as l } from "@olinfo/quizms/utils-node";
3
4
  import { info as n } from "firebase-functions/logger";
4
5
  import { upperFirst as f } from "lodash-es";
5
- import l from "picocolors";
6
- import { importCollection as p } from "./collection.js";
7
- import { userConverter as u } from "./converters-admin.js";
8
- async function A(e, o, i, t) {
6
+ import { importCollection as u } from "./collection.js";
7
+ import { userConverter as p } from "./converters-admin.js";
8
+ async function x(e, o, i, t) {
9
9
  if (t.delete) {
10
- await m({
11
- message: `You are about to delete all ${l.bold(
10
+ await s({
11
+ message: `You are about to delete all ${m(
12
+ "bold",
12
13
  o
13
14
  )} users. Any previous data will be lost, this operation cannot be undone. Are you really sure?`,
14
15
  default: !1
15
- }) || s("Command aborted.");
16
+ }) || l("Command aborted.");
16
17
  const a = e.collection("users");
17
18
  await e.recursiveDelete(a), n(`${f(o)} users deleted!`);
18
19
  }
19
- await p(
20
+ await u(
20
21
  e,
21
22
  "users",
22
23
  i.map((r) => ({ ...r, id: r.username, role: o })),
23
- u,
24
+ p,
24
25
  { ...t, delete: void 0 }
25
26
  );
26
27
  }
27
28
  export {
28
- A as importUsers
29
+ x as importUsers
29
30
  };
30
31
  //# sourceMappingURL=users.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"users.js","sources":["../../../../src/cli/utils/users.ts"],"sourcesContent":["import { confirm } from \"@inquirer/prompts\";\nimport { fatal } from \"@olinfo/quizms/utils-node\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { info } from \"firebase-functions/logger\";\nimport { upperFirst } from \"lodash-es\";\nimport pc from \"picocolors\";\n\nimport type { User } from \"~/models/user\";\n\nimport { importCollection } from \"./collection\";\nimport { userConverter } from \"./converters-admin\";\n\ntype ImportOptions = {\n delete?: true;\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importUsers(\n db: Firestore,\n role: string,\n users: Omit<User, \"id\" | \"role\">[],\n options: ImportOptions,\n) {\n if (options.delete) {\n const confirmed = await confirm({\n message: `You are about to delete all ${pc.bold(\n role,\n )} users. Any previous data will be lost, this operation cannot be undone. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n\n const ref = db.collection(\"users\");\n await db.recursiveDelete(ref);\n info(`${upperFirst(role)} users deleted!`);\n }\n\n await importCollection(\n db,\n \"users\",\n users.map((u) => ({ ...u, id: u.username, role })),\n userConverter,\n { ...options, delete: undefined },\n );\n}\n"],"names":["importUsers","db","role","users","options","confirm","pc","fatal","ref","info","upperFirst","importCollection","u","userConverter"],"mappings":";;;;;;;AAkBA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACA;AACA,MAAIA,EAAQ,QAAQ;AAOlB,IANkB,MAAMC,EAAQ;AAAA,MAC9B,SAAS,+BAA+BC,EAAG;AAAA,QACzCJ;AAAA,MAAA,CACD;AAAA,MACD,SAAS;AAAA,IAAA,CACV,KAECK,EAAM,kBAAkB;AAG1B,UAAMC,IAAMP,EAAG,WAAW,OAAO;AACjC,UAAMA,EAAG,gBAAgBO,CAAG,GAC5BC,EAAK,GAAGC,EAAWR,CAAI,CAAC,iBAAiB;AAAA,EAC3C;AAEA,QAAMS;AAAA,IACJV;AAAA,IACA;AAAA,IACAE,EAAM,IAAI,CAACS,OAAO,EAAE,GAAGA,GAAG,IAAIA,EAAE,UAAU,MAAAV,EAAA,EAAO;AAAA,IACjDW;AAAA,IACA,EAAE,GAAGT,GAAS,QAAQ,OAAA;AAAA,EAAU;AAEpC;"}
1
+ {"version":3,"file":"users.js","sources":["../../../../src/cli/utils/users.ts"],"sourcesContent":["import { styleText } from \"node:util\";\n\nimport { confirm } from \"@inquirer/prompts\";\nimport { fatal } from \"@olinfo/quizms/utils-node\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { info } from \"firebase-functions/logger\";\nimport { upperFirst } from \"lodash-es\";\n\nimport type { User } from \"~/models/user\";\n\nimport { importCollection } from \"./collection\";\nimport { userConverter } from \"./converters-admin\";\n\ntype ImportOptions = {\n delete?: true;\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importUsers(\n db: Firestore,\n role: string,\n users: Omit<User, \"id\" | \"role\">[],\n options: ImportOptions,\n) {\n if (options.delete) {\n const confirmed = await confirm({\n message: `You are about to delete all ${styleText(\n \"bold\",\n role,\n )} users. Any previous data will be lost, this operation cannot be undone. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n\n const ref = db.collection(\"users\");\n await db.recursiveDelete(ref);\n info(`${upperFirst(role)} users deleted!`);\n }\n\n await importCollection(\n db,\n \"users\",\n users.map((u) => ({ ...u, id: u.username, role })),\n userConverter,\n { ...options, delete: undefined },\n );\n}\n"],"names":["importUsers","db","role","users","options","confirm","styleText","fatal","ref","info","upperFirst","importCollection","u","userConverter"],"mappings":";;;;;;;AAmBA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACA;AACA,MAAIA,EAAQ,QAAQ;AAQlB,IAPkB,MAAMC,EAAQ;AAAA,MAC9B,SAAS,+BAA+BC;AAAA,QACtC;AAAA,QACAJ;AAAA,MAAA,CACD;AAAA,MACD,SAAS;AAAA,IAAA,CACV,KAECK,EAAM,kBAAkB;AAG1B,UAAMC,IAAMP,EAAG,WAAW,OAAO;AACjC,UAAMA,EAAG,gBAAgBO,CAAG,GAC5BC,EAAK,GAAGC,EAAWR,CAAI,CAAC,iBAAiB;AAAA,EAC3C;AAEA,QAAMS;AAAA,IACJV;AAAA,IACA;AAAA,IACAE,EAAM,IAAI,CAACS,OAAO,EAAE,GAAGA,GAAG,IAAIA,EAAE,UAAU,MAAAV,EAAA,EAAO;AAAA,IACjDW;AAAA,IACA,EAAE,GAAGT,GAAS,QAAQ,OAAA;AAAA,EAAU;AAEpC;"}
@@ -1,13 +1,28 @@
1
- import o from "node:crypto";
2
- import * as s from "firebase-functions/logger";
3
- import { auth as t } from "../common.js";
4
- import { getUser as n } from "./queries.js";
5
- const d = async (a, r) => {
6
- s.info("Admin login request received", { data: r });
7
- const e = await n(r.username);
8
- return e ? e.role !== "admin" ? { success: !1, errorCode: "INVALID_ROLE", error: "Utenza non abilitata" } : e.password !== r.password ? { success: !1, errorCode: "INVALID_PASSWORD", error: "Password non corretta" } : { success: !0, data: { token: await t.createCustomToken(o.randomUUID(), { role: "admin" }) } } : { success: !1, errorCode: "USER_NOT_FOUND", error: "Username non esistente" };
9
- };
1
+ import s from "node:crypto";
2
+ import { TRPCError as e } from "@trpc/server";
3
+ import * as i from "firebase-functions/logger";
4
+ import t from "zod";
5
+ import { auth as a } from "../common.js";
6
+ import { getUser as m } from "./queries.js";
7
+ import { publicProcedure as c } from "./trpc.js";
8
+ const U = c.input(
9
+ t.strictObject({
10
+ username: t.string(),
11
+ password: t.string()
12
+ })
13
+ ).mutation(async (n) => {
14
+ const { input: r } = n;
15
+ i.info("Admin login request received", { data: r });
16
+ const o = await m(r.username);
17
+ if (!o)
18
+ throw new e({ code: "BAD_REQUEST", message: "Username non esistente" });
19
+ if (o.password !== r.password)
20
+ throw new e({ code: "BAD_REQUEST", message: "Password non corretta" });
21
+ if (o.role !== "admin")
22
+ throw new e({ code: "FORBIDDEN", message: "Utenza non abilitata" });
23
+ return a.createCustomToken(s.randomUUID(), { role: "admin" });
24
+ });
10
25
  export {
11
- d as adminLogin
26
+ U as adminLogin
12
27
  };
13
28
  //# sourceMappingURL=admin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.js","sources":["../../../../src/functions/api/admin.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport * as logger from \"firebase-functions/logger\";\n\nimport { auth, type Endpoint } from \"~/functions/common\";\n\nimport { getUser } from \"./queries\";\nimport type { AdminLogin } from \"./schema\";\n\nexport const adminLogin: Endpoint<AdminLogin> = async (_request, data) => {\n logger.info(\"Admin login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n return { success: false, errorCode: \"USER_NOT_FOUND\", error: \"Username non esistente\" };\n }\n\n if (user.role !== \"admin\") {\n return { success: false, errorCode: \"INVALID_ROLE\", error: \"Utenza non abilitata\" };\n }\n if (user.password !== data.password) {\n return { success: false, errorCode: \"INVALID_PASSWORD\", error: \"Password non corretta\" };\n }\n const token = await auth.createCustomToken(crypto.randomUUID(), { role: \"admin\" });\n return { success: true, data: { token } };\n};\n"],"names":["adminLogin","_request","data","logger","user","getUser","auth","crypto"],"mappings":";;;;AASO,MAAMA,IAAmC,OAAOC,GAAUC,MAAS;AACxE,EAAAC,EAAO,KAAK,gCAAgC,EAAE,MAAAD,EAAA,CAAM;AAEpD,QAAME,IAAO,MAAMC,EAAQH,EAAK,QAAQ;AACxC,SAAKE,IAIDA,EAAK,SAAS,UACT,EAAE,SAAS,IAAO,WAAW,gBAAgB,OAAO,uBAAA,IAEzDA,EAAK,aAAaF,EAAK,WAClB,EAAE,SAAS,IAAO,WAAW,oBAAoB,OAAO,wBAAA,IAG1D,EAAE,SAAS,IAAM,MAAM,EAAE,OADlB,MAAMI,EAAK,kBAAkBC,EAAO,cAAc,EAAE,MAAM,SAAS,IAC3C,IAV7B,EAAE,SAAS,IAAO,WAAW,kBAAkB,OAAO,yBAAA;AAWjE;"}
1
+ {"version":3,"file":"admin.js","sources":["../../../../src/functions/api/admin.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport { TRPCError } from \"@trpc/server\";\nimport * as logger from \"firebase-functions/logger\";\nimport z from \"zod\";\n\nimport { auth } from \"../common\";\nimport { getUser } from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nexport const adminLogin = 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(\"Admin 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 !== \"admin\") {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Utenza non abilitata\" });\n }\n return auth.createCustomToken(crypto.randomUUID(), { role: \"admin\" });\n });\n"],"names":["adminLogin","publicProcedure","z","opts","data","logger","user","getUser","TRPCError","auth","crypto"],"mappings":";;;;;;;AAUO,MAAMA,IAAaC,EACvB;AAAA,EACCC,EAAE,aAAa;AAAA,IACb,UAAUA,EAAE,OAAA;AAAA,IACZ,UAAUA,EAAE,OAAA;AAAA,EAAO,CACpB;AACH,EACC,SAAS,OAAOC,MAAS;AACxB,QAAM,EAAE,OAAOC,EAAA,IAASD;AACxB,EAAAE,EAAO,KAAK,gCAAgC,EAAE,MAAAD,EAAA,CAAM;AAEpD,QAAME,IAAO,MAAMC,EAAQH,EAAK,QAAQ;AACxC,MAAI,CAACE;AACH,UAAM,IAAIE,EAAU,EAAE,MAAM,eAAe,SAAS,0BAA0B;AAGhF,MAAIF,EAAK,aAAaF,EAAK;AACzB,UAAM,IAAII,EAAU,EAAE,MAAM,eAAe,SAAS,yBAAyB;AAE/E,MAAIF,EAAK,SAAS;AAChB,UAAM,IAAIE,EAAU,EAAE,MAAM,aAAa,SAAS,wBAAwB;AAE5E,SAAOC,EAAK,kBAAkBC,EAAO,WAAA,GAAc,EAAE,MAAM,SAAS;AACtE,CAAC;"}
@@ -1,41 +1,30 @@
1
- import { validate as i } from "@olinfo/quizms/utils";
2
- import * as t from "firebase-functions/logger";
3
- import { adminLogin as o } from "./admin.js";
4
- import { apiRequestSchema as c } from "./schema.js";
5
- import { studentLogin as n } from "./student.js";
6
- import { teacherFinalizeParticipation as s, teacherStopParticipation as p, teacherStartParticipation as d, teacherLogin as m } from "./teacher.js";
7
- async function w(a) {
8
- t.info(`Received request for action: ${a.data.action}`);
9
- const r = await f(a);
10
- return r.success || t.error("Request failed", { request: a, result: r }), r;
11
- }
12
- async function f(a) {
13
- let r;
14
- try {
15
- r = i(c, a.data);
16
- } catch (e) {
17
- return t.error("Invalid request", { error: e }), { success: !1, errorCode: "INVALID_REQUEST", error: "Richiesta non valida" };
18
- }
19
- try {
20
- switch (r.action) {
21
- case "adminLogin":
22
- return await o(a, r);
23
- case "studentLogin":
24
- return await n(a, r);
25
- case "teacherLogin":
26
- return await m(a, r);
27
- case "teacherStartParticipation":
28
- return await d(a, r);
29
- case "teacherStopParticipation":
30
- return await p(a, r);
31
- case "teacherFinalizeParticipation":
32
- return await s(a, r);
1
+ import * as a from "@trpc/server/adapters/express";
2
+ import p from "express";
3
+ import { logger as m } from "firebase-functions/logger";
4
+ import { adminLogin as n } from "./admin.js";
5
+ import { studentLogin as s } from "./student.js";
6
+ import { teacherFinalizeParticipation as c, teacherStopParticipation as f, teacherStartParticipation as u, teacherLogin as g } from "./teacher.js";
7
+ import { router as x, createContext as d } from "./trpc.js";
8
+ const h = x({
9
+ adminLogin: n,
10
+ studentLogin: s,
11
+ teacherLogin: g,
12
+ teacherStartParticipation: u,
13
+ teacherStopParticipation: f,
14
+ teacherFinalizeParticipation: c
15
+ }), l = p();
16
+ l.use(
17
+ "/",
18
+ a.createExpressMiddleware({
19
+ router: h,
20
+ createContext: d,
21
+ onError(t) {
22
+ const { error: r, path: o, input: e, ctx: i } = t;
23
+ m.error(r.message, { error: r, path: o, input: e, ctx: i });
33
24
  }
34
- } catch (e) {
35
- return t.error("Error processing request", { error: e }), { success: !1, errorCode: "INTERNAL_ERROR", error: "Errore del server" };
36
- }
37
- }
25
+ })
26
+ );
38
27
  export {
39
- w as apiHandler
28
+ l as default
40
29
  };
41
30
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/functions/api/index.ts"],"sourcesContent":["import { validate } from \"@olinfo/quizms/utils\";\nimport type { CallableRequest } from \"firebase-functions/https\";\nimport * as logger from \"firebase-functions/logger\";\n\nimport { adminLogin } from \"./admin\";\nimport { type ApiRequest, apiRequestSchema } from \"./schema\";\nimport { studentLogin } from \"./student\";\nimport {\n teacherFinalizeParticipation,\n teacherLogin,\n teacherStartParticipation,\n teacherStopParticipation,\n} from \"./teacher\";\n\nexport async function apiHandler(request: CallableRequest) {\n logger.info(`Received request for action: ${request.data.action}`);\n const result = await dispatch(request);\n if (!result.success) {\n logger.error(\"Request failed\", { request, result });\n }\n return result;\n}\n\nasync function dispatch(request: CallableRequest) {\n let data: ApiRequest;\n try {\n data = validate(apiRequestSchema, request.data);\n } catch (error) {\n logger.error(\"Invalid request\", { error });\n return { success: false, errorCode: \"INVALID_REQUEST\", error: \"Richiesta non valida\" };\n }\n\n try {\n switch (data.action) {\n case \"adminLogin\":\n return await adminLogin(request, data);\n case \"studentLogin\":\n return await studentLogin(request, data);\n case \"teacherLogin\":\n return await teacherLogin(request, data);\n case \"teacherStartParticipation\":\n return await teacherStartParticipation(request, data);\n case \"teacherStopParticipation\":\n return await teacherStopParticipation(request, data);\n case \"teacherFinalizeParticipation\":\n return await teacherFinalizeParticipation(request, data);\n }\n } catch (error) {\n logger.error(\"Error processing request\", { error });\n return { success: false, errorCode: \"INTERNAL_ERROR\", error: \"Errore del server\" };\n }\n}\n"],"names":["apiHandler","request","logger","result","dispatch","data","validate","apiRequestSchema","error","adminLogin","studentLogin","teacherLogin","teacherStartParticipation","teacherStopParticipation","teacherFinalizeParticipation"],"mappings":";;;;;;AAcA,eAAsBA,EAAWC,GAA0B;AACzD,EAAAC,EAAO,KAAK,gCAAgCD,EAAQ,KAAK,MAAM,EAAE;AACjE,QAAME,IAAS,MAAMC,EAASH,CAAO;AACrC,SAAKE,EAAO,WACVD,EAAO,MAAM,kBAAkB,EAAE,SAAAD,GAAS,QAAAE,GAAQ,GAE7CA;AACT;AAEA,eAAeC,EAASH,GAA0B;AAChD,MAAII;AACJ,MAAI;AACF,IAAAA,IAAOC,EAASC,GAAkBN,EAAQ,IAAI;AAAA,EAChD,SAASO,GAAO;AACd,WAAAN,EAAO,MAAM,mBAAmB,EAAE,OAAAM,EAAA,CAAO,GAClC,EAAE,SAAS,IAAO,WAAW,mBAAmB,OAAO,uBAAA;AAAA,EAChE;AAEA,MAAI;AACF,YAAQH,EAAK,QAAA;AAAA,MACX,KAAK;AACH,eAAO,MAAMI,EAAWR,GAASI,CAAI;AAAA,MACvC,KAAK;AACH,eAAO,MAAMK,EAAaT,GAASI,CAAI;AAAA,MACzC,KAAK;AACH,eAAO,MAAMM,EAAaV,GAASI,CAAI;AAAA,MACzC,KAAK;AACH,eAAO,MAAMO,EAA0BX,GAASI,CAAI;AAAA,MACtD,KAAK;AACH,eAAO,MAAMQ,EAAyBZ,GAASI,CAAI;AAAA,MACrD,KAAK;AACH,eAAO,MAAMS,EAA6Bb,GAASI,CAAI;AAAA,IAAA;AAAA,EAE7D,SAASG,GAAO;AACd,WAAAN,EAAO,MAAM,4BAA4B,EAAE,OAAAM,EAAA,CAAO,GAC3C,EAAE,SAAS,IAAO,WAAW,kBAAkB,OAAO,oBAAA;AAAA,EAC/D;AACF;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/functions/api/index.ts"],"sourcesContent":["import * as trpcExpress from \"@trpc/server/adapters/express\";\nimport express from \"express\";\nimport { logger } from \"firebase-functions/logger\";\n\nimport { adminLogin } from \"./admin\";\nimport { studentLogin } from \"./student\";\nimport {\n teacherFinalizeParticipation,\n teacherLogin,\n teacherStartParticipation,\n teacherStopParticipation,\n} from \"./teacher\";\nimport { createContext, router } from \"./trpc\";\n\nconst appRouter = router({\n adminLogin,\n studentLogin,\n teacherLogin,\n teacherStartParticipation,\n teacherStopParticipation,\n teacherFinalizeParticipation,\n});\n\nexport type AppRouter = typeof appRouter;\n\nconst app = express();\napp.use(\n \"/\",\n trpcExpress.createExpressMiddleware({\n router: appRouter,\n createContext,\n onError(opts) {\n const { error, path, input, ctx } = opts;\n logger.error(error.message, { error, path, input, ctx });\n },\n }),\n);\n\nexport default app;\n"],"names":["appRouter","router","adminLogin","studentLogin","teacherLogin","teacherStartParticipation","teacherStopParticipation","teacherFinalizeParticipation","app","express","trpcExpress","createContext","opts","error","path","input","ctx","logger"],"mappings":";;;;;;;AAcA,MAAMA,IAAYC,EAAO;AAAA,EACvB,YAAAC;AAAA,EACA,cAAAC;AAAA,EACA,cAAAC;AAAA,EACA,2BAAAC;AAAA,EACA,0BAAAC;AAAA,EACA,8BAAAC;AACF,CAAC,GAIKC,IAAMC,EAAA;AACZD,EAAI;AAAA,EACF;AAAA,EACAE,EAAY,wBAAwB;AAAA,IAClC,QAAQV;AAAA,IACR,eAAAW;AAAA,IACA,QAAQC,GAAM;AACZ,YAAM,EAAE,OAAAC,GAAO,MAAAC,GAAM,OAAAC,GAAO,KAAAC,MAAQJ;AACpC,MAAAK,EAAO,MAAMJ,EAAM,SAAS,EAAE,OAAAA,GAAO,MAAAC,GAAM,OAAAC,GAAO,KAAAC,GAAK;AAAA,IACzD;AAAA,EAAA,CACD;AACH;"}
@@ -1,19 +1,19 @@
1
1
  import i from "node:crypto";
2
2
  import { Rng as c } from "@olinfo/quizms/utils";
3
- import { studentConverter as o, userConverter as p, participationConverter as a, contestConverter as h, variantConverter as d } from "../../cli/utils/converters-admin.js";
3
+ import { userConverter as p, participationConverter as o, contestConverter as h, studentConverter as a, variantConverter as d } from "../../cli/utils/converters-admin.js";
4
4
  import { db as e } from "../common.js";
5
5
  const u = new c(i.randomUUID());
6
6
  async function l(t) {
7
7
  return (await e.doc(`contests/${t}`).withConverter(h).get()).data();
8
8
  }
9
9
  async function m(t) {
10
- return (await e.doc(`participations/${t}`).withConverter(a).get()).data();
10
+ return (await e.doc(`participations/${t}`).withConverter(o).get()).data();
11
11
  }
12
12
  async function f(t, n) {
13
- return (await e.collection(`participations/${t}/students`).where("token", "==", n).withConverter(o).get()).docs.map((s) => s.data());
13
+ return (await e.collection(`participations/${t}/students`).where("token", "==", n).withConverter(a).get()).docs.map((s) => s.data());
14
14
  }
15
15
  async function y(t) {
16
- return (await e.collection("participations").withConverter(a).where("token", "==", t).get()).docs[0]?.data();
16
+ return (await e.collection("participations").withConverter(o).where("token", "==", t).get()).docs[0]?.data();
17
17
  }
18
18
  async function $(t) {
19
19
  return (await e.collection("users").where("username", "==", t).withConverter(p).get()).docs[0]?.data();
@@ -23,7 +23,7 @@ async function k(t) {
23
23
  return u.choice(n.docs).data();
24
24
  }
25
25
  async function P(t, n) {
26
- return (await e.collection(`participations/${t}/students`).withConverter(o).where("userDataHash", "==", n).where("disabled", "!=", !0).get()).docs[0]?.data();
26
+ return (await e.collection(`participations/${t}/students`).withConverter(a).where("userDataHash", "==", n).where("disabled", "!=", !0).get()).docs[0]?.data();
27
27
  }
28
28
  export {
29
29
  l as getContest,
@@ -1,88 +1,78 @@
1
- import g from "node:crypto";
2
- import { sha256 as k } from "@noble/hashes/sha2";
3
- import { parseUserData as w, getNormalizedUserData as T } from "@olinfo/quizms/models";
4
- import { Timestamp as s } from "firebase-admin/firestore";
5
- import * as I from "firebase-functions/logger";
6
- import { db as f, auth as i } from "../common.js";
7
- import { getParticipationByToken as v, getContest as D, getStudentByHash as h, getRandomVariant as A } from "./queries.js";
8
- function C(d, t) {
9
- const e = k(T(d, { userData: t }));
10
- return Buffer.from(e).toString("base64url");
1
+ import D, { createHash as I } from "node:crypto";
2
+ import { studentSchema as u, getNormalizedUserData as w } from "@olinfo/quizms/models";
3
+ import { TRPCError as r } from "@trpc/server";
4
+ import { Timestamp as i } from "firebase-admin/firestore";
5
+ import * as l from "firebase-functions/logger";
6
+ import s from "zod";
7
+ import { db as p, auth as m } from "../common.js";
8
+ import { getParticipationByToken as h, 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").update(w(o, { userData: t })).digest().toString("base64url");
11
12
  }
12
- const b = async (d, t) => {
13
- I.info("Student login request received", { data: t });
14
- const e = await v(t.token);
15
- if (!e)
16
- return { success: !1, errorCode: "INVALID_TOKEN", error: "Codice prova non valido" };
17
- if (e.contestId !== t.contestId)
18
- return {
19
- success: !1,
20
- errorCode: "TOKEN_CONTEST_MISMATCH",
21
- error: "Il token proviene da una gara diversa"
22
- };
23
- const c = await D(e.contestId);
24
- let r;
25
- try {
26
- r = Object.fromEntries(
27
- c.userData.map((n) => {
28
- const m = w(t.userData[n.name], n);
29
- if (m == null) throw new Error("Missing field");
30
- return [n.name, m];
31
- })
32
- );
33
- } catch (n) {
34
- return I.error("Invalid user data", { error: n }), {
35
- success: !1,
36
- errorCode: "INVALID_USER_DATA",
37
- error: "Dati non validi"
38
- };
39
- }
40
- const o = g.randomUUID(), u = C(c, r), a = await h(e.id, u);
41
- if (a)
42
- return a.token !== t.token ? {
43
- success: !1,
44
- errorCode: "MULTIPLE_PARTICIPATION",
45
- error: "Hai già partecipato a questa gara"
46
- } : (await f.doc(`studentRestores/${o}`).set({
47
- studentId: a.id,
48
- participationId: a.participationId,
13
+ const U = C.input(
14
+ s.strictObject({
15
+ contestId: s.string(),
16
+ token: s.string(),
17
+ userData: u.shape.userData.unwrap().required(),
18
+ extraData: u.shape.extraData.unwrap()
19
+ })
20
+ ).mutation(async (o) => {
21
+ const { input: t } = o;
22
+ l.info("Student login request received", { data: t });
23
+ const a = await h(t.token);
24
+ if (!a)
25
+ throw new r({ code: "BAD_REQUEST", message: "Codice prova non valido" });
26
+ if (a.contestId !== t.contestId)
27
+ throw new r({ code: "FORBIDDEN", message: "Codice prova non valido" });
28
+ const g = await k(a.contestId), n = D.randomUUID(), d = R(g, t.userData), e = await v(a.id, d);
29
+ if (e) {
30
+ if (e.token !== t.token)
31
+ throw new r({
32
+ code: "FORBIDDEN",
33
+ message: "Hai già partecipato a questa gara"
34
+ });
35
+ return await p.doc(`studentRestores/${n}`).set({
36
+ studentId: e.id,
37
+ participationId: e.participationId,
49
38
  token: t.token,
50
- name: r.name,
51
- surname: r.surname,
39
+ name: t.userData.name,
40
+ surname: t.userData.surname,
52
41
  approvalCode: Math.round(Math.random() * 1e3),
53
42
  status: "pending",
54
- createdAt: s.now()
55
- }), { success: !0, data: { token: await i.createCustomToken(o, {
43
+ createdAt: i.now()
44
+ }), m.createCustomToken(n, {
56
45
  role: "student",
57
- studentId: a.id,
58
- participationId: a.participationId,
59
- variant: a.variant
60
- }) } });
61
- const p = await A(t.contestId), l = await f.collection(`participations/${e.id}/students`).add({
62
- uid: o,
63
- userData: r,
64
- userDataHash: u,
46
+ studentId: e.id,
47
+ participationId: e.participationId,
48
+ variant: e.variant
49
+ });
50
+ }
51
+ const c = await T(t.contestId), f = await p.collection(`participations/${a.id}/students`).add({
52
+ uid: n,
53
+ userData: t.userData,
54
+ userDataHash: d,
65
55
  absent: !1,
66
56
  disabled: !1,
67
- participationId: e.id,
57
+ participationId: a.id,
68
58
  contestId: t.contestId,
69
59
  token: t.token,
70
- startedAt: e.startingTime,
71
- finishedAt: e.endingTime,
72
- variant: p.id,
60
+ startedAt: a.startingTime,
61
+ finishedAt: a.endingTime,
62
+ variant: c.id,
73
63
  answers: {},
74
64
  extraData: t.extraData,
75
- createdAt: s.now(),
76
- updatedAt: s.now()
65
+ createdAt: i.now(),
66
+ updatedAt: i.now()
77
67
  });
78
- return await i.createUser({ uid: o, displayName: `${r.name} ${r.surname}` }), { success: !0, data: { token: await i.createCustomToken(o, {
68
+ return m.createCustomToken(n, {
79
69
  role: "student",
80
- studentId: l.id,
81
- participationId: e.id,
82
- variant: p
83
- }) } };
84
- };
70
+ studentId: f.id,
71
+ participationId: a.id,
72
+ variant: c
73
+ });
74
+ });
85
75
  export {
86
- b as studentLogin
76
+ U as studentLogin
87
77
  };
88
78
  //# sourceMappingURL=student.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"student.js","sources":["../../../../src/functions/api/student.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport {\n type Contest,\n getNormalizedUserData,\n parseUserData,\n type Student,\n} from \"@olinfo/quizms/models\";\nimport { Timestamp } from \"firebase-admin/firestore\";\nimport * as logger from \"firebase-functions/logger\";\n\nimport { auth, db, type Endpoint } from \"../common\";\nimport { getContest, getParticipationByToken, getRandomVariant, getStudentByHash } from \"./queries\";\nimport type { StudentLogin } from \"./schema\";\n\nfunction studentHash(contest: Contest, userData: NonNullable<Student[\"userData\"]>) {\n const hash = sha256(getNormalizedUserData(contest, { userData }));\n return Buffer.from(hash).toString(\"base64url\");\n}\n\nexport const studentLogin: Endpoint<StudentLogin> = async (_request, data) => {\n logger.info(\"Student login request received\", { data });\n\n const participation = await getParticipationByToken(data.token);\n if (!participation) {\n return { success: false, errorCode: \"INVALID_TOKEN\", error: \"Codice prova non valido\" };\n }\n if (participation.contestId !== data.contestId) {\n return {\n success: false,\n errorCode: \"TOKEN_CONTEST_MISMATCH\",\n error: \"Il token proviene da una gara diversa\",\n };\n }\n\n const contest = await getContest(participation.contestId);\n\n let userData: NonNullable<Student[\"userData\"]>;\n try {\n userData = Object.fromEntries(\n contest.userData.map((field) => {\n const value = parseUserData(data.userData[field.name], field);\n if (value == null) throw new Error(\"Missing field\");\n return [field.name, value];\n }),\n );\n } catch (error) {\n logger.error(\"Invalid user data\", { error });\n return {\n success: false,\n errorCode: \"INVALID_USER_DATA\",\n error: \"Dati non validi\",\n };\n }\n\n const uid = crypto.randomUUID();\n const userDataHash = studentHash(contest, userData);\n\n const duplicated = await getStudentByHash(participation.id, userDataHash);\n if (duplicated) {\n if (duplicated.token !== data.token) {\n return {\n success: false,\n errorCode: \"MULTIPLE_PARTICIPATION\",\n error: \"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: userData.name,\n surname: userData.surname,\n approvalCode: Math.round(Math.random() * 1000),\n status: \"pending\",\n createdAt: Timestamp.now(),\n });\n\n const token = await auth.createCustomToken(uid, {\n role: \"student\",\n studentId: duplicated.id,\n participationId: duplicated.participationId,\n variant: duplicated.variant,\n });\n return { success: true, data: { token } };\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,\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 await auth.createUser({ uid, displayName: `${userData.name} ${userData.surname}` });\n const token = await auth.createCustomToken(uid, {\n role: \"student\",\n studentId: studentRef.id,\n participationId: participation.id,\n variant,\n });\n return { success: true, data: { token } };\n};\n"],"names":["studentHash","contest","userData","hash","sha256","getNormalizedUserData","studentLogin","_request","data","logger","participation","getParticipationByToken","getContest","field","value","parseUserData","error","uid","crypto","userDataHash","duplicated","getStudentByHash","db","Timestamp","auth","variant","getRandomVariant","studentRef"],"mappings":";;;;;;;AAgBA,SAASA,EAAYC,GAAkBC,GAA4C;AACjF,QAAMC,IAAOC,EAAOC,EAAsBJ,GAAS,EAAE,UAAAC,EAAA,CAAU,CAAC;AAChE,SAAO,OAAO,KAAKC,CAAI,EAAE,SAAS,WAAW;AAC/C;AAEO,MAAMG,IAAuC,OAAOC,GAAUC,MAAS;AAC5E,EAAAC,EAAO,KAAK,kCAAkC,EAAE,MAAAD,EAAA,CAAM;AAEtD,QAAME,IAAgB,MAAMC,EAAwBH,EAAK,KAAK;AAC9D,MAAI,CAACE;AACH,WAAO,EAAE,SAAS,IAAO,WAAW,iBAAiB,OAAO,0BAAA;AAE9D,MAAIA,EAAc,cAAcF,EAAK;AACnC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX,OAAO;AAAA,IAAA;AAIX,QAAMP,IAAU,MAAMW,EAAWF,EAAc,SAAS;AAExD,MAAIR;AACJ,MAAI;AACF,IAAAA,IAAW,OAAO;AAAA,MAChBD,EAAQ,SAAS,IAAI,CAACY,MAAU;AAC9B,cAAMC,IAAQC,EAAcP,EAAK,SAASK,EAAM,IAAI,GAAGA,CAAK;AAC5D,YAAIC,KAAS,KAAM,OAAM,IAAI,MAAM,eAAe;AAClD,eAAO,CAACD,EAAM,MAAMC,CAAK;AAAA,MAC3B,CAAC;AAAA,IAAA;AAAA,EAEL,SAASE,GAAO;AACd,WAAAP,EAAO,MAAM,qBAAqB,EAAE,OAAAO,EAAA,CAAO,GACpC;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,QAAMC,IAAMC,EAAO,WAAA,GACbC,IAAenB,EAAYC,GAASC,CAAQ,GAE5CkB,IAAa,MAAMC,EAAiBX,EAAc,IAAIS,CAAY;AACxE,MAAIC;AACF,WAAIA,EAAW,UAAUZ,EAAK,QACrB;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX,OAAO;AAAA,IAAA,KAIX,MAAMc,EAAG,IAAI,mBAAmBL,CAAG,EAAE,EAAE,IAAI;AAAA,MACzC,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,OAAOZ,EAAK;AAAA,MACZ,MAAMN,EAAS;AAAA,MACf,SAASA,EAAS;AAAA,MAClB,cAAc,KAAK,MAAM,KAAK,OAAA,IAAW,GAAI;AAAA,MAC7C,QAAQ;AAAA,MACR,WAAWqB,EAAU,IAAA;AAAA,IAAI,CAC1B,GAQM,EAAE,SAAS,IAAM,MAAM,EAAE,OANlB,MAAMC,EAAK,kBAAkBP,GAAK;AAAA,MAC9C,MAAM;AAAA,MACN,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,SAASA,EAAW;AAAA,IAAA,CACrB,IACqC;AAGxC,QAAMK,IAAU,MAAMC,EAAiBlB,EAAK,SAAS,GAE/CmB,IAAa,MAAML,EAAG,WAAW,kBAAkBZ,EAAc,EAAE,WAAW,EAAE,IAAI;AAAA,IACxF,KAAAO;AAAA,IACA,UAAAf;AAAA,IACA,cAAAiB;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiBT,EAAc;AAAA,IAC/B,WAAWF,EAAK;AAAA,IAChB,OAAOA,EAAK;AAAA,IACZ,WAAWE,EAAc;AAAA,IACzB,YAAYA,EAAc;AAAA,IAC1B,SAASe,EAAQ;AAAA,IACjB,SAAS,CAAA;AAAA,IACT,WAAWjB,EAAK;AAAA,IAChB,WAAWe,EAAU,IAAA;AAAA,IACrB,WAAWA,EAAU,IAAA;AAAA,EAAI,CAC1B;AAED,eAAMC,EAAK,WAAW,EAAE,KAAAP,GAAK,aAAa,GAAGf,EAAS,IAAI,IAAIA,EAAS,OAAO,GAAA,CAAI,GAO3E,EAAE,SAAS,IAAM,MAAM,EAAE,OANlB,MAAMsB,EAAK,kBAAkBP,GAAK;AAAA,IAC9C,MAAM;AAAA,IACN,WAAWU,EAAW;AAAA,IACtB,iBAAiBjB,EAAc;AAAA,IAC/B,SAAAe;AAAA,EAAA,CACD,IACqC;AACxC;"}
1
+ {"version":3,"file":"student.js","sources":["../../../../src/functions/api/student.ts"],"sourcesContent":["import crypto, { createHash } 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 createHash(\"sha256\")\n .update(getNormalizedUserData(contest, { userData }))\n .digest()\n .toString(\"base64url\");\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","createHash","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,EAAW,QAAQ,EACvB,OAAOC,EAAsBH,GAAS,EAAE,UAAAC,EAAA,CAAU,CAAC,EACnD,SACA,SAAS,WAAW;AACzB;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;"}