1use crate::prelude::*;
2
3#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type)]
4#[cfg_attr(feature = "ts_rs", derive(TS))]
5#[sqlx(type_name = "user_role", rename_all = "snake_case")]
6pub enum UserRole {
7 Reviewer,
8 Assistant,
9 Teacher,
10 Admin,
11 CourseOrExamCreator,
12 MaterialViewer,
13 TeachingAndLearningServices,
14 StatsViewer,
15}
16
17#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
18pub struct Role {
19 pub is_global: bool,
20 pub organization_id: Option<Uuid>,
21 pub course_id: Option<Uuid>,
22 pub course_instance_id: Option<Uuid>,
23 pub exam_id: Option<Uuid>,
24 pub role: UserRole,
25 pub user_id: Uuid,
26}
27
28impl Role {
29 pub fn is_global(&self) -> bool {
30 self.is_global
31 }
32
33 pub fn is_role_for_organization(&self, organization_id: Uuid) -> bool {
34 self.organization_id
35 .map(|id| id == organization_id)
36 .unwrap_or_default()
37 }
38
39 pub fn is_role_for_course(&self, course_id: Uuid) -> bool {
40 self.course_id.map(|id| id == course_id).unwrap_or_default()
41 }
42
43 pub fn is_role_for_course_instance(&self, course_instance_id: Uuid) -> bool {
44 self.course_instance_id
45 .map(|id| id == course_instance_id)
46 .unwrap_or_default()
47 }
48
49 pub fn is_role_for_exam(&self, exam_id: Uuid) -> bool {
50 self.exam_id.map(|id| id == exam_id).unwrap_or_default()
51 }
52
53 pub fn domain_description(&self) -> String {
55 if self.is_global {
56 "Global".to_string()
57 } else if let Some(id) = self.organization_id {
58 return format!("Organization {}", id);
59 } else if let Some(id) = self.course_id {
60 return format!("Course {}", id);
61 } else if let Some(id) = self.course_instance_id {
62 return format!("CourseInstance {}", id);
63 } else if let Some(id) = self.exam_id {
64 return format!("Exam {}", id);
65 } else {
66 return "Unknown domain".to_string();
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, Deserialize)]
72#[cfg_attr(feature = "ts_rs", derive(TS))]
73#[serde(tag = "tag", content = "id")]
74pub enum RoleDomain {
75 Global,
76 Organization(Uuid),
77 Course(Uuid),
78 CourseInstance(Uuid),
79 Exam(Uuid),
80}
81
82#[derive(Debug, Deserialize)]
83#[cfg_attr(feature = "ts_rs", derive(TS))]
84pub struct RoleInfo {
85 pub email: String,
86 pub role: UserRole,
87 pub domain: RoleDomain,
88}
89
90#[derive(Debug, Serialize)]
91#[cfg_attr(feature = "ts_rs", derive(TS))]
92pub struct RoleUser {
93 pub id: Uuid,
94 pub first_name: Option<String>,
95 pub last_name: Option<String>,
96 pub email: String,
97 pub role: UserRole,
98}
99
100pub async fn get(conn: &mut PgConnection, domain: RoleDomain) -> ModelResult<Vec<RoleUser>> {
101 let users = match domain {
102 RoleDomain::Global => {
103 sqlx::query_as!(
104 RoleUser,
105 r#"
106SELECT users.id AS "id!",
107 user_details.first_name,
108 user_details.last_name,
109 user_details.email,
110 role AS "role!: UserRole"
111FROM users
112 JOIN roles ON users.id = roles.user_id
113 JOIN user_details ON users.id = user_details.user_id
114WHERE is_global = TRUE
115AND roles.deleted_at IS NULL
116"#,
117 )
118 .fetch_all(conn)
119 .await?
120 }
121 RoleDomain::Organization(id) => {
122 sqlx::query_as!(
123 RoleUser,
124 r#"
125SELECT users.id,
126 user_details.first_name,
127 user_details.last_name,
128 user_details.email,
129 role AS "role: UserRole"
130FROM users
131 JOIN roles ON users.id = roles.user_id
132 JOIN user_details ON users.id = user_details.user_id
133WHERE roles.organization_id = $1
134AND roles.deleted_at IS NULL
135"#,
136 id
137 )
138 .fetch_all(conn)
139 .await?
140 }
141 RoleDomain::Course(id) => {
142 sqlx::query_as!(
143 RoleUser,
144 r#"
145SELECT users.id,
146 user_details.first_name,
147 user_details.last_name,
148 user_details.email,
149 role AS "role: UserRole"
150FROM users
151 JOIN roles ON users.id = roles.user_id
152 JOIN user_details ON users.id = user_details.user_id
153WHERE roles.course_id = $1
154AND roles.deleted_at IS NULL
155"#,
156 id
157 )
158 .fetch_all(conn)
159 .await?
160 }
161 RoleDomain::CourseInstance(id) => {
162 sqlx::query_as!(
163 RoleUser,
164 r#"
165SELECT users.id,
166 user_details.first_name,
167 user_details.last_name,
168 user_details.email,
169 role AS "role: UserRole"
170FROM users
171 JOIN roles ON users.id = roles.user_id
172 JOIN user_details ON users.id = user_details.user_id
173WHERE roles.course_instance_id = $1
174AND roles.deleted_at IS NULL
175"#,
176 id
177 )
178 .fetch_all(conn)
179 .await?
180 }
181 RoleDomain::Exam(id) => {
182 sqlx::query_as!(
183 RoleUser,
184 r#"
185SELECT users.id,
186 user_details.first_name,
187 user_details.last_name,
188 user_details.email,
189 role AS "role: UserRole"
190FROM users
191 JOIN roles ON users.id = roles.user_id
192 JOIN user_details ON users.id = user_details.user_id
193WHERE roles.exam_id = $1
194AND roles.deleted_at IS NULL
195"#,
196 id
197 )
198 .fetch_all(conn)
199 .await?
200 }
201 };
202 Ok(users)
203}
204
205pub async fn insert(
206 conn: &mut PgConnection,
207 user_id: Uuid,
208 role: UserRole,
209 domain: RoleDomain,
210) -> ModelResult<Uuid> {
211 let id = match domain {
212 RoleDomain::Global => {
213 sqlx::query!(
214 "
215INSERT INTO roles (user_id, role, is_global)
216VALUES ($1, $2, True)
217RETURNING id
218",
219 user_id,
220 role as UserRole
221 )
222 .fetch_one(conn)
223 .await?
224 .id
225 }
226 RoleDomain::Organization(id) => {
227 sqlx::query!(
228 "
229INSERT INTO roles (user_id, role, organization_id)
230VALUES ($1, $2, $3)
231RETURNING id
232",
233 user_id,
234 role as UserRole,
235 id
236 )
237 .fetch_one(conn)
238 .await?
239 .id
240 }
241 RoleDomain::Course(id) => {
242 sqlx::query!(
243 "
244INSERT INTO roles (user_id, role, course_id)
245VALUES ($1, $2, $3)
246RETURNING id
247",
248 user_id,
249 role as UserRole,
250 id
251 )
252 .fetch_one(conn)
253 .await?
254 .id
255 }
256 RoleDomain::CourseInstance(id) => {
257 sqlx::query!(
258 "
259INSERT INTO roles (user_id, role, course_instance_id)
260VALUES ($1, $2, $3)
261RETURNING id
262",
263 user_id,
264 role as UserRole,
265 id
266 )
267 .fetch_one(conn)
268 .await?
269 .id
270 }
271 RoleDomain::Exam(id) => {
272 sqlx::query!(
273 "
274INSERT INTO roles (user_id, role, exam_id)
275VALUES ($1, $2, $3)
276RETURNING id
277",
278 user_id,
279 role as UserRole,
280 id
281 )
282 .fetch_one(conn)
283 .await?
284 .id
285 }
286 };
287 Ok(id)
288}
289
290pub async fn remove(
291 conn: &mut PgConnection,
292 user_id: Uuid,
293 role: UserRole,
294 domain: RoleDomain,
295) -> ModelResult<()> {
296 match domain {
297 RoleDomain::Global => {
298 sqlx::query!(
299 "
300UPDATE roles
301SET deleted_at = NOW()
302WHERE user_id = $1
303 AND role = $2
304 AND deleted_at IS NULL
305",
306 user_id,
307 role as UserRole
308 )
309 .execute(conn)
310 .await?;
311 }
312 RoleDomain::Organization(id) => {
313 sqlx::query!(
314 "
315UPDATE roles
316SET deleted_at = NOW()
317WHERE user_id = $1
318 AND role = $2
319 AND organization_id = $3
320 AND deleted_at IS NULL
321",
322 user_id,
323 role as UserRole,
324 id
325 )
326 .execute(conn)
327 .await?;
328 }
329 RoleDomain::Course(id) => {
330 sqlx::query!(
331 "
332UPDATE roles
333SET deleted_at = NOW()
334WHERE user_id = $1
335 AND role = $2
336 AND course_id = $3
337 AND deleted_at IS NULL
338",
339 user_id,
340 role as UserRole,
341 id
342 )
343 .execute(conn)
344 .await?;
345 }
346 RoleDomain::CourseInstance(id) => {
347 sqlx::query!(
348 "
349UPDATE roles
350SET deleted_at = NOW()
351WHERE user_id = $1
352 AND role = $2
353 AND course_instance_id = $3
354 AND deleted_at IS NULL
355",
356 user_id,
357 role as UserRole,
358 id
359 )
360 .execute(conn)
361 .await?;
362 }
363 RoleDomain::Exam(id) => {
364 sqlx::query!(
365 "
366UPDATE roles
367SET deleted_at = NOW()
368WHERE user_id = $1
369 AND role = $2
370 AND exam_id = $3
371 AND deleted_at IS NULL
372",
373 user_id,
374 role as UserRole,
375 id
376 )
377 .execute(conn)
378 .await?;
379 }
380 }
381 Ok(())
382}
383
384pub async fn get_roles(conn: &mut PgConnection, user_id: Uuid) -> ModelResult<Vec<Role>> {
385 let roles = sqlx::query_as!(
386 Role,
387 r#"
388SELECT is_global,
389 organization_id,
390 course_id,
391 course_instance_id,
392 exam_id,
393 role AS "role: UserRole",
394 user_id
395FROM roles
396WHERE user_id = $1
397AND roles.deleted_at IS NULL
398"#,
399 user_id
400 )
401 .fetch_all(conn)
402 .await?;
403 Ok(roles)
404}
405
406pub async fn get_course_related_roles(
413 conn: &mut PgConnection,
414 course_id: Uuid,
415) -> ModelResult<Vec<Role>> {
416 let roles = sqlx::query_as!(
417 Role,
418 r#"
419WITH course_org AS (
420 SELECT organization_id
421 FROM courses
422 WHERE id = $1
423 AND deleted_at IS NULL
424)
425SELECT is_global,
426 organization_id,
427 course_id,
428 course_instance_id,
429 exam_id,
430 role AS "role: UserRole",
431 user_id
432FROM roles
433WHERE (
434 is_global = TRUE
435 OR organization_id = (
436 SELECT organization_id
437 FROM course_org
438 )
439 OR course_id = $1
440 OR course_instance_id IN (
441 SELECT id
442 FROM course_instances
443 WHERE course_id = $1
444 AND deleted_at IS NULL
445 )
446 )
447 AND deleted_at IS NULL
448"#,
449 course_id
450 )
451 .fetch_all(conn)
452 .await?;
453
454 Ok(roles)
455}
456
457pub async fn get_course_language_group_related_roles(
461 conn: &mut PgConnection,
462 course_language_group_id: Uuid,
463) -> ModelResult<Vec<Role>> {
464 let roles = sqlx::query_as!(
465 Role,
466 r#"
467WITH course_org AS (
468 SELECT DISTINCT organization_id
469 FROM courses
470 WHERE course_language_group_id = $1
471 AND deleted_at IS NULL
472)
473SELECT is_global,
474 organization_id,
475 course_id,
476 course_instance_id,
477 exam_id,
478 role AS "role: UserRole",
479 user_id
480FROM roles
481WHERE (
482 is_global = TRUE
483 OR organization_id IN (
484 SELECT organization_id
485 FROM course_org
486 )
487 OR course_id IN (
488 SELECT id
489 FROM courses
490 WHERE course_language_group_id = $1
491 AND deleted_at IS NULL
492 )
493 OR course_instance_id IN (
494 SELECT ci.id
495 FROM course_instances ci
496 JOIN courses c ON ci.course_id = c.id
497 WHERE c.course_language_group_id = $1
498 AND ci.deleted_at IS NULL
499 )
500 )
501 AND deleted_at IS NULL
502"#,
503 course_language_group_id
504 )
505 .fetch_all(conn)
506 .await?;
507
508 Ok(roles)
509}