Skip to content

Commit 0380746

Browse files
committed
feat: optionally allow assistants to take the quiz too
assistants are defined by being part of an Intra group with a specific id, set in .env
1 parent adf8706 commit 0380746

File tree

10 files changed

+181
-9
lines changed

10 files changed

+181
-9
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ INTRA_API_UID=enter_your_uid_here
55
INTRA_API_SECRET=enter_your_secret_here
66
INTRA_CAMPUS_ID=14
77
INTRA_CURSUS_ID=21
8+
INTRA_ASSISTANT_GROUP_ID=68
89
INTRA_TEST_ACCOUNTS=karthur,ctest,moretestinglogins
910

11+
# Whether or not assistants can join the Coalition System and choose a coalition
12+
# They can never gain points though, only join a coalition
13+
ASSISTANTS_CAN_QUIZ=false
14+
1015
POSTGRES_USER=enter_a_postgres_user_here
1116
POSTGRES_PASSWORD=enter_a_postgres_password_here
1217
POSTGRES_DB=coal
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- CreateTable
2+
CREATE TABLE "IntraGroup" (
3+
"id" INTEGER NOT NULL,
4+
"name" TEXT NOT NULL,
5+
6+
CONSTRAINT "IntraGroup_pkey" PRIMARY KEY ("id")
7+
);
8+
9+
-- CreateTable
10+
CREATE TABLE "IntraGroupUser" (
11+
"id" INTEGER NOT NULL,
12+
"group_id" INTEGER NOT NULL,
13+
"user_id" INTEGER NOT NULL,
14+
15+
CONSTRAINT "IntraGroupUser_pkey" PRIMARY KEY ("id")
16+
);
17+
18+
-- AddForeignKey
19+
ALTER TABLE "IntraGroupUser" ADD CONSTRAINT "IntraGroupUser_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "IntraUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
20+
21+
-- AddForeignKey
22+
ALTER TABLE "IntraGroupUser" ADD CONSTRAINT "IntraGroupUser_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "IntraGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ model IntraUser {
246246
// Relations
247247
coalition_users IntraCoalitionUser[]
248248
cursus_users IntraCursusUser[]
249+
group_users IntraGroupUser[]
249250
250251
// Codam
251252
codam_user CodamUser?
@@ -323,6 +324,29 @@ model IntraCoalitionUser {
323324
coalition IntraCoalition @relation(fields: [coalition_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
324325
}
325326

327+
// Intra Group
328+
model IntraGroup {
329+
id Int @id
330+
name String
331+
332+
// Relations
333+
group_users IntraGroupUser[]
334+
}
335+
336+
// Intra Group User
337+
// Links a user to a group
338+
model IntraGroupUser {
339+
id Int @id
340+
group_id Int @map("group_id")
341+
user_id Int @map("user_id")
342+
343+
// Relations
344+
user IntraUser @relation(fields: [user_id], references: [id])
345+
group IntraGroup @relation(fields: [group_id], references: [id])
346+
}
347+
348+
// Intra Cursus User
349+
// Links a user to a cursus
326350
model IntraCursusUser {
327351
id Int @id
328352
cursus_id Int @map("cursus_id")

src/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export const INTRA_API_UID = process.env.INTRA_API_UID!;
77
export const INTRA_API_SECRET = process.env.INTRA_API_SECRET!;
88
export const CAMPUS_ID: number = parseInt(process.env.INTRA_CAMPUS_ID!);
99
export const CURSUS_ID: number = parseInt(process.env.INTRA_CURSUS_ID!);
10+
export const ASSISTANT_GROUP_ID: number = parseInt(process.env.INTRA_ASSISTANT_GROUP_ID!);
1011
export const INTRA_TEST_ACCOUNTS: string[] = (process.env.INTRA_TEST_ACCOUNTS || '').split(',');
12+
export const ASSISTANTS_CAN_QUIZ: boolean = process.env.ASSISTANTS_CAN_QUIZ === 'true';
1113

1214
// Not defined in .env but in regular shell environment variables
1315
export const DEV_DAYS_LIMIT: number = process.env.DEV_DAYS_LIMIT ? parseInt(process.env.DEV_DAYS_LIMIT) : 365;

src/handlers/authentication.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import OAuth2Strategy from 'passport-oauth2';
44
import { PrismaClient } from '@prisma/client';
55
import { INTRA_API_UID, INTRA_API_SECRET, URL_ORIGIN, SESSION_SECRET, NODE_ENV } from '../env';
66
import { getIntraUser, ExpressIntraUser } from '../sync/oauth';
7-
import { isStudent, isStaff } from '../utils';
7+
import { isStudent, isStaff, isAssistant } from '../utils';
88
import { PrismaSessionStore } from '@quixo3/prisma-session-store';
99

1010
export const setupPassport = function(prisma: PrismaClient): void {
@@ -53,6 +53,7 @@ export const setupPassport = function(prisma: PrismaClient): void {
5353
kind: user.kind,
5454
isStudent: await isStudent(prisma, user),
5555
isStaff: await isStaff(user),
56+
isAssistant: await isAssistant(prisma, user),
5657
image_url: user.image,
5758
};
5859
cb(null, intraUser);

src/routes/quiz.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ExpressIntraUser } from '../sync/oauth';
77
import { getAPIClient } from '../utils';
88
import { fetchSingle42ApiPage } from '../sync/base';
99
import { syncCoalitionUser } from '../sync/coalitions_users';
10-
import { CURSUS_ID } from '../env';
10+
import { ASSISTANTS_CAN_QUIZ, CURSUS_ID } from '../env';
1111

1212
export interface QuizSessionQuestion {
1313
question: CodamCoalitionTestQuestion;
@@ -59,11 +59,9 @@ export const isQuizAvailable = async function(user: IntraUser | ExpressIntraUser
5959
});
6060
const currentDate = new Date();
6161
const availableDueToTime = (currentDate.getTime() >= settings.start_at.getTime() && currentDate.getTime() < settings.deadline_at.getTime());
62-
if (availableDueToTime) {
63-
return true; // Skip any further database queries, the questionnaire is available for everyone!
64-
}
6562

6663
// If the user is not part of any coalition currently, taking the questionnaire is always allowed, as long as their cursus is ongoing
64+
// Also allow assistants to take the quiz if the relevant env var is set
6765
const userDetails = await prisma.intraUser.findFirst({
6866
where: {
6967
id: user.id,
@@ -94,13 +92,31 @@ export const isQuizAvailable = async function(user: IntraUser | ExpressIntraUser
9492
end_at: true,
9593
},
9694
},
95+
group_users: {
96+
select: {
97+
id: true,
98+
},
99+
where: {
100+
group_id: parseInt(process.env.INTRA_ASSISTANT_GROUP_ID || '0'),
101+
},
102+
},
97103
},
98104
});
99105
if (!userDetails) {
100106
console.warn(`User ${user.id} not found in database when checking quiz availability`);
101107
return false;
102108
}
103-
return (userDetails.coalition_users.length == 0 && userDetails.cursus_users.length > 0 && !userDetails.cursus_users[0].end_at);
109+
if (userDetails.coalition_users.length === 0 || availableDueToTime) {
110+
if (userDetails.cursus_users.length > 0 && !userDetails.cursus_users[0].end_at) {
111+
console.log(`User ${user.id} has an ongoing cursus in cursus ${CURSUS_ID}, allowing to take the questionnaire`);
112+
return true; // User has an ongoing cursus in the relevant cursus, allow taking the questionnaire
113+
}
114+
if (userDetails.group_users.length > 0 && ASSISTANTS_CAN_QUIZ) {
115+
console.log(`User ${user.id} is an assistant and assistants are allowed to take the quiz due to env var ASSISTANTS_CAN_QUIZ, allowing to take the questionnaire`);
116+
return true; // User is an assistant and assistants are allowed to take the quiz due to env var ASSISTANTS_CAN_QUIZ
117+
}
118+
}
119+
return false;
104120
}
105121

106122
const resetQuizSession = async function(req: Request, userSession: CustomSessionData): Promise<void> {

src/sync/base.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getBlocAtDate } from '../utils';
1515
import { handleRankingTitleCreation, handleRankingBonuses } from './rankings';
1616
import { syncTitles } from './titles';
1717
import { calculateResults } from './results';
18+
import { syncGroups, syncGroupsUsers } from './groups';
1819

1920
export const prisma = new PrismaClient();
2021

@@ -229,6 +230,8 @@ export const syncWithIntra = async function(api: Fast42): Promise<void> {
229230
await syncProjects(api, lastSync, now);
230231
await syncUsers(api, lastSync, now);
231232
await syncCursusUsers(api, lastSync, now);
233+
await syncGroups(api, lastSync, now);
234+
await syncGroupsUsers(api, now);
232235
await syncBlocs(api, now); // also syncs coalitions
233236
await syncCoalitionUsers(api, lastSync, now);
234237
await handleRankingTitleCreation(api);

src/sync/groups.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import Fast42 from '@codam/fast42';
2+
import { prisma, syncDataCB } from './base';
3+
import { ASSISTANT_GROUP_ID } from '../env';
4+
5+
export const syncGroups = async function(api: Fast42, syncSince: Date, syncDate: Date): Promise<void> {
6+
// We only sync the assistant group (id defined in ASSISTANT_GROUP_ID)
7+
await syncDataCB(api, syncDate, syncSince, `/groups/${ASSISTANT_GROUP_ID}`, {}, async (group) => {
8+
try {
9+
await prisma.intraGroup.upsert({
10+
where: {
11+
id: group.id,
12+
},
13+
update: {
14+
name: group.name,
15+
},
16+
create: {
17+
id: group.id,
18+
name: group.name,
19+
}
20+
});
21+
}
22+
catch (err) {
23+
console.error(`Error syncing group ${group.id}: ${err}`);
24+
}
25+
});
26+
};
27+
28+
export const syncGroupsUsers = async function(api: Fast42, syncDate: Date): Promise<void> {
29+
const groups = await prisma.intraGroup.findMany({});
30+
31+
for (const group of groups) {
32+
await syncDataCB(api, syncDate, undefined, `/groups/${group.id}/groups_users`, {}, async (groupsUsers) => {
33+
// Delete all group_users
34+
await prisma.intraGroupUser.deleteMany({
35+
where: {
36+
group_id: group.id,
37+
},
38+
});
39+
40+
for (const groupUser of groupsUsers) {
41+
console.debug(`Syncing a group_user (user ${groupUser.user_id} in group "${groupUser.group.name}")...`);
42+
43+
try {
44+
const user = await prisma.intraUser.findFirst({
45+
where: {
46+
id: groupUser.user_id,
47+
},
48+
select: {
49+
id: true, // Minimal select to check if user exists
50+
},
51+
});
52+
if (!user) {
53+
continue; // User is likely not from our campus
54+
}
55+
56+
await prisma.intraGroupUser.upsert({
57+
where: {
58+
id: groupUser.id,
59+
},
60+
update: {
61+
user_id: groupUser.user_id,
62+
},
63+
create: {
64+
id: groupUser.id,
65+
user: {
66+
connect: {
67+
id: groupUser.user_id,
68+
},
69+
},
70+
group: {
71+
connect: {
72+
id: groupUser.group.id,
73+
},
74+
},
75+
}
76+
});
77+
}
78+
catch (err) {
79+
console.error(`Error syncing group_user ${groupUser.id}: ${err}`);
80+
}
81+
}
82+
});
83+
}
84+
};

src/sync/oauth.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import https from 'https';
22
import { CAMPUS_ID } from '../env';
3-
import { isStudent, isStaff } from '../utils';
3+
import { isStudent, isStaff, isAssistant } from '../utils';
44
import { prisma } from './base';
55

66
export interface ExpressIntraUser extends Express.User {
@@ -13,8 +13,9 @@ export interface ExpressIntraUser extends Express.User {
1313
usual_full_name: string;
1414
display_name: string;
1515
kind: string;
16-
isStudent: boolean; // can be false for pisciners
16+
isStudent: boolean; // true if the user is active in the CURSUS_ID cursus
1717
isStaff: boolean;
18+
isAssistant: boolean; // true if the user is part of the ASSISTANT_GROUP_ID group on Intra (C.A.T. team)
1819
image_url: string | null;
1920
};
2021

@@ -65,6 +66,7 @@ export const getIntraUser = async function(accessToken: string): Promise<Express
6566
kind: me.kind,
6667
isStudent: await isStudent(prisma, me),
6768
isStaff: await isStaff(me),
69+
isAssistant: await isAssistant(prisma, me),
6870
image_url: (me.image && me.image.link ? me.image.versions.medium : null),
6971
};
7072

src/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { PrismaClient, IntraUser, IntraCoalition, IntraBlocDeadline, CodamCoalit
22
import { ExpressIntraUser } from "./sync/oauth";
33
import Fast42 from "@codam/fast42";
44
import { api } from "./main";
5-
import { CURSUS_ID } from "./env";
5+
import { ASSISTANT_GROUP_ID, CURSUS_ID } from "./env";
66
import NodeCache from "node-cache";
77
import { Request } from "express";
88

@@ -78,6 +78,19 @@ export const isStaff = async function(intraUser: ExpressIntraUser | IntraUser):
7878
return intraUser.kind === 'admin';
7979
};
8080

81+
export const isAssistant = async function(prisma: PrismaClient, intraUser: ExpressIntraUser | IntraUser): Promise<boolean> {
82+
if (isNaN(ASSISTANT_GROUP_ID)) {
83+
return false;
84+
}
85+
const groupUser = await prisma.intraGroupUser.findFirst({
86+
where: {
87+
group_id: ASSISTANT_GROUP_ID,
88+
user_id: intraUser.id,
89+
},
90+
});
91+
return (groupUser !== null);
92+
};
93+
8194
export const getCoalitionIds = async function(prisma: PrismaClient): Promise<any> {
8295
const coalitionIds = await prisma.intraCoalition.findMany({
8396
select: {

0 commit comments

Comments
 (0)