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