@olinfo/quizms-firebase 4.0.1 → 4.1.1
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.
- package/dist/functions.d.ts +2 -11
- package/dist/functions.js +2 -2
- package/dist/package.json.js +1 -1
- package/dist/src/cli/files/functions-index.js +4 -4
- package/dist/src/cli/files/functions-index.js.map +1 -1
- package/dist/src/cli/import.js +85 -96
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.js +8 -8
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/update-scores.js +21 -32
- package/dist/src/cli/update-scores.js.map +1 -1
- package/dist/src/cli/utils/collection.js +20 -18
- package/dist/src/cli/utils/collection.js.map +1 -1
- package/dist/src/cli/utils/converters-admin.js +31 -32
- package/dist/src/cli/utils/converters-admin.js.map +1 -1
- package/dist/src/cli/utils/region-finder.js +6 -5
- package/dist/src/cli/utils/region-finder.js.map +1 -1
- package/dist/src/cli/utils/storage.js +26 -35
- package/dist/src/cli/utils/storage.js.map +1 -1
- package/dist/src/cli/utils/users.js +13 -12
- package/dist/src/cli/utils/users.js.map +1 -1
- package/dist/src/functions/api/admin.js +25 -10
- package/dist/src/functions/api/admin.js.map +1 -1
- package/dist/src/functions/api/index.js +26 -37
- package/dist/src/functions/api/index.js.map +1 -1
- package/dist/src/functions/api/queries.js +24 -25
- package/dist/src/functions/api/queries.js.map +1 -1
- package/dist/src/functions/api/student.js +62 -72
- package/dist/src/functions/api/student.js.map +1 -1
- package/dist/src/functions/api/teacher.js +104 -111
- package/dist/src/functions/api/teacher.js.map +1 -1
- package/dist/src/functions/api/trpc.js +18 -0
- package/dist/src/functions/api/trpc.js.map +1 -0
- package/dist/src/functions/submissions.js +1 -1
- package/dist/src/functions/submissions.js.map +1 -1
- package/dist/src/models/user.js +2 -2
- package/dist/src/models/user.js.map +1 -1
- package/dist/src/models/website.js +1 -1
- package/dist/src/models/website.js.map +1 -1
- package/dist/src/vite/preset.js +3 -4
- package/dist/src/vite/preset.js.map +1 -1
- package/dist/src/web/admin/announcements.js +9 -10
- package/dist/src/web/admin/announcements.js.map +1 -1
- package/dist/src/web/admin/school-table.js +7 -8
- package/dist/src/web/admin/school-table.js.map +1 -1
- package/dist/src/web/common/api.js +32 -35
- package/dist/src/web/common/api.js.map +1 -1
- package/dist/src/web/common/converters.js +35 -36
- package/dist/src/web/common/converters.js.map +1 -1
- package/dist/src/web/common/token-login.js +11 -11
- package/dist/src/web/common/token-login.js.map +1 -1
- package/dist/src/web/entry/admin.js +5 -7
- package/dist/src/web/entry/admin.js.map +1 -1
- package/dist/src/web/entry/student-restoring.js +18 -20
- package/dist/src/web/entry/student-restoring.js.map +1 -1
- package/dist/src/web/entry/student.js +70 -76
- package/dist/src/web/entry/student.js.map +1 -1
- package/dist/src/web/entry/teacher.js +68 -68
- package/dist/src/web/entry/teacher.js.map +1 -1
- package/dist/src/web/hooks/collection.js +29 -30
- package/dist/src/web/hooks/collection.js.map +1 -1
- package/dist/src/web/hooks/document.js +35 -36
- package/dist/src/web/hooks/document.js.map +1 -1
- package/dist/src/web/hooks/subscription.js +26 -22
- package/dist/src/web/hooks/subscription.js.map +1 -1
- package/package.json +29 -25
- package/dist/src/functions/api/schema.js +0 -42
- package/dist/src/functions/api/schema.js.map +0 -1
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
import i from "
|
|
2
|
-
import {
|
|
3
|
-
import { studentConverter as o, userConverter as p, participationConverter as a, contestConverter as h, variantConverter as d } from "../../cli/utils/converters-admin.js";
|
|
1
|
+
import { Rng as i } from "@olinfo/quizms/utils";
|
|
2
|
+
import { userConverter as c, participationConverter as o, contestConverter as p, studentConverter as a, variantConverter as h } from "../../cli/utils/converters-admin.js";
|
|
4
3
|
import { db as e } from "../common.js";
|
|
5
|
-
const u = new
|
|
6
|
-
async function
|
|
7
|
-
return (await e.doc(`contests/${t}`).withConverter(
|
|
4
|
+
const u = new i();
|
|
5
|
+
async function v(t) {
|
|
6
|
+
return (await e.doc(`contests/${t}`).withConverter(p).get()).data();
|
|
8
7
|
}
|
|
9
|
-
async function
|
|
10
|
-
return (await e.doc(`participations/${t}`).withConverter(
|
|
8
|
+
async function C(t) {
|
|
9
|
+
return (await e.doc(`participations/${t}`).withConverter(o).get()).data();
|
|
11
10
|
}
|
|
12
|
-
async function
|
|
13
|
-
return (await e.collection(`participations/${t}/students`).where("token", "==", n).withConverter(
|
|
11
|
+
async function l(t, n) {
|
|
12
|
+
return (await e.collection(`participations/${t}/students`).where("token", "==", n).withConverter(a).get()).docs.map((r) => r.data());
|
|
14
13
|
}
|
|
15
|
-
async function
|
|
16
|
-
return (await e.collection("participations").withConverter(
|
|
14
|
+
async function f(t) {
|
|
15
|
+
return (await e.collection("participations").withConverter(o).where("token", "==", t).get()).docs[0]?.data();
|
|
17
16
|
}
|
|
18
|
-
async function
|
|
19
|
-
return (await e.collection("users").where("username", "==", t).withConverter(
|
|
17
|
+
async function m(t) {
|
|
18
|
+
return (await e.collection("users").where("username", "==", t).withConverter(c).get()).docs[0]?.data();
|
|
20
19
|
}
|
|
21
|
-
async function
|
|
22
|
-
const n = await e.collection("variants").withConverter(
|
|
20
|
+
async function y(t) {
|
|
21
|
+
const n = await e.collection("variants").withConverter(h).where("contestId", "==", t).where("isOnline", "==", !0).get();
|
|
23
22
|
return u.choice(n.docs).data();
|
|
24
23
|
}
|
|
25
|
-
async function
|
|
26
|
-
return (await e.collection(`participations/${t}/students`).withConverter(
|
|
24
|
+
async function $(t, n) {
|
|
25
|
+
return (await e.collection(`participations/${t}/students`).withConverter(a).where("userDataHash", "==", n).where("disabled", "!=", !0).get()).docs[0]?.data();
|
|
27
26
|
}
|
|
28
27
|
export {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
v as getContest,
|
|
29
|
+
C as getParticipation,
|
|
30
|
+
f as getParticipationByToken,
|
|
31
|
+
l as getParticipationStudents,
|
|
32
|
+
y as getRandomVariant,
|
|
33
|
+
$ as getStudentByHash,
|
|
34
|
+
m as getUser
|
|
36
35
|
};
|
|
37
36
|
//# sourceMappingURL=queries.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queries.js","sources":["../../../../src/functions/api/queries.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"queries.js","sources":["../../../../src/functions/api/queries.ts"],"sourcesContent":["import { Rng } from \"@olinfo/quizms/utils\";\n\nimport {\n contestConverter,\n participationConverter,\n studentConverter,\n userConverter,\n variantConverter,\n} from \"~/cli/utils/converters-admin\";\nimport { db } from \"~/functions/common\";\n\nconst rng = new Rng();\n\nexport async function getContest(contestId: string) {\n const snapshot = await db.doc(`contests/${contestId}`).withConverter(contestConverter).get();\n return snapshot.data()!;\n}\n\nexport async function getParticipation(participationId: string) {\n const snapshot = await db\n .doc(`participations/${participationId}`)\n .withConverter(participationConverter)\n .get();\n return snapshot.data();\n}\n\nexport async function getParticipationStudents(participationId: string, token: string) {\n const snapshot = await db\n .collection(`participations/${participationId}/students`)\n .where(\"token\", \"==\", token)\n .withConverter(studentConverter)\n .get();\n return snapshot.docs.map((doc) => doc.data());\n}\n\nexport async function getParticipationByToken(token: string) {\n const snapshot = await db\n .collection(\"participations\")\n .withConverter(participationConverter)\n .where(\"token\", \"==\", token)\n .get();\n return snapshot.docs[0]?.data();\n}\n\nexport async function getUser(username: string) {\n const snapshot = await db\n .collection(\"users\")\n .where(\"username\", \"==\", username)\n .withConverter(userConverter)\n .get();\n return snapshot.docs[0]?.data();\n}\n\nexport async function getRandomVariant(contestId: string) {\n const snapshot = await db\n .collection(\"variants\")\n .withConverter(variantConverter)\n .where(\"contestId\", \"==\", contestId)\n .where(\"isOnline\", \"==\", true)\n .get();\n return rng.choice(snapshot.docs).data();\n}\nexport async function getStudentByHash(participationId: string, userDataHash: string) {\n const snapshot = await db\n .collection(`participations/${participationId}/students`)\n .withConverter(studentConverter)\n .where(\"userDataHash\", \"==\", userDataHash)\n .where(\"disabled\", \"!=\", true)\n .get();\n return snapshot.docs[0]?.data();\n}\n"],"names":["rng","Rng","getContest","contestId","db","contestConverter","getParticipation","participationId","participationConverter","getParticipationStudents","token","studentConverter","doc","getParticipationByToken","getUser","username","userConverter","getRandomVariant","snapshot","variantConverter","getStudentByHash","userDataHash"],"mappings":";;;AAWA,MAAMA,IAAM,IAAIC,EAAA;AAEhB,eAAsBC,EAAWC,GAAmB;AAElD,UADiB,MAAMC,EAAG,IAAI,YAAYD,CAAS,EAAE,EAAE,cAAcE,CAAgB,EAAE,IAAA,GACvE,KAAA;AAClB;AAEA,eAAsBC,EAAiBC,GAAyB;AAK9D,UAJiB,MAAMH,EACpB,IAAI,kBAAkBG,CAAe,EAAE,EACvC,cAAcC,CAAsB,EACpC,IAAA,GACa,KAAA;AAClB;AAEA,eAAsBC,EAAyBF,GAAyBG,GAAe;AAMrF,UALiB,MAAMN,EACpB,WAAW,kBAAkBG,CAAe,WAAW,EACvD,MAAM,SAAS,MAAMG,CAAK,EAC1B,cAAcC,CAAgB,EAC9B,IAAA,GACa,KAAK,IAAI,CAACC,MAAQA,EAAI,MAAM;AAC9C;AAEA,eAAsBC,EAAwBH,GAAe;AAM3D,UALiB,MAAMN,EACpB,WAAW,gBAAgB,EAC3B,cAAcI,CAAsB,EACpC,MAAM,SAAS,MAAME,CAAK,EAC1B,IAAA,GACa,KAAK,CAAC,GAAG,KAAA;AAC3B;AAEA,eAAsBI,EAAQC,GAAkB;AAM9C,UALiB,MAAMX,EACpB,WAAW,OAAO,EAClB,MAAM,YAAY,MAAMW,CAAQ,EAChC,cAAcC,CAAa,EAC3B,IAAA,GACa,KAAK,CAAC,GAAG,KAAA;AAC3B;AAEA,eAAsBC,EAAiBd,GAAmB;AACxD,QAAMe,IAAW,MAAMd,EACpB,WAAW,UAAU,EACrB,cAAce,CAAgB,EAC9B,MAAM,aAAa,MAAMhB,CAAS,EAClC,MAAM,YAAY,MAAM,EAAI,EAC5B,IAAA;AACH,SAAOH,EAAI,OAAOkB,EAAS,IAAI,EAAE,KAAA;AACnC;AACA,eAAsBE,EAAiBb,GAAyBc,GAAsB;AAOpF,UANiB,MAAMjB,EACpB,WAAW,kBAAkBG,CAAe,WAAW,EACvD,cAAcI,CAAgB,EAC9B,MAAM,gBAAgB,MAAMU,CAAY,EACxC,MAAM,YAAY,MAAM,EAAI,EAC5B,IAAA,GACa,KAAK,CAAC,GAAG,KAAA;AAC3B;"}
|
|
@@ -1,88 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Timestamp as
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import D, { hash as I } from "node:crypto";
|
|
2
|
+
import { studentSchema as p, getNormalizedUserData as w } from "@olinfo/quizms/models";
|
|
3
|
+
import { TRPCError as i } from "@trpc/server";
|
|
4
|
+
import { Timestamp as r } from "firebase-admin/firestore";
|
|
5
|
+
import * as h from "firebase-functions/logger";
|
|
6
|
+
import s from "zod";
|
|
7
|
+
import { db as u, auth as m } from "../common.js";
|
|
8
|
+
import { getParticipationByToken as l, getContest as k, getStudentByHash as v, getRandomVariant as T } from "./queries.js";
|
|
9
|
+
import { publicProcedure as C } from "./trpc.js";
|
|
10
|
+
function R(o, t) {
|
|
11
|
+
return I("sha256", w(o, { userData: t }));
|
|
11
12
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
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: p.shape.userData.unwrap().required(),
|
|
18
|
+
extraData: p.shape.extraData.unwrap()
|
|
19
|
+
})
|
|
20
|
+
).mutation(async (o) => {
|
|
21
|
+
const { input: t } = o;
|
|
22
|
+
h.info("Student login request received", { data: t });
|
|
23
|
+
const a = await l(t.token);
|
|
24
|
+
if (!a)
|
|
25
|
+
throw new i({ code: "BAD_REQUEST", message: "Codice prova non valido" });
|
|
26
|
+
if (a.contestId !== t.contestId)
|
|
27
|
+
throw new i({ code: "FORBIDDEN", message: "Codice prova non valido" });
|
|
28
|
+
const f = await k(a.contestId), n = D.randomUUID(), d = R(f, t.userData), e = await v(a.id, d);
|
|
29
|
+
if (e) {
|
|
30
|
+
if (e.token !== t.token)
|
|
31
|
+
throw new i({
|
|
32
|
+
code: "FORBIDDEN",
|
|
33
|
+
message: "Hai già partecipato a questa gara"
|
|
34
|
+
});
|
|
35
|
+
return await u.doc(`studentRestores/${n}`).set({
|
|
36
|
+
studentId: e.id,
|
|
37
|
+
participationId: e.participationId,
|
|
49
38
|
token: t.token,
|
|
50
|
-
name:
|
|
51
|
-
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:
|
|
55
|
-
}),
|
|
43
|
+
createdAt: r.now()
|
|
44
|
+
}), m.createCustomToken(n, {
|
|
56
45
|
role: "student",
|
|
57
|
-
studentId:
|
|
58
|
-
participationId:
|
|
59
|
-
variant:
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
studentId: e.id,
|
|
47
|
+
participationId: e.participationId,
|
|
48
|
+
variant: e.variant
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const c = await T(t.contestId), g = await u.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:
|
|
57
|
+
participationId: a.id,
|
|
68
58
|
contestId: t.contestId,
|
|
69
59
|
token: t.token,
|
|
70
|
-
startedAt:
|
|
71
|
-
finishedAt:
|
|
72
|
-
variant:
|
|
60
|
+
startedAt: a.startingTime,
|
|
61
|
+
finishedAt: a.endingTime,
|
|
62
|
+
variant: c.id,
|
|
73
63
|
answers: {},
|
|
74
64
|
extraData: t.extraData,
|
|
75
|
-
createdAt:
|
|
76
|
-
updatedAt:
|
|
65
|
+
createdAt: r.now(),
|
|
66
|
+
updatedAt: r.now()
|
|
77
67
|
});
|
|
78
|
-
return
|
|
68
|
+
return m.createCustomToken(n, {
|
|
79
69
|
role: "student",
|
|
80
|
-
studentId:
|
|
81
|
-
participationId:
|
|
82
|
-
variant:
|
|
83
|
-
})
|
|
84
|
-
};
|
|
70
|
+
studentId: g.id,
|
|
71
|
+
participationId: a.id,
|
|
72
|
+
variant: c
|
|
73
|
+
});
|
|
74
|
+
});
|
|
85
75
|
export {
|
|
86
|
-
|
|
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 {
|
|
1
|
+
{"version":3,"file":"student.js","sources":["../../../../src/functions/api/student.ts"],"sourcesContent":["import crypto, { hash } from \"node:crypto\";\n\nimport {\n type Contest,\n getNormalizedUserData,\n type Student,\n studentSchema,\n} from \"@olinfo/quizms/models\";\nimport { TRPCError } from \"@trpc/server\";\nimport { Timestamp } from \"firebase-admin/firestore\";\nimport * as logger from \"firebase-functions/logger\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport { getContest, getParticipationByToken, getRandomVariant, getStudentByHash } from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nfunction studentHash(contest: Contest, userData: NonNullable<Student[\"userData\"]>) {\n return hash(\"sha256\", getNormalizedUserData(contest, { userData }));\n}\n\nexport const studentLogin = publicProcedure\n .input(\n z.strictObject({\n contestId: z.string(),\n token: z.string(),\n userData: studentSchema.shape.userData.unwrap().required(),\n extraData: studentSchema.shape.extraData.unwrap(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Student login request received\", { data });\n\n const participation = await getParticipationByToken(data.token);\n if (!participation) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Codice prova non valido\" });\n }\n if (participation.contestId !== data.contestId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Codice prova non valido\" });\n }\n\n const contest = await getContest(participation.contestId);\n\n const uid = crypto.randomUUID();\n const userDataHash = studentHash(contest, data.userData);\n\n const duplicated = await getStudentByHash(participation.id, userDataHash);\n if (duplicated) {\n if (duplicated.token !== data.token) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Hai già partecipato a questa gara\",\n });\n }\n\n await db.doc(`studentRestores/${uid}`).set({\n studentId: duplicated.id,\n participationId: duplicated.participationId,\n token: data.token,\n name: data.userData.name,\n surname: data.userData.surname,\n approvalCode: Math.round(Math.random() * 1000),\n status: \"pending\",\n createdAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: duplicated.id,\n participationId: duplicated.participationId,\n variant: duplicated.variant,\n });\n }\n\n const variant = await getRandomVariant(data.contestId);\n\n const studentRef = await db.collection(`participations/${participation.id}/students`).add({\n uid,\n userData: data.userData,\n userDataHash,\n absent: false,\n disabled: false,\n participationId: participation.id,\n contestId: data.contestId,\n token: data.token,\n startedAt: participation.startingTime,\n finishedAt: participation.endingTime,\n variant: variant.id,\n answers: {},\n extraData: data.extraData,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n });\n\n return auth.createCustomToken(uid, {\n role: \"student\",\n studentId: studentRef.id,\n participationId: participation.id,\n variant,\n });\n });\n"],"names":["studentHash","contest","userData","hash","getNormalizedUserData","studentLogin","publicProcedure","z","studentSchema","opts","data","logger","participation","getParticipationByToken","TRPCError","getContest","uid","crypto","userDataHash","duplicated","getStudentByHash","db","Timestamp","auth","variant","getRandomVariant","studentRef"],"mappings":";;;;;;;;;AAiBA,SAASA,EAAYC,GAAkBC,GAA4C;AACjF,SAAOC,EAAK,UAAUC,EAAsBH,GAAS,EAAE,UAAAC,EAAA,CAAU,CAAC;AACpE;AAEO,MAAMG,IAAeC,EACzB;AAAA,EACCC,EAAE,aAAa;AAAA,IACb,WAAWA,EAAE,OAAA;AAAA,IACb,OAAOA,EAAE,OAAA;AAAA,IACT,UAAUC,EAAc,MAAM,SAAS,OAAA,EAAS,SAAA;AAAA,IAChD,WAAWA,EAAc,MAAM,UAAU,OAAA;AAAA,EAAO,CACjD;AACH,EACC,SAAS,OAAOC,MAAS;AACxB,QAAM,EAAE,OAAOC,EAAA,IAASD;AACxB,EAAAE,EAAO,KAAK,kCAAkC,EAAE,MAAAD,EAAA,CAAM;AAEtD,QAAME,IAAgB,MAAMC,EAAwBH,EAAK,KAAK;AAC9D,MAAI,CAACE;AACH,UAAM,IAAIE,EAAU,EAAE,MAAM,eAAe,SAAS,2BAA2B;AAEjF,MAAIF,EAAc,cAAcF,EAAK;AACnC,UAAM,IAAII,EAAU,EAAE,MAAM,aAAa,SAAS,2BAA2B;AAG/E,QAAMb,IAAU,MAAMc,EAAWH,EAAc,SAAS,GAElDI,IAAMC,EAAO,WAAA,GACbC,IAAelB,EAAYC,GAASS,EAAK,QAAQ,GAEjDS,IAAa,MAAMC,EAAiBR,EAAc,IAAIM,CAAY;AACxE,MAAIC,GAAY;AACd,QAAIA,EAAW,UAAUT,EAAK;AAC5B,YAAM,IAAII,EAAU;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAGH,iBAAMO,EAAG,IAAI,mBAAmBL,CAAG,EAAE,EAAE,IAAI;AAAA,MACzC,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,OAAOT,EAAK;AAAA,MACZ,MAAMA,EAAK,SAAS;AAAA,MACpB,SAASA,EAAK,SAAS;AAAA,MACvB,cAAc,KAAK,MAAM,KAAK,OAAA,IAAW,GAAI;AAAA,MAC7C,QAAQ;AAAA,MACR,WAAWY,EAAU,IAAA;AAAA,IAAI,CAC1B,GAEMC,EAAK,kBAAkBP,GAAK;AAAA,MACjC,MAAM;AAAA,MACN,WAAWG,EAAW;AAAA,MACtB,iBAAiBA,EAAW;AAAA,MAC5B,SAASA,EAAW;AAAA,IAAA,CACrB;AAAA,EACH;AAEA,QAAMK,IAAU,MAAMC,EAAiBf,EAAK,SAAS,GAE/CgB,IAAa,MAAML,EAAG,WAAW,kBAAkBT,EAAc,EAAE,WAAW,EAAE,IAAI;AAAA,IACxF,KAAAI;AAAA,IACA,UAAUN,EAAK;AAAA,IACf,cAAAQ;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,iBAAiBN,EAAc;AAAA,IAC/B,WAAWF,EAAK;AAAA,IAChB,OAAOA,EAAK;AAAA,IACZ,WAAWE,EAAc;AAAA,IACzB,YAAYA,EAAc;AAAA,IAC1B,SAASY,EAAQ;AAAA,IACjB,SAAS,CAAA;AAAA,IACT,WAAWd,EAAK;AAAA,IAChB,WAAWY,EAAU,IAAA;AAAA,IACrB,WAAWA,EAAU,IAAA;AAAA,EAAI,CAC1B;AAED,SAAOC,EAAK,kBAAkBP,GAAK;AAAA,IACjC,MAAM;AAAA,IACN,WAAWU,EAAW;AAAA,IACtB,iBAAiBd,EAAc;AAAA,IAC/B,SAAAY;AAAA,EAAA,CACD;AACH,CAAC;"}
|
|
@@ -1,136 +1,129 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { randomToken as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import D from "node:crypto";
|
|
2
|
+
import { randomToken as E } from "@olinfo/quizms/utils";
|
|
3
|
+
import { TRPCError as o } from "@trpc/server";
|
|
4
|
+
import { isFuture as g, roundToNearestMinutes as O, addSeconds as l, addMinutes as R, isPast as h } from "date-fns";
|
|
5
|
+
import { getFunctions as B } from "firebase-admin/functions";
|
|
6
|
+
import * as d from "firebase-functions/logger";
|
|
7
|
+
import { defineString as F } from "firebase-functions/params";
|
|
8
|
+
import { chunk as P } from "lodash-es";
|
|
9
|
+
import r from "zod";
|
|
10
|
+
import { auth as T, db as c } from "../common.js";
|
|
11
|
+
import { getUser as S, getParticipationStudents as I, getParticipation as y, getContest as b, getParticipationByToken as v } from "./queries.js";
|
|
12
|
+
import { publicProcedure as w } from "./trpc.js";
|
|
13
|
+
const N = F("QUIZMS_REGION"), W = w.input(
|
|
14
|
+
r.strictObject({
|
|
15
|
+
username: r.string(),
|
|
16
|
+
password: r.string()
|
|
17
|
+
})
|
|
18
|
+
).mutation(async (a) => {
|
|
19
|
+
const { input: i } = a;
|
|
20
|
+
d.info("Teacher login request received", { data: i });
|
|
21
|
+
const e = await S(i.username);
|
|
22
|
+
if (!e)
|
|
23
|
+
throw new o({ code: "BAD_REQUEST", message: "Username non esistente" });
|
|
24
|
+
if (e.password !== i.password)
|
|
25
|
+
throw new o({ code: "BAD_REQUEST", message: "Password non corretta" });
|
|
26
|
+
if (e.role !== "teacher")
|
|
27
|
+
throw new o({ code: "FORBIDDEN", message: "Utenza non abilitata" });
|
|
28
|
+
return T.createCustomToken(D.randomUUID(), {
|
|
14
29
|
role: "teacher",
|
|
15
|
-
schoolId:
|
|
16
|
-
})
|
|
17
|
-
};
|
|
18
|
-
async function f(
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!e.hasOnline)
|
|
33
|
-
return [
|
|
34
|
-
{
|
|
35
|
-
success: !1,
|
|
36
|
-
errorCode: "CONTEST_OFFLINE",
|
|
37
|
-
error: "Gara non abilitata per prove online"
|
|
38
|
-
}
|
|
39
|
-
];
|
|
40
|
-
if (T(e.contestWindowStart))
|
|
41
|
-
return [{ success: !1, errorCode: "CONTEST_NOT_STARTED", error: "Gara non iniziata" }];
|
|
42
|
-
if (l(e.contestWindowEnd))
|
|
43
|
-
return [{ success: !1, errorCode: "CONTEST_ENDED", error: "Gara già terminata" }];
|
|
30
|
+
schoolId: e.id
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
async function f(a, i, e) {
|
|
34
|
+
const t = await y(a);
|
|
35
|
+
if (!t)
|
|
36
|
+
throw new o({ code: "BAD_REQUEST", message: "Gara non valida" });
|
|
37
|
+
if (t.schoolId !== i?.schoolId)
|
|
38
|
+
throw new o({ code: "FORBIDDEN", message: "Gara non valida" });
|
|
39
|
+
const n = await b(t.contestId);
|
|
40
|
+
if (e) {
|
|
41
|
+
if (!n.hasOnline)
|
|
42
|
+
throw new o({ code: "FORBIDDEN", message: "Gara non abilitata per prove online" });
|
|
43
|
+
if (g(n.contestWindowStart))
|
|
44
|
+
throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
|
|
45
|
+
if (h(n.contestWindowEnd))
|
|
46
|
+
throw new o({ code: "FORBIDDEN", message: "Gara già terminata" });
|
|
44
47
|
}
|
|
45
|
-
return [
|
|
48
|
+
return [t, n];
|
|
46
49
|
}
|
|
47
|
-
async function
|
|
50
|
+
async function z() {
|
|
48
51
|
for (; ; ) {
|
|
49
|
-
const
|
|
50
|
-
if (!await
|
|
51
|
-
return
|
|
52
|
+
const a = await E();
|
|
53
|
+
if (!await v(a))
|
|
54
|
+
return a;
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await h(d);
|
|
57
|
+
const L = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
|
|
58
|
+
const { input: i, ctx: e } = a;
|
|
59
|
+
d.info("Teacher start participation received", { data: i });
|
|
60
|
+
const [t, n] = await f(
|
|
61
|
+
i.participationId,
|
|
62
|
+
e.claims,
|
|
63
|
+
!0
|
|
64
|
+
);
|
|
65
|
+
if (t.endingTime != null && n.hasOnline && (g(t.endingTime) || !n.allowRestart))
|
|
66
|
+
throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
|
|
67
|
+
if (t.token) {
|
|
68
|
+
const m = await I(t.id, t.token);
|
|
69
|
+
await k(m);
|
|
68
70
|
}
|
|
69
|
-
const
|
|
70
|
-
await
|
|
71
|
+
const p = O(l(Date.now(), 3.5 * 60)), s = R(p, n.hasOnline ? n.duration : 0), u = await z();
|
|
72
|
+
await c.doc(`participations/${t.id}`).update({
|
|
71
73
|
token: u,
|
|
72
|
-
startingTime:
|
|
73
|
-
endingTime:
|
|
74
|
+
startingTime: p,
|
|
75
|
+
endingTime: s
|
|
74
76
|
});
|
|
75
77
|
try {
|
|
76
|
-
await
|
|
77
|
-
`locations/${
|
|
78
|
+
await B().taskQueue(
|
|
79
|
+
`locations/${N.value()}/functions/updateScores`
|
|
78
80
|
).enqueue(
|
|
79
|
-
{ participationId:
|
|
80
|
-
{ scheduleTime:
|
|
81
|
+
{ participationId: t.id, token: u },
|
|
82
|
+
{ scheduleTime: l(s, 5) }
|
|
81
83
|
);
|
|
82
|
-
} catch (
|
|
83
|
-
|
|
84
|
+
} catch (m) {
|
|
85
|
+
d.error("Failed to queue updateScores task", { error: m });
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const [
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
errorCode: "PARTICIPATION_ALREADY_STARTED",
|
|
101
|
-
error: "Gara già iniziata"
|
|
102
|
-
};
|
|
103
|
-
const i = await w(e.id, e.token);
|
|
104
|
-
await h(i);
|
|
105
|
-
for (const c of A(i, 300)) {
|
|
106
|
-
const o = n.batch();
|
|
107
|
-
for (const u of c)
|
|
108
|
-
o.delete(n.doc(`participations/${e.id}/students/${u.id}`));
|
|
109
|
-
await o.commit();
|
|
87
|
+
}), Z = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
|
|
88
|
+
const { input: i, ctx: e } = a;
|
|
89
|
+
d.info("Teacher stop participation received", { data: i });
|
|
90
|
+
const [t] = await f(i.participationId, e.claims, !0);
|
|
91
|
+
if (t.startingTime == null)
|
|
92
|
+
throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
|
|
93
|
+
if (h(t.startingTime))
|
|
94
|
+
throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
|
|
95
|
+
const n = await I(t.id, t.token);
|
|
96
|
+
await k(n);
|
|
97
|
+
for (const p of P(n, 300)) {
|
|
98
|
+
const s = c.batch();
|
|
99
|
+
for (const u of p)
|
|
100
|
+
s.delete(c.doc(`participations/${t.id}/students/${u.id}`));
|
|
101
|
+
await s.commit();
|
|
110
102
|
}
|
|
111
|
-
|
|
103
|
+
await c.doc(`participations/${t.id}`).update({
|
|
112
104
|
token: null,
|
|
113
105
|
startingTime: null,
|
|
114
106
|
endingTime: null
|
|
115
|
-
})
|
|
116
|
-
};
|
|
117
|
-
async function
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
async function k(a) {
|
|
118
110
|
await Promise.all(
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
a.map(async (i) => {
|
|
112
|
+
i.uid && await T.revokeRefreshTokens(i.uid).catch(() => {
|
|
121
113
|
});
|
|
122
114
|
})
|
|
123
115
|
);
|
|
124
116
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
117
|
+
const H = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
|
|
118
|
+
const { input: i, ctx: e } = a;
|
|
119
|
+
d.info("Teacher finalize participation received", { data: i });
|
|
120
|
+
const [t] = await f(i.participationId, e.claims, !1);
|
|
121
|
+
await c.doc(`participations/${t.id}`).update({ finalized: !0 });
|
|
122
|
+
});
|
|
130
123
|
export {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
124
|
+
H as teacherFinalizeParticipation,
|
|
125
|
+
W as teacherLogin,
|
|
126
|
+
L as teacherStartParticipation,
|
|
127
|
+
Z as teacherStopParticipation
|
|
135
128
|
};
|
|
136
129
|
//# sourceMappingURL=teacher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Participation, Student } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport type { CallableRequest } from \"firebase-functions/https\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\n\nimport { auth, db, type Endpoint, type EndpointError } from \"../common\";\nimport {\n getContest,\n getParticipation,\n getParticipationByToken,\n getParticipationStudents,\n getUser,\n} from \"./queries\";\nimport type {\n TeacherFinalizeParticipation,\n TeacherLogin,\n TeacherStartParticipation,\n TeacherStopParticipation,\n} from \"./schema\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin: Endpoint<TeacherLogin> = async (_request, data) => {\n logger.info(\"Teacher 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 !== \"teacher\") {\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(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n return { success: true, data: { token } };\n};\n\nasync function checkParticipation(\n request: CallableRequest<{ participationId: string }>,\n onlineOnly: boolean,\n): Promise<[EndpointError] | [null, Participation, Contest]> {\n const participation = await getParticipation(request.data.participationId);\n if (!participation) {\n return [{ success: false, errorCode: \"PARTICIPATION_NOT_FOUND\", error: \"Gara non valida\" }];\n }\n\n if (participation.schoolId !== request.auth?.token.schoolId) {\n return [\n {\n success: false,\n errorCode: \"FORBIDDEN\",\n error: \"Gara non valida\",\n },\n ];\n }\n\n const contest = await getContest(participation.contestId);\n if (onlineOnly) {\n if (!contest.hasOnline) {\n return [\n {\n success: false,\n errorCode: \"CONTEST_OFFLINE\",\n error: \"Gara non abilitata per prove online\",\n },\n ];\n }\n if (isFuture(contest.contestWindowStart)) {\n return [{ success: false, errorCode: \"CONTEST_NOT_STARTED\", error: \"Gara non iniziata\" }];\n }\n if (isPast(contest.contestWindowEnd)) {\n return [{ success: false, errorCode: \"CONTEST_ENDED\", error: \"Gara già terminata\" }];\n }\n }\n\n return [null, participation, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getParticipationByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartParticipation: Endpoint<TeacherStartParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher start participation received\", { data });\n\n const [error, participation, contest] = await checkParticipation(request, true);\n if (error) {\n return error;\n }\n\n if (\n participation.endingTime != null &&\n contest.hasOnline &&\n (isFuture(participation.endingTime) || !contest.allowRestart)\n ) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_ALREADY_STARTED\",\n error: \"Gara già iniziata\",\n };\n }\n\n if (participation.token) {\n const students = await getParticipationStudents(participation.id, participation.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n const token = await generateToken();\n\n await db.doc(`participations/${participation.id}`).update({\n token,\n startingTime,\n endingTime,\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { participationId: participation.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n\n return { success: true, data: {} };\n};\n\nexport const teacherStopParticipation: Endpoint<TeacherStopParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher stop participation received\", { data });\n\n const [error, participation] = await checkParticipation(request, true);\n if (error) {\n return error;\n }\n\n if (participation.startingTime == null) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_NOT_STARTED\",\n error: \"Gara non iniziata\",\n };\n }\n\n if (isPast(participation.startingTime)) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_ALREADY_STARTED\",\n error: \"Gara già iniziata\",\n };\n }\n\n const students = await getParticipationStudents(participation.id, participation.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`participations/${participation.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n\n return { success: true, data: {} };\n};\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeParticipation: Endpoint<TeacherFinalizeParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher finalize participation received\", { data });\n\n const [error, participation] = await checkParticipation(request, false);\n if (error) {\n return error;\n }\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n\n return { success: true, data: {} };\n};\n"],"names":["region","defineString","teacherLogin","_request","data","logger","user","getUser","auth","crypto","checkParticipation","request","onlineOnly","participation","getParticipation","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getParticipationByToken","teacherStartParticipation","error","students","getParticipationStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","teacherStopParticipation","studentChunk","chunk","batch","student","teacherFinalizeParticipation"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Participation, Student } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { TRPCError } from \"@trpc/server\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport type { DecodedIdToken } from \"firebase-admin/auth\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport {\n getContest,\n getParticipation,\n getParticipationByToken,\n getParticipationStudents,\n getUser,\n} from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin = publicProcedure\n .input(\n z.strictObject({\n username: z.string(),\n password: z.string(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Teacher login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Username non esistente\" });\n }\n\n if (user.password !== data.password) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Password non corretta\" });\n }\n if (user.role !== \"teacher\") {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Utenza non abilitata\" });\n }\n\n return auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n });\n\nasync function checkParticipation(\n participationId: string,\n claims: DecodedIdToken | undefined,\n onlineOnly: boolean,\n): Promise<[Participation, Contest]> {\n const participation = await getParticipation(participationId);\n if (!participation) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Gara non valida\" });\n }\n\n if (participation.schoolId !== claims?.schoolId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non valida\" });\n }\n\n const contest = await getContest(participation.contestId);\n if (onlineOnly) {\n if (!contest.hasOnline) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non abilitata per prove online\" });\n }\n if (isFuture(contest.contestWindowStart)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n if (isPast(contest.contestWindowEnd)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già terminata\" });\n }\n }\n\n return [participation, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getParticipationByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher start participation received\", { data });\n\n const [participation, contest] = await checkParticipation(\n data.participationId,\n ctx.claims,\n true,\n );\n\n if (\n participation.endingTime != null &&\n contest.hasOnline &&\n (isFuture(participation.endingTime) || !contest.allowRestart)\n ) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n if (participation.token) {\n const students = await getParticipationStudents(participation.id, participation.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n const token = await generateToken();\n\n await db.doc(`participations/${participation.id}`).update({\n token,\n startingTime,\n endingTime,\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { participationId: participation.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n });\n\nexport const teacherStopParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher stop participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, true);\n\n if (participation.startingTime == null) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n\n if (isPast(participation.startingTime)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n const students = await getParticipationStudents(participation.id, participation.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`participations/${participation.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n });\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher finalize participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, false);\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n });\n"],"names":["region","defineString","teacherLogin","publicProcedure","z","opts","data","logger","user","getUser","TRPCError","auth","crypto","checkParticipation","participationId","claims","onlineOnly","participation","getParticipation","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getParticipationByToken","teacherStartParticipation","ctx","students","getParticipationStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","error","teacherStopParticipation","studentChunk","chunk","batch","student","teacherFinalizeParticipation"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { initTRPC as n } from "@trpc/server";
|
|
2
|
+
import c from "superjson";
|
|
3
|
+
import { auth as a } from "../common.js";
|
|
4
|
+
async function p({ req: o }) {
|
|
5
|
+
const r = o.header("authorization");
|
|
6
|
+
if (r) {
|
|
7
|
+
const e = r.replace("Bearer ", "");
|
|
8
|
+
return { claims: await a.verifyIdToken(e, !0) };
|
|
9
|
+
}
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
const t = n.context().create({ transformer: c }), f = t.router, d = t.procedure;
|
|
13
|
+
export {
|
|
14
|
+
p as createContext,
|
|
15
|
+
d as publicProcedure,
|
|
16
|
+
f as router
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=trpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trpc.js","sources":["../../../../src/functions/api/trpc.ts"],"sourcesContent":["import { initTRPC } from \"@trpc/server\";\nimport type * as trpcExpress from \"@trpc/server/adapters/express\";\nimport superjson from \"superjson\";\n\nimport { auth } from \"~/functions/common\";\n\nexport async function createContext({ req }: trpcExpress.CreateExpressContextOptions) {\n const authorization = req.header(\"authorization\");\n if (authorization) {\n const token = authorization.replace(\"Bearer \", \"\");\n const claims = await auth.verifyIdToken(token, true);\n return { claims };\n }\n return {};\n}\nexport type Context = Awaited<ReturnType<typeof createContext>>;\n\nconst t = initTRPC.context<Context>().create({ transformer: superjson });\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\n"],"names":["createContext","req","authorization","token","auth","initTRPC","superjson","router","publicProcedure"],"mappings":";;;AAMA,eAAsBA,EAAc,EAAE,KAAAC,KAAgD;AACpF,QAAMC,IAAgBD,EAAI,OAAO,eAAe;AAChD,MAAIC,GAAe;AACjB,UAAMC,IAAQD,EAAc,QAAQ,WAAW,EAAE;AAEjD,WAAO,EAAE,QADM,MAAME,EAAK,cAAcD,GAAO,EAAI,EAC1C;AAAA,EACX;AACA,SAAO,CAAA;AACT;AAGA,MAAM,IAAIE,EAAS,QAAA,EAAmB,OAAO,EAAE,aAAaC,GAAW,GAC1DC,IAAS,EAAE,QACXC,IAAkB,EAAE;"}
|