@olinfo/quizms-firebase 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +28 -0
- package/dist/src/functions/api/admin.js.map +1 -0
- package/dist/src/functions/api/index.js +26 -34
- package/dist/src/functions/api/index.js.map +1 -1
- package/dist/src/functions/api/queries.js +5 -5
- 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 +23 -19
- package/dist/src/functions/api/schema.js +0 -42
- package/dist/src/functions/api/schema.js.map +0 -1
|
@@ -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;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data) return;\n\n await db.collection(\"submissions\").add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student: data.data(),\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","db"],"mappings":";AAKA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,EAAKC,
|
|
1
|
+
{"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data?.exists) return;\n\n await db.collection(\"submissions\").add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student: data.data(),\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","db"],"mappings":";AAKA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,EAAKC,GAAM,UAEX,MAAMC,EAAG,WAAW,aAAa,EAAE,IAAI;AAAA,IACrC,UAAUF,EAAM;AAAA,IAChB,QAAQA,EAAM;AAAA,IACd,WAAWC,EAAK;AAAA,IAChB,SAASA,EAAK,KAAA;AAAA,IACd,aAAaD,EAAM;AAAA,EAAA,CACpB;AACH;"}
|
package/dist/src/models/user.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import r from "zod";
|
|
2
|
-
const
|
|
2
|
+
const t = r.strictObject({
|
|
3
3
|
id: r.string(),
|
|
4
4
|
name: r.string(),
|
|
5
5
|
username: r.string(),
|
|
@@ -7,6 +7,6 @@ const n = r.object({
|
|
|
7
7
|
role: r.enum(["admin", "teacher"])
|
|
8
8
|
});
|
|
9
9
|
export {
|
|
10
|
-
|
|
10
|
+
t as userSchema
|
|
11
11
|
};
|
|
12
12
|
//# sourceMappingURL=user.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.js","sources":["../../../src/models/user.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const userSchema = z.
|
|
1
|
+
{"version":3,"file":"user.js","sources":["../../../src/models/user.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const userSchema = z.strictObject({\n id: z.string(),\n name: z.string(),\n username: z.string(),\n password: z.string(),\n role: z.enum([\"admin\", \"teacher\"]),\n});\n\nexport type User = z.infer<typeof userSchema>;\n"],"names":["userSchema","z"],"mappings":";AAEO,MAAMA,IAAaC,EAAE,aAAa;AAAA,EACvC,IAAIA,EAAE,OAAA;AAAA,EACN,MAAMA,EAAE,OAAA;AAAA,EACR,UAAUA,EAAE,OAAA;AAAA,EACZ,UAAUA,EAAE,OAAA;AAAA,EACZ,MAAMA,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC;AACnC,CAAC;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website.js","sources":["../../../src/models/website.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const websiteSchema = z.
|
|
1
|
+
{"version":3,"file":"website.js","sources":["../../../src/models/website.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const websiteSchema = z.strictObject({\n id: z.string(),\n origin: z.string(),\n title: z.string(),\n contests: z.string().array(),\n});\n\nexport type Website = z.infer<typeof websiteSchema>;\n"],"names":["websiteSchema","z"],"mappings":";AAEO,MAAMA,IAAgBC,EAAE,aAAa;AAAA,EAC1C,IAAIA,EAAE,OAAA;AAAA,EACN,QAAQA,EAAE,OAAA;AAAA,EACV,OAAOA,EAAE,OAAA;AAAA,EACT,UAAUA,EAAE,OAAA,EAAS,MAAA;AACvB,CAAC;"}
|
package/dist/src/vite/preset.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultClientMainFields as e, defaultClientConditions as i } from "vite";
|
|
2
|
-
function
|
|
2
|
+
function t() {
|
|
3
3
|
return {
|
|
4
4
|
name: "quizms:firebase-preset",
|
|
5
5
|
apply: "build",
|
|
@@ -7,7 +7,6 @@ function r() {
|
|
|
7
7
|
return {
|
|
8
8
|
environments: {
|
|
9
9
|
firebase: {
|
|
10
|
-
ssr: !1,
|
|
11
10
|
resolve: {
|
|
12
11
|
conditions: [...i],
|
|
13
12
|
mainFields: [...e],
|
|
@@ -16,7 +15,7 @@ function r() {
|
|
|
16
15
|
build: {
|
|
17
16
|
rollupOptions: {
|
|
18
17
|
input: {
|
|
19
|
-
index: "virtual:quizms-entry
|
|
18
|
+
index: "virtual:quizms-entry?id=@olinfo/quizms-firebase/entry"
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
}
|
|
@@ -27,6 +26,6 @@ function r() {
|
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
export {
|
|
30
|
-
|
|
29
|
+
t as default
|
|
31
30
|
};
|
|
32
31
|
//# sourceMappingURL=preset.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preset.js","sources":["../../../src/vite/preset.ts"],"sourcesContent":["import { defaultClientConditions, defaultClientMainFields, type PluginOption } from \"vite\";\n\nexport default function firebasePreset(): PluginOption {\n return {\n name: \"quizms:firebase-preset\",\n apply: \"build\",\n config() {\n return {\n environments: {\n firebase: {\n
|
|
1
|
+
{"version":3,"file":"preset.js","sources":["../../../src/vite/preset.ts"],"sourcesContent":["import { defaultClientConditions, defaultClientMainFields, type PluginOption } from \"vite\";\n\nexport default function firebasePreset(): PluginOption {\n return {\n name: \"quizms:firebase-preset\",\n apply: \"build\",\n config() {\n return {\n environments: {\n firebase: {\n resolve: {\n conditions: [...defaultClientConditions],\n mainFields: [...defaultClientMainFields],\n noExternal: true,\n },\n build: {\n rollupOptions: {\n input: {\n index: \"virtual:quizms-entry?id=@olinfo/quizms-firebase/entry\",\n },\n },\n },\n },\n },\n };\n },\n } satisfies PluginOption;\n}\n"],"names":["firebasePreset","defaultClientConditions","defaultClientMainFields"],"mappings":";AAEA,SAAwBA,IAA+B;AACrD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AACP,aAAO;AAAA,QACL,cAAc;AAAA,UACZ,UAAU;AAAA,YACR,SAAS;AAAA,cACP,YAAY,CAAC,GAAGC,CAAuB;AAAA,cACvC,YAAY,CAAC,GAAGC,CAAuB;AAAA,cACvC,YAAY;AAAA,YAAA;AAAA,YAEd,OAAO;AAAA,cACL,eAAe;AAAA,gBACb,OAAO;AAAA,kBACL,OAAO;AAAA,gBAAA;AAAA,cACT;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { jsxs as o, Fragment as
|
|
2
|
-
import { useRef as
|
|
1
|
+
import { jsxs as o, Fragment as f, jsx as e } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as p, Fragment as u, forwardRef as g, useReducer as h } from "react";
|
|
3
3
|
import { randomId as A } from "@olinfo/quizms/utils";
|
|
4
|
-
import { DateTime as x, CardActions as w, Button as
|
|
4
|
+
import { DateTime as x, CardActions as w, Button as y, Modal as N, Form as b, SelectField as z, TextField as F, TextAreaField as I, SubmitButton as v } from "@olinfo/react-components";
|
|
5
5
|
import { Info as C, TriangleAlert as M, CircleX as T } from "lucide-react";
|
|
6
6
|
import S from "react-markdown";
|
|
7
7
|
import { announcementConverter as B } from "../common/converters.js";
|
|
@@ -14,15 +14,14 @@ import { useCollection as R } from "../hooks/collection.js";
|
|
|
14
14
|
import "firebase/firestore";
|
|
15
15
|
import { useAdmin as s } from "./context.js";
|
|
16
16
|
function W() {
|
|
17
|
-
const { contest: a } = s(), t =
|
|
17
|
+
const { contest: a } = s(), t = p(null), [r, n] = R("announcements", B, {
|
|
18
18
|
arrayConstraints: {
|
|
19
19
|
contestIds: a.id
|
|
20
20
|
},
|
|
21
|
-
orderBy: ["createdAt", "desc"]
|
|
22
|
-
subscribe: !0
|
|
21
|
+
orderBy: ["createdAt", "desc"]
|
|
23
22
|
});
|
|
24
|
-
return /* @__PURE__ */ o(
|
|
25
|
-
/* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: r.map((i) => /* @__PURE__ */ o(
|
|
23
|
+
return /* @__PURE__ */ o(f, { children: [
|
|
24
|
+
/* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: r.map((i) => /* @__PURE__ */ o(u, { children: [
|
|
26
25
|
/* @__PURE__ */ o("h4", { className: "flex items-center gap-3", children: [
|
|
27
26
|
i.level === "info" && /* @__PURE__ */ e(C, { size: 20, className: "text-info" }),
|
|
28
27
|
i.level === "warning" && /* @__PURE__ */ e(M, { size: 20, className: "text-warning" }),
|
|
@@ -33,13 +32,13 @@ function W() {
|
|
|
33
32
|
/* @__PURE__ */ e(S, { children: i.body }),
|
|
34
33
|
/* @__PURE__ */ e("hr", { className: "my-5 last:hidden" })
|
|
35
34
|
] }, i.id)) }),
|
|
36
|
-
/* @__PURE__ */ e(w, { children: /* @__PURE__ */ e(
|
|
35
|
+
/* @__PURE__ */ e(w, { children: /* @__PURE__ */ e(y, { className: "btn-primary", onClick: () => t.current?.showModal(), children: "Aggiungi comunicazione" }) }),
|
|
37
36
|
/* @__PURE__ */ e(j, { ref: t, addAnnouncement: n })
|
|
38
37
|
] });
|
|
39
38
|
}
|
|
40
39
|
const j = g(function({ addAnnouncement: t }, r) {
|
|
41
40
|
const { contest: n } = s(), [i, c] = h((l) => l + 1, 0);
|
|
42
|
-
return /* @__PURE__ */ e(
|
|
41
|
+
return /* @__PURE__ */ e(N, { ref: r, title: "Aggiungi comunicazione", children: /* @__PURE__ */ o(b, { onSubmit: async ({
|
|
43
42
|
level: l,
|
|
44
43
|
title: m,
|
|
45
44
|
body: d
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, forwardRef, type Ref, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n
|
|
1
|
+
{"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, forwardRef, type Ref, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n });\n\n return (\n <>\n <div className=\"prose mt-4 max-w-none\">\n {announcements.map((announcement) => (\n <Fragment key={announcement.id}>\n <h4 className=\"flex items-center gap-3\">\n {announcement.level === \"info\" && <Info size={20} className=\"text-info\" />}\n {announcement.level === \"warning\" && (\n <TriangleAlert size={20} className=\"text-warning\" />\n )}\n {announcement.level === \"error\" && <CircleX size={20} className=\"text-error\" />}\n {announcement.title}\n </h4>\n <small>\n <DateTime date={announcement.createdAt} />\n </small>\n <Markdown>{announcement.body}</Markdown>\n <hr className=\"my-5 last:hidden\" />\n </Fragment>\n ))}\n </div>\n <CardActions>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Aggiungi comunicazione\n </Button>\n </CardActions>\n <AnnouncementModal ref={ref} addAnnouncement={addAnnouncement} />\n </>\n );\n}\n\nconst AnnouncementModal = forwardRef(function AnnouncementModal(\n { addAnnouncement }: { addAnnouncement: (announcement: Announcement) => Promise<void> },\n ref: Ref<HTMLDialogElement>,\n) {\n const { contest } = useAdmin();\n const [id, nextId] = useReducer((prev) => prev + 1, 0);\n\n const finalize = async ({\n level,\n title,\n body,\n }: Pick<Announcement, \"level\" | \"title\" | \"body\">) => {\n try {\n await addAnnouncement({\n id: randomId(),\n createdAt: new Date(),\n contestIds: [contest.id],\n level,\n title,\n body,\n });\n nextId();\n } finally {\n if (ref && \"current\" in ref) {\n ref.current?.close();\n }\n }\n };\n\n return (\n <Modal ref={ref} title=\"Aggiungi comunicazione\">\n <Form key={id} onSubmit={finalize} className=\"!max-w-full\">\n <SelectField\n field=\"level\"\n label=\"Priorità\"\n options={{ info: \"Normale\", warning: \"Alta\" }}\n placeholder=\"Seleziona la priorità\"\n />\n <TextField field=\"title\" label=\"Titolo\" placeholder=\"Inserisci il titolo\" />\n <TextAreaField\n field=\"body\"\n label=\"Messaggio\"\n placeholder=\"Inserisci il messaggio\"\n rows={5}\n />\n <SubmitButton>Aggiungi</SubmitButton>\n </Form>\n </Modal>\n );\n});\n"],"names":["Announcements","contest","useAdmin","ref","useRef","announcements","addAnnouncement","useCollection","announcementConverter","jsxs","Fragment","jsx","announcement","Info","TriangleAlert","CircleX","DateTime","Markdown","CardActions","Button","AnnouncementModal","forwardRef","id","nextId","useReducer","prev","Modal","Form","level","title","body","randomId","SelectField","TextField","TextAreaField","SubmitButton"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as e } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo as s, Suspense as
|
|
2
|
+
import { useMemo as s, Suspense as o } from "react";
|
|
3
3
|
import { AgGrid as d, Loading as c } from "@olinfo/quizms/components";
|
|
4
4
|
import { participationConverter as f } from "../common/converters.js";
|
|
5
5
|
import "firebase/auth";
|
|
@@ -12,18 +12,17 @@ import "firebase/firestore";
|
|
|
12
12
|
import { useCount as p } from "../hooks/count.js";
|
|
13
13
|
import { useAdmin as u } from "./context.js";
|
|
14
14
|
function T() {
|
|
15
|
-
const { contest: i } = u(), [
|
|
15
|
+
const { contest: i } = u(), [r, n] = m(
|
|
16
16
|
"participations",
|
|
17
17
|
f,
|
|
18
18
|
{
|
|
19
|
-
constraints: { contestId: i.id }
|
|
20
|
-
subscribe: !0
|
|
19
|
+
constraints: { contestId: i.id }
|
|
21
20
|
}
|
|
22
21
|
), a = s(() => h(), []);
|
|
23
|
-
return /* @__PURE__ */ e(
|
|
22
|
+
return /* @__PURE__ */ e(o, { fallback: /* @__PURE__ */ e(c, {}), children: /* @__PURE__ */ e("div", { className: "relative grow p-2", children: /* @__PURE__ */ e("div", { className: "absolute inset-0", children: /* @__PURE__ */ e(
|
|
24
23
|
d,
|
|
25
24
|
{
|
|
26
|
-
rowData:
|
|
25
|
+
rowData: r,
|
|
27
26
|
getRowId: (t) => t.data.id,
|
|
28
27
|
columnDefs: a,
|
|
29
28
|
onCellEditRequest: async (t) => {
|
|
@@ -55,7 +54,7 @@ function h() {
|
|
|
55
54
|
sortable: !1,
|
|
56
55
|
cellRenderer: ({ data: i }) => {
|
|
57
56
|
if (i)
|
|
58
|
-
return /* @__PURE__ */ e(
|
|
57
|
+
return /* @__PURE__ */ e(o, { children: /* @__PURE__ */ e(w, { participationId: i.id }) });
|
|
59
58
|
}
|
|
60
59
|
},
|
|
61
60
|
{
|
|
@@ -68,7 +67,7 @@ function h() {
|
|
|
68
67
|
}
|
|
69
68
|
];
|
|
70
69
|
}
|
|
71
|
-
function
|
|
70
|
+
function w({ participationId: i }) {
|
|
72
71
|
return p(`participations/${i}/students`, {
|
|
73
72
|
constraints: { disabled: !1 }
|
|
74
73
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"school-table.js","sources":["../../../../src/web/admin/school-table.tsx"],"sourcesContent":["import { Suspense, useMemo } from \"react\";\n\nimport { AgGrid, Loading } from \"@olinfo/quizms/components\";\nimport type { Participation, Student } from \"@olinfo/quizms/models\";\nimport type { CellEditRequestEvent, ColDef, ICellRendererParams } from \"ag-grid-community\";\n\nimport { participationConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport { useAdmin } from \"./context\";\n\nexport function SchoolTable() {\n const { contest } = useAdmin();\n\n const [participations, setParticipation] = useCollection(\n \"participations\",\n participationConverter,\n {\n constraints: { contestId: contest.id },\n
|
|
1
|
+
{"version":3,"file":"school-table.js","sources":["../../../../src/web/admin/school-table.tsx"],"sourcesContent":["import { Suspense, useMemo } from \"react\";\n\nimport { AgGrid, Loading } from \"@olinfo/quizms/components\";\nimport type { Participation, Student } from \"@olinfo/quizms/models\";\nimport type { CellEditRequestEvent, ColDef, ICellRendererParams } from \"ag-grid-community\";\n\nimport { participationConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport { useAdmin } from \"./context\";\n\nexport function SchoolTable() {\n const { contest } = useAdmin();\n\n const [participations, setParticipation] = useCollection(\n \"participations\",\n participationConverter,\n {\n constraints: { contestId: contest.id },\n },\n );\n\n const colDefs = useMemo(() => columnDefinition(), []);\n\n const onCellEditRequest = async (ev: CellEditRequestEvent) => {\n const participation = ev.data as Participation;\n if (ev.colDef.field === \"finalized\") {\n await setParticipation({ ...participation, finalized: ev.newValue });\n }\n ev.api.refreshCells({ force: true });\n };\n\n return (\n <Suspense fallback={<Loading />}>\n <div className=\"relative grow p-2\">\n <div className=\"absolute inset-0\">\n <AgGrid\n rowData={participations}\n getRowId={(row) => (row.data as Participation).id}\n columnDefs={colDefs}\n onCellEditRequest={onCellEditRequest}\n />\n </div>\n </div>\n </Suspense>\n );\n}\n\nfunction columnDefinition(): ColDef[] {\n return [\n {\n field: \"schoolId\",\n headerName: \"ID\",\n width: 150,\n filter: true,\n },\n {\n field: \"name\",\n headerName: \"Nome\",\n minWidth: 200,\n flex: 1,\n filter: true,\n },\n {\n field: \"count\",\n headerName: \"Studenti\",\n width: 100,\n sortable: false,\n cellRenderer: ({ data }: ICellRendererParams<Participation>) => {\n if (!data) return;\n return (\n <Suspense>\n <Count participationId={data.id} />\n </Suspense>\n );\n },\n },\n {\n field: \"finalized\",\n headerName: \"Finalizzato\",\n width: 100,\n filter: true,\n cellDataType: \"boolean\",\n editable: true,\n },\n ];\n}\n\nfunction Count({ participationId }: { participationId: string }) {\n return useCount<Student>(`participations/${participationId}/students`, {\n constraints: { disabled: false },\n });\n}\n"],"names":["SchoolTable","contest","useAdmin","participations","setParticipation","useCollection","participationConverter","colDefs","useMemo","columnDefinition","jsx","Suspense","Loading","AgGrid","row","ev","participation","data","Count","participationId","useCount"],"mappings":";;;;;;;;;;;;;AAYO,SAASA,IAAc;AAC5B,QAAM,EAAE,SAAAC,EAAA,IAAYC,EAAA,GAEd,CAACC,GAAgBC,CAAgB,IAAIC;AAAA,IACzC;AAAA,IACAC;AAAA,IACA;AAAA,MACE,aAAa,EAAE,WAAWL,EAAQ,GAAA;AAAA,IAAG;AAAA,EACvC,GAGIM,IAAUC,EAAQ,MAAMC,EAAA,GAAoB,CAAA,CAAE;AAUpD,SACE,gBAAAC,EAACC,GAAA,EAAS,UAAU,gBAAAD,EAACE,GAAA,CAAA,CAAQ,GAC3B,UAAA,gBAAAF,EAAC,OAAA,EAAI,WAAU,qBACb,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,oBACb,UAAA,gBAAAA;AAAA,IAACG;AAAA,IAAA;AAAA,MACC,SAASV;AAAA,MACT,UAAU,CAACW,MAASA,EAAI,KAAuB;AAAA,MAC/C,YAAYP;AAAA,MACZ,mBAhBgB,OAAOQ,MAA6B;AAC5D,cAAMC,IAAgBD,EAAG;AACzB,QAAIA,EAAG,OAAO,UAAU,eACtB,MAAMX,EAAiB,EAAE,GAAGY,GAAe,WAAWD,EAAG,UAAU,GAErEA,EAAG,IAAI,aAAa,EAAE,OAAO,IAAM;AAAA,MACrC;AAAA,IAUU;AAAA,EAAA,EACF,CACF,GACF,GACF;AAEJ;AAEA,SAASN,IAA6B;AACpC,SAAO;AAAA,IACL;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,UAAU;AAAA,MACV,cAAc,CAAC,EAAE,MAAAQ,QAA+C;AAC9D,YAAKA;AACL,mCACGN,GAAA,EACC,UAAA,gBAAAD,EAACQ,KAAM,iBAAiBD,EAAK,IAAI,EAAA,CACnC;AAAA,MAEJ;AAAA,IAAA;AAAA,IAEF;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,UAAU;AAAA,IAAA;AAAA,EACZ;AAEJ;AAEA,SAASC,EAAM,EAAE,iBAAAC,KAAgD;AAC/D,SAAOC,EAAkB,kBAAkBD,CAAe,aAAa;AAAA,IACrE,aAAa,EAAE,UAAU,GAAA;AAAA,EAAM,CAChC;AACH;"}
|