@olinfo/quizms-firebase 5.0.0 → 5.0.2

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.
@@ -1,4 +1,4 @@
1
- const e = "5.0.0", n = { "firebase-functions": "^7.1.0" }, s = {
1
+ const e = "5.0.2", n = { "firebase-functions": "^7.3.2" }, s = {
2
2
  version: e,
3
3
  dependencies: n
4
4
  };
@@ -63,7 +63,7 @@ async function H(t, e) {
63
63
  for (const r of o) {
64
64
  if (!N.isMatch(a.id, r.contestIds)) continue;
65
65
  let c = [];
66
- a.hasVariants && (r.pdfVariants ? c = r.pdfVariants.map((d) => `${a.id}-${d}`) : c = new x(`${a.secret}-${a.id}-${r.id}-venue`).sample(a.pdfVariantIds, a.pdfPerSchool)), i.push({
66
+ a.hasVariants && (r.pdfVariants.length > 0 ? c = r.pdfVariants.map((d) => `${a.id}-${d}`) : c = new x(`${a.secret}-${a.id}-${r.id}-venue`).sample(a.pdfVariantIds, a.pdfPerSchool)), i.push({
67
67
  id: `${r.id}-${a.id}`,
68
68
  schoolId: r.id,
69
69
  contestId: a.id,
@@ -112,7 +112,7 @@ async function X(t, e) {
112
112
  const a = y([...i.variantIds, ...i.pdfVariantIds]).map(
113
113
  async (r) => {
114
114
  const c = m.join("variants", i.id, r), d = m.join("statements", i.id, r);
115
- return (await O("*.{pdf,js,css}", { cwd: c })).flatMap((l) => {
115
+ return (await O("*.{pdf,css,txt,mjs}", { cwd: c })).flatMap((l) => {
116
116
  const h = m.extname(l);
117
117
  return [
118
118
  [
@@ -1 +1 @@
1
- {"version":3,"file":"import.js","sources":["../../../src/cli/import.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport {\n contestSchema,\n studentSchema,\n type Venue,\n variantSchema,\n variantsConfigSchema,\n venueSchema,\n} from \"@olinfo/quizms/models\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { fatal, load, loadContests, success, warning } from \"@olinfo/quizms/utils-node\";\nimport { deleteApp } from \"firebase-admin/app\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { groupBy, uniq } from \"lodash-es\";\nimport picomatch from \"picomatch\";\nimport { glob } from \"tinyglobby\";\nimport z from \"zod\";\n\nimport { userSchema } from \"~/models/user\";\nimport { websiteSchema } from \"~/models/website\";\n\nimport { importCollection } from \"./utils/collection\";\nimport {\n contestConverter,\n studentConverter,\n variantConverter,\n venueConverter,\n websiteConverter,\n} from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { importStorage } from \"./utils/storage\";\nimport { importUsers } from \"./utils/users\";\n\ntype ImportOptions = {\n config: string;\n skipExisting?: true;\n delete?: true;\n force?: true;\n\n admins?: true;\n contests?: true;\n schools?: true;\n statements?: true;\n students?: true;\n teachers?: true;\n variants?: true;\n websites?: true;\n};\n\nexport default async function importData(options: ImportOptions) {\n if (!existsSync(\"data\")) {\n fatal(\"Cannot find data directory. Make sure you're inside a QuizMS project.\");\n }\n\n const collections: (keyof ImportOptions)[] = [\n \"admins\",\n \"contests\",\n \"schools\",\n \"statements\",\n \"students\",\n \"teachers\",\n \"variants\",\n \"websites\",\n ];\n if (collections.every((key) => !options[key])) {\n warning(\"`Nothing to import. Use `--help` for usage.`\");\n return;\n }\n\n const { app, bucket, db } = await initializeFirebase();\n\n if (options.admins) {\n await importAdmins(db, options);\n }\n if (options.contests) {\n await importContests(db, options);\n }\n if (options.schools || options.teachers) {\n await importVenues(db, options);\n }\n if (options.students) {\n await importStudents(db, options);\n }\n if (options.variants) {\n await importVariants(db, options);\n }\n if (options.websites) {\n await importWebsites(db, options);\n }\n if (options.statements) {\n await importStatements(bucket, options);\n }\n\n success(\"All done!\");\n await deleteApp(app);\n}\n\nasync function importAdmins(db: Firestore, options: ImportOptions) {\n const users = await load(\"admins\", userSchema.omit({ role: true }));\n await importUsers(db, \"admin\", users, options);\n}\n\nasync function importContests(db: Firestore, options: ImportOptions) {\n const contests = await load(\"contests\", contestSchema);\n await importCollection(db, \"contests\", contests, contestConverter, options);\n}\n\nasync function importVenues(db: Firestore, options: ImportOptions) {\n const schoolSchema = venueSchema\n .omit({\n schoolId: true,\n contestId: true,\n })\n .extend({\n contestIds: z.union([z.string(), z.array(z.string())]).default(\"*\"),\n password: z.string(),\n });\n const schools = await load(\"schools\", schoolSchema);\n const contests = await loadContests();\n\n if (options.teachers) {\n const teachers = schools.map((school) => ({\n name: school.name,\n username: school.id,\n password: school.password,\n }));\n await importUsers(db, \"teacher\", teachers, options);\n }\n\n if (options.schools) {\n const venues: Venue[] = [];\n\n for (const contest of contests) {\n for (const school of schools) {\n if (!picomatch.isMatch(contest.id, school.contestIds)) continue;\n\n let pdfVariants: string[] = [];\n if (contest.hasVariants) {\n if (school.pdfVariants) {\n pdfVariants = school.pdfVariants.map((id) => `${contest.id}-${id}`);\n } else {\n const rng = new Rng(`${contest.secret}-${contest.id}-${school.id}-venue`);\n pdfVariants = rng.sample(contest.pdfVariantIds, contest.pdfPerSchool);\n }\n }\n\n venues.push({\n id: `${school.id}-${contest.id}`,\n schoolId: school.id,\n contestId: contest.id,\n name: school.name,\n finalized: false,\n pdfVariants,\n disabled: false,\n });\n }\n }\n\n await importCollection(db, \"venues\", venues, venueConverter, options);\n }\n}\n\nasync function importStudents(db: Firestore, options: ImportOptions) {\n const students = await load(\"students\", studentSchema);\n const venues = groupBy(students, \"venueId\");\n await Promise.all(\n Object.entries(venues).map(([venueId, students]) =>\n importCollection(db, `venues/${venueId}/students`, students, studentConverter, options),\n ),\n );\n}\n\nasync function importVariants(db: Firestore, options: ImportOptions) {\n const variantsConfig = await load(\"variants\", variantsConfigSchema);\n const variants = await Promise.all(\n variantsConfig.flatMap((config) => {\n const ids = uniq([...config.variantIds, ...config.pdfVariantIds]);\n return ids.map(async (id) => {\n const fileName = path.join(\"variants\", config.id, id, \"answers.json\");\n let schema: string;\n try {\n schema = await readFile(fileName, \"utf8\");\n } catch {\n fatal(`Cannot find schema for variant ${id}. Use \\`quizms variants\\` to generate it.`);\n }\n try {\n return validate(variantSchema, JSON.parse(schema));\n } catch (err) {\n fatal(`Invalid schema for variant ${id}: ${err}`);\n }\n });\n }),\n );\n await importCollection(db, \"variants\", variants, variantConverter, options);\n}\n\nasync function importWebsites(db: Firestore, options: ImportOptions) {\n const websites = await load(\"websites\", websiteSchema);\n await importCollection(db, \"websites\", websites, websiteConverter, options);\n}\n\nasync function importStatements(bucket: Bucket, options: ImportOptions) {\n const contests = await loadContests();\n\n const timestamp = new Date().toISOString().replace(/:/g, \"-\");\n\n const statements = contests.map(async (config) => {\n const files = uniq([...config.variantIds, ...config.pdfVariantIds]).map(\n async (id): Promise<[string, string][]> => {\n const localDir = path.join(\"variants\", config.id, id);\n const remoteDir = path.join(\"statements\", config.id, id);\n const files = await glob(\"*.{pdf,js,css}\", { cwd: localDir });\n\n return files.flatMap((file) => {\n const ext = path.extname(file);\n return [\n [\n path.join(localDir, file),\n path.join(remoteDir, `${path.basename(file, ext)}-${timestamp}${ext}`),\n ],\n [path.join(localDir, file), path.join(remoteDir, file)],\n ];\n });\n },\n );\n return (await Promise.all(files)).flat();\n });\n await importStorage(bucket, \"statements\", (await Promise.all(statements)).flat(), options);\n}\n"],"names":["importData","options","existsSync","fatal","key","warning","app","bucket","db","initializeFirebase","importAdmins","importContests","importVenues","importStudents","importVariants","importWebsites","importStatements","success","deleteApp","users","load","userSchema","importUsers","contests","contestSchema","importCollection","contestConverter","schoolSchema","venueSchema","z","schools","loadContests","teachers","school","venues","contest","picomatch","pdfVariants","id","Rng","venueConverter","students","studentSchema","groupBy","venueId","studentConverter","variantsConfig","variantsConfigSchema","variants","config","uniq","fileName","path","schema","readFile","validate","variantSchema","err","variantConverter","websites","websiteSchema","websiteConverter","timestamp","statements","files","localDir","remoteDir","glob","file","ext","importStorage"],"mappings":"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"}
1
+ {"version":3,"file":"import.js","sources":["../../../src/cli/import.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport {\n contestSchema,\n studentSchema,\n type Venue,\n variantSchema,\n variantsConfigSchema,\n venueSchema,\n} from \"@olinfo/quizms/models\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { fatal, load, loadContests, success, warning } from \"@olinfo/quizms/utils-node\";\nimport { deleteApp } from \"firebase-admin/app\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { groupBy, uniq } from \"lodash-es\";\nimport picomatch from \"picomatch\";\nimport { glob } from \"tinyglobby\";\nimport z from \"zod\";\n\nimport { userSchema } from \"~/models/user\";\nimport { websiteSchema } from \"~/models/website\";\n\nimport { importCollection } from \"./utils/collection\";\nimport {\n contestConverter,\n studentConverter,\n variantConverter,\n venueConverter,\n websiteConverter,\n} from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { importStorage } from \"./utils/storage\";\nimport { importUsers } from \"./utils/users\";\n\ntype ImportOptions = {\n config: string;\n skipExisting?: true;\n delete?: true;\n force?: true;\n\n admins?: true;\n contests?: true;\n schools?: true;\n statements?: true;\n students?: true;\n teachers?: true;\n variants?: true;\n websites?: true;\n};\n\nexport default async function importData(options: ImportOptions) {\n if (!existsSync(\"data\")) {\n fatal(\"Cannot find data directory. Make sure you're inside a QuizMS project.\");\n }\n\n const collections: (keyof ImportOptions)[] = [\n \"admins\",\n \"contests\",\n \"schools\",\n \"statements\",\n \"students\",\n \"teachers\",\n \"variants\",\n \"websites\",\n ];\n if (collections.every((key) => !options[key])) {\n warning(\"`Nothing to import. Use `--help` for usage.`\");\n return;\n }\n\n const { app, bucket, db } = await initializeFirebase();\n\n if (options.admins) {\n await importAdmins(db, options);\n }\n if (options.contests) {\n await importContests(db, options);\n }\n if (options.schools || options.teachers) {\n await importVenues(db, options);\n }\n if (options.students) {\n await importStudents(db, options);\n }\n if (options.variants) {\n await importVariants(db, options);\n }\n if (options.websites) {\n await importWebsites(db, options);\n }\n if (options.statements) {\n await importStatements(bucket, options);\n }\n\n success(\"All done!\");\n await deleteApp(app);\n}\n\nasync function importAdmins(db: Firestore, options: ImportOptions) {\n const users = await load(\"admins\", userSchema.omit({ role: true }));\n await importUsers(db, \"admin\", users, options);\n}\n\nasync function importContests(db: Firestore, options: ImportOptions) {\n const contests = await load(\"contests\", contestSchema);\n await importCollection(db, \"contests\", contests, contestConverter, options);\n}\n\nasync function importVenues(db: Firestore, options: ImportOptions) {\n const schoolSchema = venueSchema\n .omit({\n schoolId: true,\n contestId: true,\n })\n .extend({\n contestIds: z.union([z.string(), z.array(z.string())]).default(\"*\"),\n password: z.string(),\n });\n const schools = await load(\"schools\", schoolSchema);\n const contests = await loadContests();\n\n if (options.teachers) {\n const teachers = schools.map((school) => ({\n name: school.name,\n username: school.id,\n password: school.password,\n }));\n await importUsers(db, \"teacher\", teachers, options);\n }\n\n if (options.schools) {\n const venues: Venue[] = [];\n\n for (const contest of contests) {\n for (const school of schools) {\n if (!picomatch.isMatch(contest.id, school.contestIds)) continue;\n\n let pdfVariants: string[] = [];\n if (contest.hasVariants) {\n if (school.pdfVariants.length > 0) {\n pdfVariants = school.pdfVariants.map((id) => `${contest.id}-${id}`);\n } else {\n const rng = new Rng(`${contest.secret}-${contest.id}-${school.id}-venue`);\n pdfVariants = rng.sample(contest.pdfVariantIds, contest.pdfPerSchool);\n }\n }\n\n venues.push({\n id: `${school.id}-${contest.id}`,\n schoolId: school.id,\n contestId: contest.id,\n name: school.name,\n finalized: false,\n pdfVariants,\n disabled: false,\n });\n }\n }\n\n await importCollection(db, \"venues\", venues, venueConverter, options);\n }\n}\n\nasync function importStudents(db: Firestore, options: ImportOptions) {\n const students = await load(\"students\", studentSchema);\n const venues = groupBy(students, \"venueId\");\n await Promise.all(\n Object.entries(venues).map(([venueId, students]) =>\n importCollection(db, `venues/${venueId}/students`, students, studentConverter, options),\n ),\n );\n}\n\nasync function importVariants(db: Firestore, options: ImportOptions) {\n const variantsConfig = await load(\"variants\", variantsConfigSchema);\n const variants = await Promise.all(\n variantsConfig.flatMap((config) => {\n const ids = uniq([...config.variantIds, ...config.pdfVariantIds]);\n return ids.map(async (id) => {\n const fileName = path.join(\"variants\", config.id, id, \"answers.json\");\n let schema: string;\n try {\n schema = await readFile(fileName, \"utf8\");\n } catch {\n fatal(`Cannot find schema for variant ${id}. Use \\`quizms variants\\` to generate it.`);\n }\n try {\n return validate(variantSchema, JSON.parse(schema));\n } catch (err) {\n fatal(`Invalid schema for variant ${id}: ${err}`);\n }\n });\n }),\n );\n await importCollection(db, \"variants\", variants, variantConverter, options);\n}\n\nasync function importWebsites(db: Firestore, options: ImportOptions) {\n const websites = await load(\"websites\", websiteSchema);\n await importCollection(db, \"websites\", websites, websiteConverter, options);\n}\n\nasync function importStatements(bucket: Bucket, options: ImportOptions) {\n const contests = await loadContests();\n\n const timestamp = new Date().toISOString().replace(/:/g, \"-\");\n\n const statements = contests.map(async (config) => {\n const files = uniq([...config.variantIds, ...config.pdfVariantIds]).map(\n async (id): Promise<[string, string][]> => {\n const localDir = path.join(\"variants\", config.id, id);\n const remoteDir = path.join(\"statements\", config.id, id);\n const files = await glob(\"*.{pdf,css,txt,mjs}\", { cwd: localDir });\n\n return files.flatMap((file) => {\n const ext = path.extname(file);\n return [\n [\n path.join(localDir, file),\n path.join(remoteDir, `${path.basename(file, ext)}-${timestamp}${ext}`),\n ],\n [path.join(localDir, file), path.join(remoteDir, file)],\n ];\n });\n },\n );\n return (await Promise.all(files)).flat();\n });\n await importStorage(bucket, \"statements\", (await Promise.all(statements)).flat(), options);\n}\n"],"names":["importData","options","existsSync","fatal","key","warning","app","bucket","db","initializeFirebase","importAdmins","importContests","importVenues","importStudents","importVariants","importWebsites","importStatements","success","deleteApp","users","load","userSchema","importUsers","contests","contestSchema","importCollection","contestConverter","schoolSchema","venueSchema","z","schools","loadContests","teachers","school","venues","contest","picomatch","pdfVariants","id","Rng","venueConverter","students","studentSchema","groupBy","venueId","studentConverter","variantsConfig","variantsConfigSchema","variants","config","uniq","fileName","path","schema","readFile","validate","variantSchema","err","variantConverter","websites","websiteSchema","websiteConverter","timestamp","statements","files","localDir","remoteDir","glob","file","ext","importStorage"],"mappings":";;;;;;;;;;;;;;;;;;AAqDA,eAA8BA,GAAWC,GAAwB;AAe/D,MAdKC,EAAW,MAAM,KACpBC,EAAM,uEAAuE,GAGlC;AAAA,IAC3C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,EAEc,MAAM,CAACC,MAAQ,CAACH,EAAQG,CAAG,CAAC,GAAG;AAC7C,IAAAC,EAAQ,8CAA8C;AACtD;AAAA,EACF;AAEA,QAAM,EAAE,KAAAC,GAAK,QAAAC,GAAQ,IAAAC,EAAA,IAAO,MAAMC,EAAA;AAElC,EAAIR,EAAQ,UACV,MAAMS,EAAaF,GAAIP,CAAO,GAE5BA,EAAQ,YACV,MAAMU,EAAeH,GAAIP,CAAO,IAE9BA,EAAQ,WAAWA,EAAQ,aAC7B,MAAMW,EAAaJ,GAAIP,CAAO,GAE5BA,EAAQ,YACV,MAAMY,EAAeL,GAAIP,CAAO,GAE9BA,EAAQ,YACV,MAAMa,EAAeN,GAAIP,CAAO,GAE9BA,EAAQ,YACV,MAAMc,EAAeP,GAAIP,CAAO,GAE9BA,EAAQ,cACV,MAAMe,EAAiBT,GAAQN,CAAO,GAGxCgB,EAAQ,WAAW,GACnB,MAAMC,EAAUZ,CAAG;AACrB;AAEA,eAAeI,EAAaF,GAAeP,GAAwB;AACjE,QAAMkB,IAAQ,MAAMC,EAAK,UAAUC,EAAW,KAAK,EAAE,MAAM,GAAA,CAAM,CAAC;AAClE,QAAMC,EAAYd,GAAI,SAASW,GAAOlB,CAAO;AAC/C;AAEA,eAAeU,EAAeH,GAAeP,GAAwB;AACnE,QAAMsB,IAAW,MAAMH,EAAK,YAAYI,CAAa;AACrD,QAAMC,EAAiBjB,GAAI,YAAYe,GAAUG,GAAkBzB,CAAO;AAC5E;AAEA,eAAeW,EAAaJ,GAAeP,GAAwB;AACjE,QAAM0B,IAAeC,EAClB,KAAK;AAAA,IACJ,UAAU;AAAA,IACV,WAAW;AAAA,EAAA,CACZ,EACA,OAAO;AAAA,IACN,YAAYC,EAAE,MAAM,CAACA,EAAE,UAAUA,EAAE,MAAMA,EAAE,QAAQ,CAAC,CAAC,EAAE,QAAQ,GAAG;AAAA,IAClE,UAAUA,EAAE,OAAA;AAAA,EAAO,CACpB,GACGC,IAAU,MAAMV,EAAK,WAAWO,CAAY,GAC5CJ,IAAW,MAAMQ,EAAA;AAEvB,MAAI9B,EAAQ,UAAU;AACpB,UAAM+B,IAAWF,EAAQ,IAAI,CAACG,OAAY;AAAA,MACxC,MAAMA,EAAO;AAAA,MACb,UAAUA,EAAO;AAAA,MACjB,UAAUA,EAAO;AAAA,IAAA,EACjB;AACF,UAAMX,EAAYd,GAAI,WAAWwB,GAAU/B,CAAO;AAAA,EACpD;AAEA,MAAIA,EAAQ,SAAS;AACnB,UAAMiC,IAAkB,CAAA;AAExB,eAAWC,KAAWZ;AACpB,iBAAWU,KAAUH,GAAS;AAC5B,YAAI,CAACM,EAAU,QAAQD,EAAQ,IAAIF,EAAO,UAAU,EAAG;AAEvD,YAAII,IAAwB,CAAA;AAC5B,QAAIF,EAAQ,gBACNF,EAAO,YAAY,SAAS,IAC9BI,IAAcJ,EAAO,YAAY,IAAI,CAACK,MAAO,GAAGH,EAAQ,EAAE,IAAIG,CAAE,EAAE,IAGlED,IADY,IAAIE,EAAI,GAAGJ,EAAQ,MAAM,IAAIA,EAAQ,EAAE,IAAIF,EAAO,EAAE,QAAQ,EACtD,OAAOE,EAAQ,eAAeA,EAAQ,YAAY,IAIxED,EAAO,KAAK;AAAA,UACV,IAAI,GAAGD,EAAO,EAAE,IAAIE,EAAQ,EAAE;AAAA,UAC9B,UAAUF,EAAO;AAAA,UACjB,WAAWE,EAAQ;AAAA,UACnB,MAAMF,EAAO;AAAA,UACb,WAAW;AAAA,UACX,aAAAI;AAAA,UACA,UAAU;AAAA,QAAA,CACX;AAAA,MACH;AAGF,UAAMZ,EAAiBjB,GAAI,UAAU0B,GAAQM,GAAgBvC,CAAO;AAAA,EACtE;AACF;AAEA,eAAeY,EAAeL,GAAeP,GAAwB;AACnE,QAAMwC,IAAW,MAAMrB,EAAK,YAAYsB,CAAa,GAC/CR,IAASS,EAAQF,GAAU,SAAS;AAC1C,QAAM,QAAQ;AAAA,IACZ,OAAO,QAAQP,CAAM,EAAE;AAAA,MAAI,CAAC,CAACU,GAASH,CAAQ,MAC5ChB,EAAiBjB,GAAI,UAAUoC,CAAO,aAAaH,GAAUI,GAAkB5C,CAAO;AAAA,IAAA;AAAA,EACxF;AAEJ;AAEA,eAAea,EAAeN,GAAeP,GAAwB;AACnE,QAAM6C,IAAiB,MAAM1B,EAAK,YAAY2B,CAAoB,GAC5DC,IAAW,MAAM,QAAQ;AAAA,IAC7BF,EAAe,QAAQ,CAACG,MACVC,EAAK,CAAC,GAAGD,EAAO,YAAY,GAAGA,EAAO,aAAa,CAAC,EACrD,IAAI,OAAOX,MAAO;AAC3B,YAAMa,IAAWC,EAAK,KAAK,YAAYH,EAAO,IAAIX,GAAI,cAAc;AACpE,UAAIe;AACJ,UAAI;AACF,QAAAA,IAAS,MAAMC,EAASH,GAAU,MAAM;AAAA,MAC1C,QAAQ;AACN,QAAAhD,EAAM,kCAAkCmC,CAAE,2CAA2C;AAAA,MACvF;AACA,UAAI;AACF,eAAOiB,EAASC,GAAe,KAAK,MAAMH,CAAM,CAAC;AAAA,MACnD,SAASI,GAAK;AACZ,QAAAtD,EAAM,8BAA8BmC,CAAE,KAAKmB,CAAG,EAAE;AAAA,MAClD;AAAA,IACF,CAAC,CACF;AAAA,EAAA;AAEH,QAAMhC,EAAiBjB,GAAI,YAAYwC,GAAUU,GAAkBzD,CAAO;AAC5E;AAEA,eAAec,EAAeP,GAAeP,GAAwB;AACnE,QAAM0D,IAAW,MAAMvC,EAAK,YAAYwC,CAAa;AACrD,QAAMnC,EAAiBjB,GAAI,YAAYmD,GAAUE,GAAkB5D,CAAO;AAC5E;AAEA,eAAee,EAAiBT,GAAgBN,GAAwB;AACtE,QAAMsB,IAAW,MAAMQ,EAAA,GAEjB+B,yBAAgB,KAAA,GAAO,cAAc,QAAQ,MAAM,GAAG,GAEtDC,IAAaxC,EAAS,IAAI,OAAO0B,MAAW;AAChD,UAAMe,IAAQd,EAAK,CAAC,GAAGD,EAAO,YAAY,GAAGA,EAAO,aAAa,CAAC,EAAE;AAAA,MAClE,OAAOX,MAAoC;AACzC,cAAM2B,IAAWb,EAAK,KAAK,YAAYH,EAAO,IAAIX,CAAE,GAC9C4B,IAAYd,EAAK,KAAK,cAAcH,EAAO,IAAIX,CAAE;AAGvD,gBAFc,MAAM6B,EAAK,uBAAuB,EAAE,KAAKF,GAAU,GAEpD,QAAQ,CAACG,MAAS;AAC7B,gBAAMC,IAAMjB,EAAK,QAAQgB,CAAI;AAC7B,iBAAO;AAAA,YACL;AAAA,cACEhB,EAAK,KAAKa,GAAUG,CAAI;AAAA,cACxBhB,EAAK,KAAKc,GAAW,GAAGd,EAAK,SAASgB,GAAMC,CAAG,CAAC,IAAIP,CAAS,GAAGO,CAAG,EAAE;AAAA,YAAA;AAAA,YAEvE,CAACjB,EAAK,KAAKa,GAAUG,CAAI,GAAGhB,EAAK,KAAKc,GAAWE,CAAI,CAAC;AAAA,UAAA;AAAA,QAE1D,CAAC;AAAA,MACH;AAAA,IAAA;AAEF,YAAQ,MAAM,QAAQ,IAAIJ,CAAK,GAAG,KAAA;AAAA,EACpC,CAAC;AACD,QAAMM,EAAc/D,GAAQ,eAAe,MAAM,QAAQ,IAAIwD,CAAU,GAAG,KAAA,GAAQ9D,CAAO;AAC3F;"}
@@ -85,7 +85,7 @@ function P(e) {
85
85
  ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
86
86
  rewrites: [
87
87
  {
88
- source: "/api",
88
+ source: "/api/**",
89
89
  function: {
90
90
  functionId: "api",
91
91
  region: e
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../src/cli/init.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { cwd } from \"node:process\";\nimport { styleText } from \"node:util\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm, select } from \"@inquirer/prompts\";\nimport { error, info, success } from \"@olinfo/quizms/utils-node\";\nimport { type App, applicationDefault } from \"firebase-admin/app\";\n\nimport { getGcpRegions } from \"~/cli/utils/region-finder\";\n\nimport firestoreRules from \"./files/firestore.rules?raw\";\nimport firestoreIndexes from \"./files/firestore-indexes.json\";\nimport functionsIndex from \"./files/functions-index.js?raw\";\nimport functionsPackage from \"./files/functions-package\";\nimport storageRules from \"./files/storage.rules?raw\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { restApi, restProjectApi } from \"./utils/rest-api\";\n\ntype InitOptions = {\n force?: boolean;\n};\n\nexport default async function init(options: InitOptions) {\n let initialized = true;\n\n await selectProject();\n const region = await getRegion();\n\n if (await copyFiles(options, region)) initialized = false;\n\n const { app, bucket } = await initializeFirebase();\n if (await enableBackups(app)) initialized = false;\n if (await enableCors(bucket)) initialized = false;\n\n if (initialized) {\n success(\"Project initialized!\");\n } else {\n error(\"The initialization was not completed due to some previous errors.\");\n }\n}\n\nasync function selectProject() {\n const data = await restApi(applicationDefault(), \"firebase\", \"v1beta1\", \"\");\n\n const answer = await select<string>({\n message: \"Seleziona il progetto Firebase\",\n choices: data.results.map((project: any) => ({\n value: project.projectId,\n name: project.displayName,\n })),\n });\n await writeFile(\".firebaserc\", JSON.stringify({ projects: { default: answer } }, null, 2));\n}\n\nasync function getRegion() {\n const regions = await getGcpRegions();\n return select<string>({\n message: \"Seleziona la regione\",\n choices: regions.map((region) => ({\n value: region.region,\n name: `${region.region} (${region.name})`,\n })),\n });\n}\n\nasync function copyFiles(options: InitOptions, region: string) {\n info(\"Copying files...\");\n\n await mkdir(\"firebase/functions\", { recursive: true });\n\n const firestoreRulesPath = \"firebase/firestore.rules\";\n await initFile(firestoreRulesPath, firestoreRules, options);\n\n const firestoreIndexesPath = \"firebase/firestore-indexes.json\";\n await initFile(firestoreIndexesPath, JSON.stringify(firestoreIndexes, undefined, 2), options);\n\n const storageRulesPath = \"firebase/storage.rules\";\n await initFile(storageRulesPath, storageRules, options);\n\n await initFile(\"firebase/functions/.env\", `QUIZMS_REGION=${region}`, options);\n await initFile(\n \"firebase/functions/index.js\",\n functionsIndex.replace(\"QUIZMS_REGION\", region),\n options,\n );\n await initFile(\n \"firebase/functions/package.json\",\n JSON.stringify(functionsPackage, undefined, 2),\n options,\n );\n\n const configs = {\n $schema: \"./node_modules/firebase-tools/schema/firebase-config.json\",\n hosting: hostingConfigs(region),\n firestore: {\n rules: path.relative(cwd(), firestoreRulesPath),\n indexes: path.relative(cwd(), firestoreIndexesPath),\n },\n storage: {\n rules: path.relative(cwd(), storageRulesPath),\n },\n functions: [\n {\n codebase: \"default\",\n disallowLegacyRuntimeConfig: true,\n ignore: [\"node_modules\", \"*.log\"],\n source: \"firebase/functions\",\n runtime: \"nodejs24\",\n },\n ],\n };\n await initFile(\"firebase.json\", JSON.stringify(configs, undefined, 2), options);\n\n success(\"Files copied!\");\n return 0;\n}\n\nfunction hostingConfigs(region: string) {\n return {\n public: \".quizms/hosting-build\",\n ignore: [\"firebase.json\", \"**/.*\", \"**/node_modules/**\"],\n rewrites: [\n {\n source: \"/api\",\n function: {\n functionId: \"api\",\n region,\n },\n },\n {\n source: \"**\",\n destination: \"/index.html\",\n },\n ],\n trailingSlash: false,\n headers: [\n {\n source: \"/assets/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=31536000, immutable\", // 365 days\n },\n ],\n },\n {\n source: \"/blockly/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=86400\", // 1 day\n },\n ],\n },\n ],\n predeploy: \"npx quizms build firebase\",\n };\n}\n\nasync function enableBackups(app: App) {\n info(\"Enabling Firestore backups...\");\n\n try {\n const existingsBackups = await restProjectApi(\n app,\n \"firestore\",\n \"v1\",\n \"/databases/(default)/backupSchedules\",\n );\n const hasDaily = existingsBackups.backupSchedules?.some((s: any) => s.dailyRecurrence);\n if (hasDaily) {\n info(\"Daily backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n dailyRecurrence: {},\n retention: \"604800s\",\n });\n }\n\n const hasWeekly = existingsBackups.backupSchedules?.some((s: any) => s.weeklyRecurrence);\n if (hasWeekly) {\n info(\"Weekly backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n weeklyRecurrence: {\n day: \"SUNDAY\",\n },\n retention: \"8467200s\",\n });\n }\n\n success(\"Backups enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to enable Firestore backups: ${err}`);\n return 1;\n }\n}\n\nasync function enableCors(bucket: Bucket) {\n info(\"Enabling CORS for Firebase bucket...\");\n\n try {\n await bucket.setCorsConfiguration([\n {\n origin: [\"*\"],\n method: [\"GET\"],\n maxAgeSeconds: 3600,\n },\n ]);\n success(\"CORS enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to set CORS configuration: ${err}`);\n return 1;\n }\n}\n\nasync function initFile(fileName: string, content: string, options?: InitOptions) {\n let write = options?.force ?? false;\n if (!write) {\n try {\n const prevContent = await readFile(fileName, \"utf8\");\n if (prevContent === content) return;\n } catch {\n write = true;\n }\n }\n\n if (!write) {\n write = await confirm({\n message: `The file ${styleText(\"bold\", path.basename(fileName))} already exists. Do you want to overwrite it?`,\n });\n }\n\n if (write) {\n await writeFile(fileName, content);\n }\n}\n"],"names":["init","options","initialized","selectProject","region","getRegion","copyFiles","app","bucket","initializeFirebase","enableBackups","enableCors","success","error","data","restApi","applicationDefault","answer","select","project","writeFile","regions","getGcpRegions","info","mkdir","firestoreRulesPath","initFile","firestoreRules","firestoreIndexesPath","firestoreIndexes","storageRulesPath","storageRules","functionsIndex","functionsPackage","configs","hostingConfigs","path","cwd","existingsBackups","restProjectApi","err","fileName","content","write","readFile","confirm","styleText"],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sources":["../../../src/cli/init.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { cwd } from \"node:process\";\nimport { styleText } from \"node:util\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm, select } from \"@inquirer/prompts\";\nimport { error, info, success } from \"@olinfo/quizms/utils-node\";\nimport { type App, applicationDefault } from \"firebase-admin/app\";\n\nimport { getGcpRegions } from \"~/cli/utils/region-finder\";\n\nimport firestoreRules from \"./files/firestore.rules?raw\";\nimport firestoreIndexes from \"./files/firestore-indexes.json\";\nimport functionsIndex from \"./files/functions-index.js?raw\";\nimport functionsPackage from \"./files/functions-package\";\nimport storageRules from \"./files/storage.rules?raw\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { restApi, restProjectApi } from \"./utils/rest-api\";\n\ntype InitOptions = {\n force?: boolean;\n};\n\nexport default async function init(options: InitOptions) {\n let initialized = true;\n\n await selectProject();\n const region = await getRegion();\n\n if (await copyFiles(options, region)) initialized = false;\n\n const { app, bucket } = await initializeFirebase();\n if (await enableBackups(app)) initialized = false;\n if (await enableCors(bucket)) initialized = false;\n\n if (initialized) {\n success(\"Project initialized!\");\n } else {\n error(\"The initialization was not completed due to some previous errors.\");\n }\n}\n\nasync function selectProject() {\n const data = await restApi(applicationDefault(), \"firebase\", \"v1beta1\", \"\");\n\n const answer = await select<string>({\n message: \"Seleziona il progetto Firebase\",\n choices: data.results.map((project: any) => ({\n value: project.projectId,\n name: project.displayName,\n })),\n });\n await writeFile(\".firebaserc\", JSON.stringify({ projects: { default: answer } }, null, 2));\n}\n\nasync function getRegion() {\n const regions = await getGcpRegions();\n return select<string>({\n message: \"Seleziona la regione\",\n choices: regions.map((region) => ({\n value: region.region,\n name: `${region.region} (${region.name})`,\n })),\n });\n}\n\nasync function copyFiles(options: InitOptions, region: string) {\n info(\"Copying files...\");\n\n await mkdir(\"firebase/functions\", { recursive: true });\n\n const firestoreRulesPath = \"firebase/firestore.rules\";\n await initFile(firestoreRulesPath, firestoreRules, options);\n\n const firestoreIndexesPath = \"firebase/firestore-indexes.json\";\n await initFile(firestoreIndexesPath, JSON.stringify(firestoreIndexes, undefined, 2), options);\n\n const storageRulesPath = \"firebase/storage.rules\";\n await initFile(storageRulesPath, storageRules, options);\n\n await initFile(\"firebase/functions/.env\", `QUIZMS_REGION=${region}`, options);\n await initFile(\n \"firebase/functions/index.js\",\n functionsIndex.replace(\"QUIZMS_REGION\", region),\n options,\n );\n await initFile(\n \"firebase/functions/package.json\",\n JSON.stringify(functionsPackage, undefined, 2),\n options,\n );\n\n const configs = {\n $schema: \"./node_modules/firebase-tools/schema/firebase-config.json\",\n hosting: hostingConfigs(region),\n firestore: {\n rules: path.relative(cwd(), firestoreRulesPath),\n indexes: path.relative(cwd(), firestoreIndexesPath),\n },\n storage: {\n rules: path.relative(cwd(), storageRulesPath),\n },\n functions: [\n {\n codebase: \"default\",\n disallowLegacyRuntimeConfig: true,\n ignore: [\"node_modules\", \"*.log\"],\n source: \"firebase/functions\",\n runtime: \"nodejs24\",\n },\n ],\n };\n await initFile(\"firebase.json\", JSON.stringify(configs, undefined, 2), options);\n\n success(\"Files copied!\");\n return 0;\n}\n\nfunction hostingConfigs(region: string) {\n return {\n public: \".quizms/hosting-build\",\n ignore: [\"firebase.json\", \"**/.*\", \"**/node_modules/**\"],\n rewrites: [\n {\n source: \"/api/**\",\n function: {\n functionId: \"api\",\n region,\n },\n },\n {\n source: \"**\",\n destination: \"/index.html\",\n },\n ],\n trailingSlash: false,\n headers: [\n {\n source: \"/assets/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=31536000, immutable\", // 365 days\n },\n ],\n },\n {\n source: \"/blockly/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=86400\", // 1 day\n },\n ],\n },\n ],\n predeploy: \"npx quizms build firebase\",\n };\n}\n\nasync function enableBackups(app: App) {\n info(\"Enabling Firestore backups...\");\n\n try {\n const existingsBackups = await restProjectApi(\n app,\n \"firestore\",\n \"v1\",\n \"/databases/(default)/backupSchedules\",\n );\n const hasDaily = existingsBackups.backupSchedules?.some((s: any) => s.dailyRecurrence);\n if (hasDaily) {\n info(\"Daily backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n dailyRecurrence: {},\n retention: \"604800s\",\n });\n }\n\n const hasWeekly = existingsBackups.backupSchedules?.some((s: any) => s.weeklyRecurrence);\n if (hasWeekly) {\n info(\"Weekly backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n weeklyRecurrence: {\n day: \"SUNDAY\",\n },\n retention: \"8467200s\",\n });\n }\n\n success(\"Backups enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to enable Firestore backups: ${err}`);\n return 1;\n }\n}\n\nasync function enableCors(bucket: Bucket) {\n info(\"Enabling CORS for Firebase bucket...\");\n\n try {\n await bucket.setCorsConfiguration([\n {\n origin: [\"*\"],\n method: [\"GET\"],\n maxAgeSeconds: 3600,\n },\n ]);\n success(\"CORS enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to set CORS configuration: ${err}`);\n return 1;\n }\n}\n\nasync function initFile(fileName: string, content: string, options?: InitOptions) {\n let write = options?.force ?? false;\n if (!write) {\n try {\n const prevContent = await readFile(fileName, \"utf8\");\n if (prevContent === content) return;\n } catch {\n write = true;\n }\n }\n\n if (!write) {\n write = await confirm({\n message: `The file ${styleText(\"bold\", path.basename(fileName))} already exists. Do you want to overwrite it?`,\n });\n }\n\n if (write) {\n await writeFile(fileName, content);\n }\n}\n"],"names":["init","options","initialized","selectProject","region","getRegion","copyFiles","app","bucket","initializeFirebase","enableBackups","enableCors","success","error","data","restApi","applicationDefault","answer","select","project","writeFile","regions","getGcpRegions","info","mkdir","firestoreRulesPath","initFile","firestoreRules","firestoreIndexesPath","firestoreIndexes","storageRulesPath","storageRules","functionsIndex","functionsPackage","configs","hostingConfigs","path","cwd","existingsBackups","restProjectApi","err","fileName","content","write","readFile","confirm","styleText"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import D, { hash as f } from "node:crypto";
2
- import { studentSchema as c, getNormalizedUserData as v } from "@olinfo/quizms/models";
1
+ import v, { hash as D } from "node:crypto";
2
+ import { studentSchema as c, getNormalizedUserData as f } from "@olinfo/quizms/models";
3
3
  import { TRPCError as r } from "@trpc/server";
4
4
  import { Timestamp as s } from "firebase-admin/firestore";
5
5
  import * as g from "firebase-functions/logger";
@@ -8,7 +8,7 @@ import { db as m, auth as p } from "../common.js";
8
8
  import { getVenueByToken as h, getContest as l, getStudentByHash as k, getRandomVariant as C } from "./queries.js";
9
9
  import { publicProcedure as R } from "./trpc.js";
10
10
  function T(o, t) {
11
- return f("sha256", v(o, { userData: t }));
11
+ return D("sha256", f(o, { userData: t }));
12
12
  }
13
13
  const U = R.input(
14
14
  d.strictObject({
@@ -25,7 +25,7 @@ const U = R.input(
25
25
  throw new r({ code: "BAD_REQUEST", message: "Codice prova non valido" });
26
26
  if (e.contestId !== t.contestId)
27
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);
28
+ const I = await l(e.contestId), n = v.randomUUID(), i = T(I, t.userData), a = await k(e.id, i);
29
29
  if (a) {
30
30
  if (a.token !== t.token)
31
31
  throw new r({
@@ -58,7 +58,7 @@ const U = R.input(
58
58
  contestId: t.contestId,
59
59
  token: t.token,
60
60
  participationWindow: e.participationWindow,
61
- variant: u.id,
61
+ variantId: u.id,
62
62
  answers: {},
63
63
  extraData: t.extraData,
64
64
  createdAt: s.now(),
@@ -68,7 +68,7 @@ const U = R.input(
68
68
  role: "student",
69
69
  studentId: w.id,
70
70
  venueId: e.id,
71
- variant: u
71
+ variantId: u.id
72
72
  });
73
73
  });
74
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, 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
+ {"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 variantId: 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 variantId: variant.id,\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,WAAWY,EAAQ;AAAA,IACnB,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,WAAWY,EAAQ;AAAA,EAAA,CACpB;AACH,CAAC;"}
@@ -80,11 +80,12 @@ function V(s, o, e) {
80
80
  }
81
81
  }
82
82
  for (const a of s.problemIds)
83
- if (c[a] !== void 0) {
83
+ if (c[a] === void 0)
84
+ r.push(""), r.push("");
85
+ else {
84
86
  const [t, n] = c[a];
85
87
  r.push(`${t}`), r.push(`${n}`);
86
- } else
87
- r.push(""), r.push("");
88
+ }
88
89
  return r.push(`${e.score ?? ""}`), r.push(`${e.absent ? 1 : 0}`), r.map((a) => `"${a}"`).join(",");
89
90
  }
90
91
  async function N(s, o, e, r, c, a, t) {
@@ -1 +1 @@
1
- {"version":3,"file":"export.js","sources":["../../../../src/web/admin/export.tsx"],"sourcesContent":["import { type RefObject, useRef } from \"react\";\n\nimport {\n type Contest,\n calcProblemPoints,\n displayAnswer,\n formatUserData,\n type Student,\n unshuffleAnswer,\n type Variant,\n} from \"@olinfo/quizms/models\";\nimport { Button, Modal } from \"@olinfo/react-components\";\nimport { formatISO } from \"date-fns\";\nimport {\n type DocumentSnapshot,\n type Firestore,\n type FirestoreDataConverter,\n getDocs,\n} from \"firebase/firestore\";\nimport { TriangleAlert } from \"lucide-react\";\n\nimport { useDb } from \"~/web/common/base-login\";\nimport query, { type QueryOption } from \"~/web/common/query\";\n\nimport { useAdmin } from \"./context\";\n\ntype Props<T> = {\n label: string;\n description: string;\n collection: string;\n converter: FirestoreDataConverter<T>;\n options: QueryOption<T>;\n suggestedName?: string;\n formatter: (data: T) => string;\n header?: string;\n};\n\nexport default function Export<T>(props: Props<T>) {\n const ref = useRef<HTMLDialogElement>(null);\n\n return (\n <>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Esporta {props.label}\n </Button>\n <ExportModal ref={ref} {...props} />\n </>\n );\n}\n\nfunction ExportModal({\n label,\n description,\n collection,\n converter,\n options,\n formatter,\n suggestedName,\n header,\n ref,\n}: Props<any> & { ref: RefObject<HTMLDialogElement | null> }) {\n const { contest } = useAdmin();\n const db = useDb();\n\n const onExport = async () => {\n await saveExport(db, collection, converter, options, formatter, suggestedName, header);\n ref.current?.close();\n };\n\n return (\n <Modal ref={ref} title={`Esporta ${label}`}>\n {window.showSaveFilePicker === undefined ? (\n <div className=\"flex items-center gap-3\">\n <TriangleAlert size={20} className=\"flex-none text-warning\" />\n <span>\n Il tuo browser non supporta l&apos;esportazione dei dati. Usa un browser Chrome o Edge.\n </span>\n </div>\n ) : (\n <>\n <div>\n Stai per esportare {description} della gara <b>{contest.name}</b>. Questa operazione\n esegue un grande numero di query al database che richiederà del tempo e potrà incidere\n sulla fatturazione del progetto.\n </div>\n <div className=\"mt-4 flex flex-wrap justify-center gap-2\">\n <Button className=\"btn-primary\" onClick={onExport}>\n Esporta\n </Button>\n </div>\n </>\n )}\n </Modal>\n );\n}\n\nexport function scoreboradHeader(contest: Contest) {\n const row: string[] = [];\n for (const field of contest.userData) {\n row.push(field.label);\n }\n row.push(\"Scuola\", \"Variante\");\n for (const problemId of contest.problemIds) {\n row.push(`${problemId}A`, `${problemId}P`);\n }\n row.push(\"Punteggio\");\n row.push(\"Assente\");\n return `${row.map((s) => `\"${s}\"`).join(\",\")}\\n`;\n}\n\n// TODO: move logic to @olinfo/quizms\nexport function scoreboardFormatter(\n contest: Contest,\n variants: Record<string, Variant>,\n student: Student,\n) {\n const row: string[] = [];\n for (const field of contest.userData) {\n if (field.type === \"date\") {\n const value = student.userData?.[field.name];\n if (value) {\n row.push(formatISO(value, { representation: \"date\" }));\n } else {\n row.push(\"\");\n }\n } else {\n row.push(formatUserData(student, field));\n }\n }\n row.push(student.venueId?.split(\"-\")[0] ?? \"\");\n row.push(student.variantId ?? \"\");\n const unshuflledProblems: Record<string, [string, number]> = {};\n if (\n student.answers !== undefined &&\n student.variantId !== undefined &&\n variants[student.variantId] !== undefined\n ) {\n const schema = variants[student.variantId].schema;\n for (const problemId in schema) {\n const problem = schema[problemId];\n const originalId = problem.originalId;\n const answer = student.answers[problemId];\n unshuflledProblems[originalId] = [\n answer?.value\n ? displayAnswer({\n type: answer.type,\n value: unshuffleAnswer(problem, answer.value) as any,\n })\n : \"\",\n calcProblemPoints(problem, answer),\n ];\n }\n }\n for (const problemId of contest.problemIds) {\n if (unshuflledProblems[problemId] !== undefined) {\n const [answer, points] = unshuflledProblems[problemId];\n row.push(`${answer}`);\n row.push(`${points}`);\n } else {\n row.push(\"\");\n row.push(\"\");\n }\n }\n row.push(`${student.score ?? \"\"}`);\n row.push(`${student.absent ? 1 : 0}`);\n return row.map((s) => `\"${s}\"`).join(\",\");\n}\n\nasync function saveExport<T>(\n db: Firestore,\n collection: string,\n converter: FirestoreDataConverter<T>,\n options: QueryOption<T>,\n formatter: (data: T) => string,\n suggestedName: string | undefined,\n header?: string,\n) {\n const handle = await window.showSaveFilePicker({\n suggestedName: suggestedName ?? `${collection}.jsonl`,\n });\n\n const writableStream = await handle.createWritable();\n\n if (header !== undefined) {\n await writableStream.write(header);\n }\n\n const chunkSize = 10_000;\n\n let last: DocumentSnapshot<T> | undefined;\n for (;;) {\n const q = query(db, collection, converter, {\n ...options,\n limit: chunkSize,\n startAfter: last,\n });\n\n const snapshot = await getDocs(q);\n if (snapshot.empty) break;\n\n for (const doc of snapshot.docs) {\n await writableStream.write(`${formatter(doc.data())}\\n`);\n }\n\n last = snapshot.docs.at(-1);\n }\n\n await writableStream.close();\n}\n"],"names":["Export","props","ref","useRef","jsxs","Fragment","Button","jsx","ExportModal","label","description","collection","converter","options","formatter","suggestedName","header","contest","useAdmin","db","useDb","onExport","saveExport","Modal","TriangleAlert","scoreboradHeader","row","field","problemId","s","scoreboardFormatter","variants","student","value","formatISO","formatUserData","unshuflledProblems","schema","problem","originalId","answer","displayAnswer","unshuffleAnswer","calcProblemPoints","points","writableStream","chunkSize","last","q","query","snapshot","getDocs","doc"],"mappings":"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"}
1
+ {"version":3,"file":"export.js","sources":["../../../../src/web/admin/export.tsx"],"sourcesContent":["import { type RefObject, useRef } from \"react\";\n\nimport {\n type Contest,\n calcProblemPoints,\n displayAnswer,\n formatUserData,\n type Student,\n unshuffleAnswer,\n type Variant,\n} from \"@olinfo/quizms/models\";\nimport { Button, Modal } from \"@olinfo/react-components\";\nimport { formatISO } from \"date-fns\";\nimport {\n type DocumentSnapshot,\n type Firestore,\n type FirestoreDataConverter,\n getDocs,\n} from \"firebase/firestore\";\nimport { TriangleAlert } from \"lucide-react\";\n\nimport { useDb } from \"~/web/common/base-login\";\nimport query, { type QueryOption } from \"~/web/common/query\";\n\nimport { useAdmin } from \"./context\";\n\ntype Props<T> = {\n label: string;\n description: string;\n collection: string;\n converter: FirestoreDataConverter<T>;\n options: QueryOption<T>;\n suggestedName?: string;\n formatter: (data: T) => string;\n header?: string;\n};\n\nexport default function Export<T>(props: Props<T>) {\n const ref = useRef<HTMLDialogElement>(null);\n\n return (\n <>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Esporta {props.label}\n </Button>\n <ExportModal ref={ref} {...props} />\n </>\n );\n}\n\nfunction ExportModal({\n label,\n description,\n collection,\n converter,\n options,\n formatter,\n suggestedName,\n header,\n ref,\n}: Props<any> & { ref: RefObject<HTMLDialogElement | null> }) {\n const { contest } = useAdmin();\n const db = useDb();\n\n const onExport = async () => {\n await saveExport(db, collection, converter, options, formatter, suggestedName, header);\n ref.current?.close();\n };\n\n return (\n <Modal ref={ref} title={`Esporta ${label}`}>\n {window.showSaveFilePicker === undefined ? (\n <div className=\"flex items-center gap-3\">\n <TriangleAlert size={20} className=\"flex-none text-warning\" />\n <span>\n Il tuo browser non supporta l&apos;esportazione dei dati. Usa un browser Chrome o Edge.\n </span>\n </div>\n ) : (\n <>\n <div>\n Stai per esportare {description} della gara <b>{contest.name}</b>. Questa operazione\n esegue un grande numero di query al database che richiederà del tempo e potrà incidere\n sulla fatturazione del progetto.\n </div>\n <div className=\"mt-4 flex flex-wrap justify-center gap-2\">\n <Button className=\"btn-primary\" onClick={onExport}>\n Esporta\n </Button>\n </div>\n </>\n )}\n </Modal>\n );\n}\n\nexport function scoreboradHeader(contest: Contest) {\n const row: string[] = [];\n for (const field of contest.userData) {\n row.push(field.label);\n }\n row.push(\"Scuola\", \"Variante\");\n for (const problemId of contest.problemIds) {\n row.push(`${problemId}A`, `${problemId}P`);\n }\n row.push(\"Punteggio\");\n row.push(\"Assente\");\n return `${row.map((s) => `\"${s}\"`).join(\",\")}\\n`;\n}\n\n// TODO: move logic to @olinfo/quizms\nexport function scoreboardFormatter(\n contest: Contest,\n variants: Record<string, Variant>,\n student: Student,\n) {\n const row: string[] = [];\n for (const field of contest.userData) {\n if (field.type === \"date\") {\n const value = student.userData?.[field.name];\n if (value) {\n row.push(formatISO(value, { representation: \"date\" }));\n } else {\n row.push(\"\");\n }\n } else {\n row.push(formatUserData(student, field));\n }\n }\n row.push(student.venueId?.split(\"-\")[0] ?? \"\");\n row.push(student.variantId ?? \"\");\n const unshuflledProblems: Record<string, [string, number]> = {};\n if (\n student.answers !== undefined &&\n student.variantId !== undefined &&\n variants[student.variantId] !== undefined\n ) {\n const schema = variants[student.variantId].schema;\n for (const problemId in schema) {\n const problem = schema[problemId];\n const originalId = problem.originalId;\n const answer = student.answers[problemId];\n unshuflledProblems[originalId] = [\n answer?.value\n ? displayAnswer({\n type: answer.type,\n value: unshuffleAnswer(problem, answer.value) as any,\n })\n : \"\",\n calcProblemPoints(problem, answer),\n ];\n }\n }\n for (const problemId of contest.problemIds) {\n if (unshuflledProblems[problemId] === undefined) {\n row.push(\"\");\n row.push(\"\");\n } else {\n const [answer, points] = unshuflledProblems[problemId];\n row.push(`${answer}`);\n row.push(`${points}`);\n }\n }\n row.push(`${student.score ?? \"\"}`);\n row.push(`${student.absent ? 1 : 0}`);\n return row.map((s) => `\"${s}\"`).join(\",\");\n}\n\nasync function saveExport<T>(\n db: Firestore,\n collection: string,\n converter: FirestoreDataConverter<T>,\n options: QueryOption<T>,\n formatter: (data: T) => string,\n suggestedName: string | undefined,\n header?: string,\n) {\n const handle = await window.showSaveFilePicker({\n suggestedName: suggestedName ?? `${collection}.jsonl`,\n });\n\n const writableStream = await handle.createWritable();\n\n if (header !== undefined) {\n await writableStream.write(header);\n }\n\n const chunkSize = 10_000;\n\n let last: DocumentSnapshot<T> | undefined;\n for (;;) {\n const q = query(db, collection, converter, {\n ...options,\n limit: chunkSize,\n startAfter: last,\n });\n\n const snapshot = await getDocs(q);\n if (snapshot.empty) break;\n\n for (const doc of snapshot.docs) {\n await writableStream.write(`${formatter(doc.data())}\\n`);\n }\n\n last = snapshot.docs.at(-1);\n }\n\n await writableStream.close();\n}\n"],"names":["Export","props","ref","useRef","jsxs","Fragment","Button","jsx","ExportModal","label","description","collection","converter","options","formatter","suggestedName","header","contest","useAdmin","db","useDb","onExport","saveExport","Modal","TriangleAlert","scoreboradHeader","row","field","problemId","s","scoreboardFormatter","variants","student","value","formatISO","formatUserData","unshuflledProblems","schema","problem","originalId","answer","displayAnswer","unshuffleAnswer","calcProblemPoints","points","writableStream","chunkSize","last","q","query","snapshot","getDocs","doc"],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "@olinfo/quizms-firebase",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "type": "module",
5
5
  "author": "Alessandro Bortolin <bortolin.alessandro@outlook.it>",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/olimpiadi-informatica/quizms.git",
10
+ "directory": "packages/quizms-firebase"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/olimpiadi-informatica/quizms/issues"
14
+ },
15
+ "homepage": "https://quizms.olinfo.it",
7
16
  "bin": {
8
17
  "quizms-firebase": "bin/bin.js"
9
18
  },
@@ -26,40 +35,40 @@
26
35
  "dist"
27
36
  ],
28
37
  "dependencies": {
29
- "@inquirer/prompts": "^8.3.0",
30
- "@olinfo/react-components": "^0.12.2",
31
- "@trpc/client": "^11.12.0",
32
- "@trpc/server": "^11.12.0",
38
+ "@inquirer/prompts": "^8.5.2",
39
+ "@olinfo/quizms": "^5.0.6",
40
+ "@olinfo/react-components": "^0.12.3",
41
+ "@trpc/client": "^11.18.0",
42
+ "@trpc/server": "^11.18.0",
33
43
  "@types/cli-progress": "^3.11.6",
34
44
  "@types/express": "^5.0.6",
35
- "@types/express-serve-static-core": "^5.1.1",
45
+ "@types/express-serve-static-core": "^5.1.2",
36
46
  "@types/lodash-es": "^4.17.12",
37
- "@types/picomatch": "^4.0.2",
38
- "ag-grid-community": "^35.1.0",
47
+ "@types/picomatch": "^4.0.3",
48
+ "ag-grid-community": "^35.3.1",
39
49
  "cli-progress": "^3.12.0",
40
50
  "clsx": "^2.1.1",
41
51
  "commander": "^14.0.3",
42
- "date-fns": "^4.1.0",
52
+ "date-fns": "^4.4.0",
43
53
  "express": "^5.2.1",
44
- "fastify": "^5.8.2",
45
- "firebase": "^12.10.0",
46
- "firebase-admin": "^13.7.0",
47
- "firebase-functions": "^7.1.0",
48
- "lodash-es": "^4.17.23",
54
+ "fastify": "^5.12.1",
55
+ "firebase": "^12.17.0",
56
+ "firebase-admin": "^13.10.0",
57
+ "firebase-functions": "^7.3.2",
58
+ "lodash-es": "^4.18.1",
49
59
  "lucide-react": "^0.562.0",
50
- "picomatch": "^4.0.3",
60
+ "picomatch": "^4.0.5",
51
61
  "react-error-boundary": "~6.0.3",
52
62
  "react-markdown": "^10.1.0",
53
63
  "rollup-plugin-node-externals": "^8.1.2",
54
64
  "rollup-preserve-directives": "^1.1.3",
55
65
  "superjson": "^2.2.6",
56
- "swr": "^2.4.1",
57
- "tinyglobby": "^0.2.15",
58
- "vite": "^7.3.1",
66
+ "swr": "^2.4.2",
67
+ "tinyglobby": "^0.2.17",
68
+ "vite": "^7.3.6",
59
69
  "vite-plugin-dts": "^4.5.4",
60
- "wouter": "^3.9.0",
61
- "zod": "^4.3.6",
62
- "@olinfo/quizms": "^5.0.2"
70
+ "wouter": "^3.10.0",
71
+ "zod": "^4.4.3"
63
72
  },
64
73
  "peerDependencies": {
65
74
  "react": "^19.2.4"
@@ -70,11 +79,11 @@
70
79
  }
71
80
  },
72
81
  "devDependencies": {
73
- "@biomejs/biome": "^2.4.6",
74
- "@google-cloud/storage": "^7.19.0",
75
- "@types/react": "^19.2.14",
82
+ "@biomejs/biome": "^2.5.6",
83
+ "@google-cloud/storage": "^7.21.0",
84
+ "@types/react": "^19.2.18",
76
85
  "@types/wicg-file-system-access": "^2023.10.7",
77
- "typescript": "5.8.2",
86
+ "typescript": "5.9.3",
78
87
  "vite-tsconfig-paths": "^6.1.1"
79
88
  },
80
89
  "scripts": {