1use time;
16
17use config::OauthProvider;
18use db_conn::MedalConnection;
19#[cfg(feature = "signup")]
20use db_conn::SignupResult;
21use db_objects::OptionSession;
22use db_objects::SessionUser;
23use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
24use helpers;
25use webauthn_rs::prelude as webauthn;
26use webfw_iron::{json_val, to_json};
27
28#[derive(Serialize, Deserialize)]
29pub struct SubTaskInfo {
30 pub id: i32,
31 pub linktext: String,
32 pub active: bool,
33 pub greyout: bool,
34}
35
36#[derive(Serialize, Deserialize)]
37pub struct TaskInfo {
38 pub name: String,
39 pub subtasks: Vec<SubTaskInfo>,
40}
41
42#[derive(Clone, Serialize, Deserialize)]
43pub struct ContestInfo {
44 pub id: i32,
45 pub name: String,
46 pub duration: i32,
47 pub public: bool,
48 pub requires_login: bool,
49 pub image: Option<String>,
50 pub language: Option<String>,
51 pub category: Option<String>,
52 pub team_participation: bool,
53 pub colour: Option<String>,
54 pub tags: Vec<String>,
55}
56
57#[derive(Clone, Debug)]
58pub enum MedalError {
59 NotLoggedIn,
60 AccessDenied,
61 CsrfCheckFailed,
62 SessionTimeout,
63 DatabaseError,
64 ConfigurationError,
65 DatabaseConnectionError,
66 PasswordHashingError,
67 UnmatchedPasswords,
68 NotFound,
69 AccountIncomplete,
70 UnknownId,
71 OauthError(String),
72 WebauthnError,
73 ErrorWithJson(json_val::Map<String, json_val::Value>),
74}
75
76pub struct LoginInfo {
77 pub password_login: bool,
78 pub self_url: Option<String>,
79 pub oauth_providers: Option<Vec<OauthProvider>>,
80}
81
82type MedalValue = (String, json_val::Map<String, json_val::Value>);
83type MedalResult<T> = Result<T, MedalError>;
84type MedalValueResult = MedalResult<MedalValue>;
85
86type JsonValue = json_val::Map<String, json_val::Value>;
87type JsonValueResult = MedalResult<JsonValue>;
88
89fn fill_user_data_prefix(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>, prefix: &str) {
90 data.insert(prefix.to_string() + "username", to_json(&session.username));
91 data.insert(prefix.to_string() + "firstname", to_json(&session.firstname));
92 data.insert(prefix.to_string() + "lastname", to_json(&session.lastname));
93 data.insert(prefix.to_string() + "teacher", to_json(&session.is_teacher));
94 data.insert(prefix.to_string() + "is_teacher", to_json(&session.is_teacher));
95 data.insert(prefix.to_string() + "teacher_schoolname", to_json(&session.school_name));
96 data.insert(prefix.to_string() + "admin", to_json(&session.is_admin));
97 data.insert(prefix.to_string() + "is_admin", to_json(&session.is_admin));
98 data.insert(prefix.to_string() + "logged_in", to_json(&session.is_logged_in()));
99 data.insert(prefix.to_string() + "csrf_token", to_json(&session.csrf_token));
100 data.insert(prefix.to_string() + "sex",
101 to_json(&(match session.sex {
102 Some(0) | None => "/",
103 Some(1) => "m",
104 Some(2) => "w",
105 Some(3) => "d",
106 Some(4) => "…",
107 _ => "?",
108 })));
109}
110
111fn fill_user_data(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>) {
112 fill_user_data_prefix(session, data, "");
113
114 data.insert("parent".to_string(), to_json(&"base"));
115 data.insert("medal_version".to_string(), to_json(&env!("GIT_VERSION")));
116}
117
118fn fill_oauth_data(login_info: LoginInfo, data: &mut json_val::Map<String, serde_json::Value>) {
119 let mut oauth_links: Vec<(String, String, String)> = Vec::new();
120 if let Some(oauth_providers) = login_info.oauth_providers {
121 for oauth_provider in oauth_providers {
122 oauth_links.push((oauth_provider.provider_id.to_owned(),
123 oauth_provider.login_link_text.to_owned(),
124 oauth_provider.url.to_owned()));
125 }
126 }
127
128 data.insert("self_url".to_string(), to_json(&login_info.self_url));
129 data.insert("oauth_links".to_string(), to_json(&oauth_links));
130
131 data.insert("password_login".to_string(), to_json(&login_info.password_login));
132}
133
134fn grade_to_string(grade: i32) -> String {
135 match grade {
136 0 => "Noch kein Schüler".to_string(),
137 n @ 1..=10 => format!("{}", n),
138 11 => "11 (G8)".to_string(),
139 12 => "12 (G8)".to_string(),
140 111 => "11 (G9)".to_string(),
141 112 => "12 (G9)".to_string(),
142 113 => "13 (G9)".to_string(),
143 114 => "Berufsschule".to_string(),
144 255 => "Kein Schüler mehr".to_string(),
145 _ => "?".to_string(),
146 }
147}
148
149fn common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str {
150 let mut ac = a.char_indices();
151 let mut bc = b.char_indices();
152
153 let mut ai;
154 let mut bi;
155 loop {
156 ai = ac.next();
157 bi = bc.next();
158 if ai != bi {
159 break;
160 }
161 if ai.is_none() {
162 break;
163 }
164 }
165 if ai.is_none() {
166 return a;
167 }
168 if bi.is_none() {
169 return b;
170 }
171
172 a.get(..(ai.unwrap().0)).unwrap()
173}
174
175pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo) -> MedalValueResult {
176 let mut data = json_val::Map::new();
177
178 if let Some(token) = session_token {
179 if let Some(session) = conn.get_session(&token) {
180 fill_user_data(&session, &mut data);
181
182 if session.logincode.is_some() && session.firstname.is_none() {
183 return Err(MedalError::AccountIncomplete);
184 }
185 }
186 }
187
188 fill_oauth_data(login_info, &mut data);
189
190 let now = time::get_time();
191 let contest_list = conn.get_contest_list();
192 let mut contests_running: Vec<(String, String, (i64, i64))> =
193 contest_list.iter()
194 .filter(|c| c.public)
195 .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
196 .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
197 .filter(|c| c.end.map(|end| now <= end).unwrap_or(false))
198 .filter(|c| c.start.map(|start| now >= start).unwrap_or(true))
199 .map(|c| {
200 (c.name.clone(),
201 if let Some(cat) = c.category.as_ref() {
202 format!("{}/{}", cat, c.id.unwrap_or(0))
203 } else {
204 format!("{}", c.id.unwrap_or(0))
205 },
206 {
207 let t = c.end.unwrap().sec - now.sec;
208 (t / 60 / 60 / 24, t / 60 / 60 % 24)
209 })
210 })
211 .collect();
212
213 let mut i = 0;
214 while i < contests_running.len() {
215 let mut count = 0;
216 while i + 1 < contests_running.len() {
217 if contests_running[i].2 == contests_running[i + 1].2 {
218 contests_running[i].0 = common_prefix(&contests_running[i].0, &contests_running[i + 1].0).to_string();
221 count += 1;
222 contests_running.remove(i + 1);
223 } else {
224 break;
225 }
226 }
227 if count > 0 {
228 contests_running[i].0 = format!("{}… ({}x)", contests_running[i].0, count + 1);
229 contests_running[i].1 = "?filter=current".to_string();
230 }
231 i += 1;
232 }
233
234 if contests_running.len() > 0 {
235 data.insert("current_contests".to_string(), to_json(&contests_running));
236 }
237
238 let mut contests_comming: Vec<(String, String, (i64, i64))> =
239 contest_list.iter()
240 .filter(|c| c.public)
241 .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
242 .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
243 .filter(|c| c.start.map(|start| now <= start).unwrap_or(false))
244 .map(|c| {
245 (c.name.clone(),
246 if let Some(cat) = c.category.as_ref() {
247 format!("{}/{}", cat, c.id.unwrap_or(0))
248 } else {
249 format!("{}", c.id.unwrap_or(0))
250 },
251 {
252 let t = c.start.unwrap().sec - now.sec;
253 (t / 60 / 60 / 24, t / 60 / 60 % 24)
254 })
255 })
256 .collect();
257
258 let mut i = 0;
259 while i < contests_comming.len() {
260 let mut count = 0;
261 while i + 1 < contests_comming.len() {
262 if contests_comming[i].2 == contests_comming[i + 1].2 {
263 contests_comming[i].0 = common_prefix(&contests_comming[i].0, &contests_comming[i + 1].0).to_string();
266 count += 1;
267 contests_comming.remove(i + 1);
268 } else {
269 break;
270 }
271 }
272 if count > 0 {
273 contests_comming[i].0 = format!("{}… ({}x)", contests_comming[i].0, count + 1);
274 contests_comming[i].1 = "?filter=current".to_string();
275 }
276 i += 1;
277 }
278
279 if contests_comming.len() > 0 {
280 data.insert("upcoming_contests".to_string(), to_json(&contests_comming));
281 }
282
283 data.insert("parent".to_string(), to_json(&"base"));
284 data.insert("index".to_string(), to_json(&true));
285 Ok(("index".to_owned(), data))
286}
287
288pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
289 -> (String, json_val::Map<String, json_val::Value>) {
290 let mut data = json_val::Map::new();
291
292 if let Some(token) = session_token {
293 if let Some(session) = conn.get_session(&token) {
294 fill_user_data(&session, &mut data);
295 }
296 }
297
298 fill_oauth_data(login_info, &mut data);
299
300 data.insert("parent".to_string(), to_json(&"base"));
301 ("login".to_owned(), data)
302}
303
304pub fn status<T: MedalConnection>(conn: &T, config_secret: Option<String>, given_secret: Option<String>)
305 -> MedalResult<String> {
306 if config_secret == given_secret {
307 Ok(conn.get_debug_information())
308 } else {
309 Err(MedalError::AccessDenied)
310 }
311}
312
313pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
314 -> (String, json_val::Map<String, json_val::Value>) {
315 let mut data = json_val::Map::new();
316
317 if let Some(token) = session_token {
318 if let Some(session) = conn.get_session(&token) {
319 data.insert("known_session".to_string(), to_json(&true));
320 data.insert("session_id".to_string(), to_json(&session.id));
321 data.insert("now_timestamp".to_string(), to_json(&time::get_time().sec));
322 if let Some(last_activity) = session.last_activity {
323 data.insert("session_timestamp".to_string(), to_json(&last_activity.sec));
324 data.insert("timediff".to_string(), to_json(&(time::get_time() - last_activity).num_seconds()));
325 }
326 if session.is_alive() {
327 data.insert("alive_session".to_string(), to_json(&true));
328 if session.is_logged_in() {
329 data.insert("logged_in".to_string(), to_json(&true));
330 data.insert("username".to_string(), to_json(&session.username));
331 data.insert("firstname".to_string(), to_json(&session.firstname));
332 data.insert("lastname".to_string(), to_json(&session.lastname));
333 data.insert("teacher".to_string(), to_json(&session.is_teacher));
334 data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
335 data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
336 data.insert("logincode".to_string(), to_json(&session.logincode));
337 data.insert("managed_by".to_string(), to_json(&session.managed_by));
338 }
339 }
340 }
341 data.insert("session".to_string(), to_json(&token));
342 } else {
343 data.insert("session".to_string(), to_json(&"No session token given"));
344 }
345
346 ("debug".to_owned(), data)
347}
348
349pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
350 if let Some(token) = session_token {
351 conn.get_session_or_new(&token).unwrap();
352 }
353}
354
355#[derive(PartialEq, Eq, Debug)]
356pub enum ContestVisibility {
357 All,
358 Open,
359 Current,
360 LoginRequired,
361 StandaloneTask,
362 WithSecret(String),
363}
364
365pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, category: Option<String>,
366 login_info: LoginInfo, visibility: ContestVisibility, is_results: bool)
367 -> MedalValueResult {
368 let mut data = json_val::Map::new();
369
370 let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
371 fill_user_data(&session, &mut data);
372
373 if session.is_logged_in() {
374 data.insert("can_start".to_string(), to_json(&true));
375 }
376
377 fill_oauth_data(login_info, &mut data);
378
379 let contest_list = if is_results {
380 conn.get_contest_list_with_group_member_participations(session.id)
381 } else {
382 conn.get_contest_list()
383 };
384
385 let now = time::get_time();
386 let v: Vec<ContestInfo> =
387 contest_list.iter()
388 .filter(|c| category.as_ref().map(|cat| c.category.as_ref() == Some(cat)).unwrap_or(true))
389 .filter(|c| c.public || matches!(visibility, ContestVisibility::WithSecret(_)))
390 .filter(|c| {
391 if let ContestVisibility::WithSecret(secret) = &visibility {
392 c.secret.as_ref() == Some(secret)
393 } else {
394 true
395 }
396 })
397 .filter(|c| {
398 (!c.standalone_task.unwrap_or(false))
399 || visibility == ContestVisibility::StandaloneTask
400 || category == Some("standalone_task".to_string())
401 })
402 .filter(|c| c.standalone_task.unwrap_or(false) || visibility != ContestVisibility::StandaloneTask)
403 .filter(|c| {
404 c.end.map(|end| now <= end).unwrap_or(true)
405 || (visibility == ContestVisibility::All && (c.category.is_none() || is_results))
406 })
407 .filter(|c| c.duration == 0 || visibility != ContestVisibility::Open)
408 .filter(|c| c.duration != 0 || visibility != ContestVisibility::Current)
409 .filter(|c| c.requires_login.unwrap_or(false) || visibility != ContestVisibility::LoginRequired)
410 .filter(|c| {
411 !c.requires_login.unwrap_or(false)
412 || visibility == ContestVisibility::LoginRequired
413 || visibility == ContestVisibility::All
414 })
415 .map(|c| ContestInfo { id: c.id.unwrap(),
416 name: c.name.clone(),
417 duration: c.duration,
418 public: c.public,
419 requires_login: c.requires_login.unwrap_or(false),
420 image: c.image.as_ref().map(|i| format!("/{}{}", c.location, i)),
421 language: c.language.clone(),
422 category: c.category.clone(),
423 team_participation: false,
424 colour: c.colour.clone(),
425 tags: c.tags.clone() })
426 .collect();
427
428 if category.is_some() {
429 data.insert("contests".to_string(), to_json(&v));
430 } else {
431 let contests_training: Vec<ContestInfo> =
432 v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration == 0).collect();
433 let contests_contest: Vec<ContestInfo> =
434 v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration != 0).collect();
435 let contests_challenge: Vec<ContestInfo> = v.into_iter().filter(|c| c.requires_login).collect();
436
437 data.insert("contests_training".to_string(), to_json(&contests_training));
438 data.insert("contests_contest".to_string(), to_json(&contests_contest));
439 data.insert("contests_challenge".to_string(), to_json(&contests_challenge));
440
441 data.insert("contests_training_header".to_string(), to_json(&"Trainingsaufgaben"));
442 data.insert("contests_contest_header".to_string(), to_json(&"Wettbewerbe"));
443 data.insert("contests_challenge_header".to_string(), to_json(&"Herausforderungen"));
444
445 if visibility == ContestVisibility::StandaloneTask {
446 data.insert("contests_training_header".to_string(), to_json(&"Einzelne Aufgaben ohne Wertung"));
447 }
448 }
449
450 if let ContestVisibility::WithSecret(secret) = visibility {
451 data.insert("secret".to_string(), to_json(&secret));
452 data.insert("has_secret".to_string(), to_json(&true));
453 }
454
455 Ok(("contests".to_owned(), data))
456}
457
458fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
459 let mut subtaskinfos = Vec::new();
460 let mut not_print_yet = true;
461 for st in &tg.tasks {
462 let mut blackstars: usize = 0;
463 if not_print_yet && st.stars >= grade.grade.unwrap_or(0) {
464 blackstars = grade.grade.unwrap_or(0) as usize;
465 not_print_yet = false;
466 }
467
468 let greyout = not_print_yet && st.stars < grade.grade.unwrap_or(0);
469 let active = ast.is_some() && st.id == ast;
470 let linktext = format!("{}{}",
471 str::repeat("★", blackstars as usize),
472 str::repeat("☆", st.stars as usize - blackstars as usize));
473 let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
474
475 subtaskinfos.push(si);
476 }
477 subtaskinfos
478}
479
480#[derive(Serialize, Deserialize)]
481pub struct ContestStartConstraints {
482 pub contest_not_begun: bool,
483 pub contest_over: bool,
484 pub contest_running: bool,
485 pub grade_too_low: bool,
486 pub grade_too_high: bool,
487 pub grade_matching: bool,
488}
489
490fn check_contest_qualification<T: MedalConnection>(conn: &T, session: &SessionUser, contest: &Contest) -> Option<bool> {
491 if contest.requires_contests.is_empty() {
492 return None;
495 }
496
497 for required_contest in &contest.requires_contests {
498 if required_contest.required_stars == 0 {
499 if conn.has_participation_by_contest_file(session.id, &contest.location, &required_contest.filename) {
501 return Some(true);
502 }
503 }
504 }
505
506 for required_contest in &contest.requires_contests {
507 if required_contest.required_stars != 0 {
508 if conn.has_participation_by_contest_file_and_stars(session.id,
510 &contest.location,
511 &required_contest.filename,
512 required_contest.required_stars)
513 {
514 return Some(true);
515 }
516 }
517 }
518
519 Some(false)
520}
521
522fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints {
523 let now = time::get_time();
524 let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 };
525
526 let contest_not_begun = contest.start.map(|start| now < start).unwrap_or(false);
527 let contest_over = contest.end.map(|end| now > end).unwrap_or(false);
528 let grade_too_low =
529 contest.min_grade.map(|min_grade| student_grade < min_grade && !session.is_teacher).unwrap_or(false);
530 let grade_too_high =
531 contest.max_grade.map(|max_grade| student_grade > max_grade && !session.is_teacher).unwrap_or(false);
532
533 let contest_running = !contest_not_begun && !contest_over;
534 let grade_matching = !grade_too_low && !grade_too_high;
535
536 ContestStartConstraints { contest_not_begun,
537 contest_over,
538 contest_running,
539 grade_too_low,
540 grade_too_high,
541 grade_matching }
542}
543
544#[derive(Serialize, Deserialize)]
545pub struct ContestTimeInfo {
546 pub passed_secs_total: i64,
547 pub left_secs_total: i64,
548 pub left_mins_total: i64,
549 pub left_hour: i64,
550 pub left_min: i64,
551 pub left_sec: i64,
552 pub has_timelimit: bool,
553 pub is_time_left: bool,
554 pub exempt_from_timelimit: bool,
555 pub can_still_compete: bool,
556 pub review_has_timelimit: bool,
557 pub has_future_review: bool,
558 pub has_review_end: bool,
559 pub is_review: bool,
560 pub can_still_compete_or_review: bool,
561
562 pub until_review_start_day: i64,
563 pub until_review_start_hour: i64,
564 pub until_review_start_min: i64,
565
566 pub until_review_end_day: i64,
567 pub until_review_end_hour: i64,
568 pub until_review_end_min: i64,
569}
570
571fn check_contest_time_left(session: &SessionUser, contest: &Contest, participation: &Participation) -> ContestTimeInfo {
572 let now = time::get_time();
573 let passed_secs_total = now.sec - participation.start.sec;
574 if passed_secs_total < 0 {
575 }
577 let left_secs_total = i64::from(contest.duration) * 60 - passed_secs_total;
578
579 let is_time_left = contest.duration == 0 || left_secs_total >= 0;
580 let exempt_from_timelimit = session.is_teacher() || session.is_admin();
581
582 let can_still_compete = is_time_left || exempt_from_timelimit;
583
584 let review_has_timelimit = contest.review_end.is_none() && contest.review_start.is_some();
585 let has_future_review = (contest.review_start.is_some() || contest.review_end.is_some())
586 && contest.review_end.map(|end| end > now).unwrap_or(true);
587 let has_review_end = contest.review_end.is_some();
588 let is_review = !can_still_compete
589 && (contest.review_start.is_some() || contest.review_end.is_some())
590 && contest.review_start.map(|start| now >= start).unwrap_or(true)
591 && contest.review_end.map(|end| now <= end).unwrap_or(true);
592
593 let until_review_start = contest.review_start.map(|start| start.sec - now.sec).unwrap_or(0);
594 let until_review_end = contest.review_end.map(|end| end.sec - now.sec).unwrap_or(0);
595
596 ContestTimeInfo { passed_secs_total,
597 left_secs_total,
598 left_mins_total: left_secs_total / 60,
599 left_hour: left_secs_total / (60 * 60),
600 left_min: (left_secs_total / 60) % 60,
601 left_sec: left_secs_total % 60,
602 has_timelimit: contest.duration != 0,
603 is_time_left,
604 exempt_from_timelimit,
605 can_still_compete,
606 review_has_timelimit,
607 has_future_review,
608 has_review_end,
609 is_review,
610 can_still_compete_or_review: can_still_compete || is_review,
611
612 until_review_start_day: until_review_start / (60 * 60 * 24),
613 until_review_start_hour: (until_review_start / (60 * 60)) % 24,
614 until_review_start_min: (until_review_start / 60) % 60,
615
616 until_review_end_day: until_review_end / (60 * 60 * 24),
617 until_review_end_hour: (until_review_end / (60 * 60)) % 24,
618 until_review_end_min: (until_review_end / 60) % 60 }
619}
620
621pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
622 query_string: Option<String>, login_info: LoginInfo, secret: Option<String>)
623 -> MedalResult<Result<MedalValue, i32>> {
624 let session = conn.get_session_or_new(&session_token).unwrap();
625
626 if session.logincode.is_some() && session.firstname.is_none() {
627 return Err(MedalError::AccountIncomplete);
628 }
629
630 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
631
632 let mut opt_part = conn.get_participation(session.id, contest_id);
633 let mut grades = if let Some(part) = opt_part.as_ref() {
634 if let Some(team) = part.team {
635 conn.get_contest_user_grades(team, contest_id)
636 } else {
637 conn.get_contest_user_grades(session.id, contest_id)
638 }
639 } else {
640 conn.get_contest_user_grades(session.id, contest_id)
641 };
642
643 let ci = ContestInfo { id: contest.id.unwrap(),
644 name: contest.name.clone(),
645 duration: contest.duration,
646 public: contest.public,
647 requires_login: contest.requires_login.unwrap_or(false),
648 image: None,
649 language: None,
650 category: contest.category.clone(),
651 team_participation: contest.max_teamsize.map(|size| size > 1).unwrap_or(false),
652 colour: contest.colour.clone(),
653 tags: Vec::new() };
654
655 let mut data = json_val::Map::new();
656 data.insert("parent".to_string(), to_json(&"base"));
657 data.insert("empty".to_string(), to_json(&"empty"));
658 data.insert("contest".to_string(), to_json(&ci));
659 data.insert("title".to_string(), to_json(&ci.name));
660 data.insert("message".to_string(), to_json(&contest.message));
661 fill_oauth_data(login_info, &mut data);
662
663 if secret.is_some() && secret != contest.secret {
664 return Err(MedalError::AccessDenied);
665 }
666
667 let has_secret = contest.secret.is_some();
668 let mut require_secret = false;
669 if has_secret {
670 data.insert("secret_field".to_string(), to_json(&true));
671
672 if secret.is_some() {
673 data.insert("secret_field_prefill".to_string(), to_json(&secret));
674 } else {
675 require_secret = true;
676 }
677 }
678
679 let constraints = check_contest_constraints(&session, &contest);
680 let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
681
682 let has_tasks = contest.taskgroups.len() > 0;
683 let can_start = constraints.contest_running
684 && constraints.grade_matching
685 && is_qualified
686 && (has_tasks || has_secret)
687 && (session.is_logged_in() || contest.secret.is_some() && !contest.requires_login.unwrap_or(false));
688
689 let has_duration = contest.duration > 0;
690
691 data.insert("constraints".to_string(), to_json(&constraints));
692 data.insert("is_qualified".to_string(), to_json(&is_qualified));
693 data.insert("has_duration".to_string(), to_json(&has_duration));
694 data.insert("can_start".to_string(), to_json(&can_start));
695 data.insert("has_tasks".to_string(), to_json(&has_tasks));
696 data.insert("no_tasks".to_string(), to_json(&!has_tasks));
697
698 if opt_part.is_none()
703 && contest.duration == 0
704 && constraints.contest_running
705 && constraints.grade_matching
706 && !require_secret
707 && contest.requires_login != Some(true)
708 {
709 conn.new_participation(session.id, contest_id, None).map_err(|_| MedalError::AccessDenied)?;
710 opt_part = Some(Participation { contest: contest_id,
711 user: session.id,
712 start: time::get_time(),
713 team: None,
714 annotation: None });
715 }
716
717 let now = time::get_time();
718 if let Some(start) = contest.start {
719 if now < start {
720 let until = start - now;
721 data.insert("time_until_start".to_string(),
722 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
723 }
724 }
725
726 if let Some(end) = contest.end {
727 if now < end {
728 let until = end - now;
729 data.insert("time_until_end".to_string(),
730 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
731 }
732 }
733
734 if session.is_logged_in() {
735 data.insert("logged_in".to_string(), to_json(&true));
736 data.insert("username".to_string(), to_json(&session.username));
737 data.insert("firstname".to_string(), to_json(&session.firstname));
738 data.insert("lastname".to_string(), to_json(&session.lastname));
739 data.insert("teacher".to_string(), to_json(&session.is_teacher));
740 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
741 }
742
743 if let Some(participation) = opt_part {
744 let time_info = check_contest_time_left(&session, &contest, &participation);
745 data.insert("time_info".to_string(), to_json(&time_info));
746
747 let time_left_formatted =
748 format!("{}:{:02}:{:02}", time_info.left_hour, time_info.left_min, time_info.left_sec);
749 data.insert("time_left_formatted".to_string(), to_json(&time_left_formatted));
750
751 if let Some(team) = participation.team {
753 if team != session.id {
755 if time_info.is_time_left {
756 data.insert("block_team_participation".to_string(), to_json(&true));
758 } else {
759 grades = conn.get_contest_user_grades(team, contest_id);
761 }
762 }
763
764 let names =
765 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
766 .iter()
767 .filter(|user| user.id != session.id)
768 .map(|user| {
769 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
770 })
771 .collect::<Vec<String>>()
772 .join(", ");
773 data.insert("team_partners".to_string(), to_json(&names));
774 }
775
776 let mut totalgrade = 0;
777 let mut max_totalgrade = 0;
778
779 let mut tasks = Vec::new();
780 for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
781 let subtaskstars = generate_subtaskstars(&taskgroup, &grade, None);
782 let ti = TaskInfo { name: taskgroup.name, subtasks: subtaskstars };
783 tasks.push(ti);
784
785 totalgrade += grade.grade.unwrap_or(0);
786 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
787 }
788 let relative_points = if max_totalgrade > 0 { (totalgrade * 100) / max_totalgrade } else { 0 };
789
790 data.insert("tasks".to_string(), to_json(&tasks));
791
792 data.insert("is_started".to_string(), to_json(&true));
793 data.insert("total_points".to_string(), to_json(&totalgrade));
794 data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
795 data.insert("relative_points".to_string(), to_json(&relative_points));
796 data.insert("lean_page".to_string(), to_json(&true));
797
798 if has_tasks && contest.standalone_task.unwrap_or(false) {
799 return Ok(Err(tasks[0].subtasks[0].id));
800 }
801 }
802
803 if let Some(query_string) = query_string {
804 if !query_string.starts_with("bare") {
805 data.insert("not_bare".to_string(), to_json(&true));
806 }
807
808 if query_string.contains("team_participation=no_logincode") {
809 data.insert("team_error".to_string(), to_json(&"Kein Logincode angegeben"));
810 }
811 if query_string.contains("team_participation=invalid_logincode") {
812 data.insert("team_error".to_string(), to_json(&"Ungültiger Logincode angegeben"));
813 }
814 if query_string.contains("team_participation=own_logincode") {
815 data.insert("team_error".to_string(), to_json(&"Eigener Logincode angegeben"));
816 }
817 if query_string.contains("team_participation=logincode_has_participation") {
818 data.insert("team_error".to_string(), to_json(&"Logincode hat diesen Wettbewerb bereits gestartet"));
819 }
820 } else {
821 data.insert("not_bare".to_string(), to_json(&true));
822 }
823
824 Ok(Ok(("contest".to_owned(), data)))
825}
826
827pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
828 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
829 let mut data = json_val::Map::new();
830 fill_user_data(&session, &mut data);
831
832 let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);
833
834 #[derive(Serialize, Deserialize)]
835 struct UserResults {
836 firstname: String,
837 lastname: String,
838 user_id: i32,
839 grade: String,
840 logincode: String,
841 annotation: String,
842 annotation_result: String,
843 team_participants: String,
844 results: Vec<String>,
845 }
846
847 #[derive(Serialize, Deserialize)]
848 struct GroupResults {
849 groupname: String,
850 group_id: i32,
851 groupcode: String,
852 user_results: Vec<UserResults>,
853 }
854
855 let mut results: Vec<GroupResults> = Vec::new();
856 let mut has_annotations = false;
857 let mut has_annotations_result = false;
858 let mut has_teams = false;
859
860 for (group, groupdata) in resultdata {
861 let mut groupresults: Vec<UserResults> = Vec::new();
862
863 for (user, userdata) in groupdata {
864 let mut userresults: Vec<String> = Vec::new();
865
866 userresults.push(String::new());
867 let mut summe = 0;
868
869 for grade in userdata {
870 if let Some(g) = grade.grade {
871 userresults.push(format!("{}", g));
872 summe += g;
873 } else {
874 userresults.push("–".to_string());
875 }
876 }
877
878 userresults[0] = format!("{}", summe);
879
880 let (annotation, annotation_result) = if let Some(annotation) = user.annotation {
881 let mut split = annotation.split('\x1f');
882
883 (split.next()
884 .filter(|s| s.len() > 0)
885 .map(|s| {
886 has_annotations = true;
887 s.to_string()
888 })
889 .unwrap_or_default(),
890 split.next()
891 .filter(|s| s.len() > 0)
892 .map(|s| {
893 has_annotations_result = true;
894 s.to_string()
895 })
896 .unwrap_or_default())
897 } else {
898 (Default::default(), Default::default())
899 };
900
901 let team_participants = if let Some(team) = user.team {
902 has_teams = true;
903 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
904 .iter()
905 .filter(|user| user.id != session.id)
906 .map(|user| {
907 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
908 })
909 .collect::<Vec<String>>()
910 .join(", ")
911 } else {
912 Default::default()
913 };
914
915 groupresults.push(UserResults { firstname: user.firstname.unwrap_or_else(|| "–".to_string()),
916 lastname: user.lastname.unwrap_or_else(|| "–".to_string()),
917 user_id: user.id,
918 grade: grade_to_string(user.grade),
919 logincode: user.logincode.unwrap_or_else(|| "".to_string()),
920 annotation,
921 annotation_result,
922 team_participants,
923 results: userresults });
924 }
925
926 results.push(GroupResults { groupname: group.name.to_string(),
927 group_id: group.id.unwrap_or(0),
928 groupcode: group.groupcode,
929 user_results: groupresults });
930 }
931
932 data.insert("taskname".to_string(), to_json(&tasknames));
933 data.insert("result".to_string(), to_json(&results));
934 data.insert("has_annotations".to_string(), to_json(&has_annotations));
935 data.insert("has_annotations_result".to_string(), to_json(&has_annotations_result));
936 data.insert("has_teams".to_string(), to_json(&has_teams));
937
938 let c = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
939 let ci = ContestInfo { id: c.id.unwrap(),
940 name: c.name.clone(),
941 duration: c.duration,
942 public: c.public,
943 requires_login: c.requires_login.unwrap_or(false),
944 image: None,
945 language: None,
946 category: c.category.clone(),
947 team_participation: false,
948 colour: c.colour.clone(),
949 tags: Vec::new() };
950
951 data.insert("contest".to_string(), to_json(&ci));
952 data.insert("contestname".to_string(), to_json(&c.name));
953
954 Ok(("contestresults".to_owned(), data))
955}
956
957pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str,
958 secret: Option<String>, logincode_team: Option<String>)
959 -> MedalResult<Result<(), String>> {
960 let session = conn.get_session_or_new(&session_token).unwrap();
962 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
964
965 if contest.duration != 0
967 && !session.is_logged_in()
968 && (contest.requires_login.unwrap_or(false) || contest.secret.is_none())
969 {
970 return Err(MedalError::AccessDenied);
971 }
972
973 if session.is_logged_in() && session.csrf_token != csrf_token {
975 return Err(MedalError::CsrfCheckFailed);
976 }
977
978 let constraints = check_contest_constraints(&session, &contest);
980
981 if !(constraints.contest_running && constraints.grade_matching) {
982 return Err(MedalError::AccessDenied);
983 }
984
985 let is_qualified = check_contest_qualification(conn, &session, &contest);
986
987 if is_qualified == Some(false) {
988 return Err(MedalError::AccessDenied);
989 }
990
991 if contest.secret != secret {
992 return Err(MedalError::AccessDenied);
993 }
994
995 if let Some(logincode_team) = logincode_team {
996 match contest.max_teamsize {
997 None => return Err(MedalError::AccessDenied), Some(max_teamsize) => {
999 if max_teamsize < 2 {
1000 return Err(MedalError::AccessDenied); }
1002 }
1003 };
1004
1005 if logincode_team == "" {
1006 return Ok(Err("no_logincode".to_string()));
1007 }
1008
1009 let teampartner = conn.get_user_and_group_by_logincode(&logincode_team);
1010 if let Some((teampartner_user, _)) = teampartner {
1011 if teampartner_user.id == session.id {
1012 return Ok(Err("own_logincode".to_string()));
1013 }
1014
1015 if conn.get_participation(teampartner_user.id, contest_id).is_some() {
1016 return Ok(Err("logincode_has_participation".to_string()));
1017 }
1018
1019 if conn.get_participation(session.id, contest_id).is_some() {
1020 return Err(MedalError::AccessDenied); }
1022
1023 let res = conn.new_participation(session.id, contest_id, Some(session.id));
1027 let _ = conn.new_participation(teampartner_user.id, contest_id, Some(session.id));
1028 return match res {
1029 Ok(_) => Ok(Ok(())),
1030 _ => Err(MedalError::AccessDenied), };
1032 } else {
1033 return Ok(Err("invalid_logincode".to_string()));
1034 }
1035 }
1036
1037 match conn.new_participation(session.id, contest_id, None) {
1039 Ok(_) => Ok(Ok(())),
1040 _ => Err(MedalError::AccessDenied), }
1042}
1043
1044pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String), login_info: LoginInfo)
1045 -> Result<String, MedalValue> {
1046 let (username, password) = login_data;
1047
1048 match conn.login(None, &username, &password) {
1049 Ok(session_token) => Ok(session_token),
1050 Err(()) => {
1051 let mut data = json_val::Map::new();
1052 data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
1053 data.insert("username".to_string(), to_json(&username));
1054 data.insert("parent".to_string(), to_json(&"base"));
1055
1056 fill_oauth_data(login_info, &mut data);
1057
1058 Err(("login".to_owned(), data))
1059 }
1060 }
1061}
1062
1063pub fn login_with_code<T: MedalConnection>(
1064 conn: &T, code: &str, login_info: LoginInfo)
1065 -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
1066 match conn.login_with_code(None, &code.trim()) {
1067 Ok(session_token) => Ok(Ok(session_token)),
1068 Err(()) => match conn.create_user_with_groupcode(None, &code.trim()) {
1069 Ok(session_token) => Ok(Err(session_token)),
1070 Err(()) => {
1071 let mut data = json_val::Map::new();
1072 data.insert("reason".to_string(), to_json(&"Kein gültiger Code. Bitte erneut versuchen.".to_string()));
1073 data.insert("code".to_string(), to_json(&code));
1074 data.insert("parent".to_string(), to_json(&"base"));
1075
1076 fill_oauth_data(login_info, &mut data);
1077
1078 Err(("login".to_owned(), data))
1079 }
1080 },
1081 }
1082}
1083
1084fn webauthn(self_url: &str) -> webauthn::Webauthn {
1085 use webauthn_rs::prelude::*;
1086
1087 let rp_id = self_url.split("://").nth(1).unwrap().split('/').next().unwrap().split(':').next().unwrap();
1089 let rp_origin = Url::parse(self_url).expect("Invalid URL");
1090 WebauthnBuilder::new(rp_id, &rp_origin).expect("Invalid configuration").build().expect("Invalid configuration")
1091}
1092
1093pub fn register_key_challenge<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str) -> JsonValueResult {
1094 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1095
1096 let webauthn = webauthn(self_url);
1097
1098 let cred_ids = conn.get_all_webauthn_credentials();
1099
1100 let (ccr, skr) =
1102 webauthn.start_passkey_registration(webauthn::Uuid::from_u64_pair(0, session.id as u64),
1103 session.firstname.as_ref().unwrap_or(&("".to_string())),
1104 session.firstname.as_ref().unwrap_or(&("".to_string())),
1105 Some(cred_ids.into_iter()
1106 .map(|x| serde_json::from_str(&format!("\"{}\"", x)).unwrap())
1107 .collect()))
1108 .expect("Failed to start webauthn registration.");
1109
1110 conn.set_webauthn_passkey_registration(session.id, &serde_json::json!(skr).to_string());
1111
1112 let mut data = json_val::Map::new();
1113 data.insert("challenge".to_string(), to_json(&ccr));
1114 Ok(data)
1115}
1116
1117pub fn register_key<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str, csrf_token: &str,
1118 credential: String, name: String)
1119 -> JsonValueResult {
1120 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1121
1122 if session.csrf_token != csrf_token {
1123 return Err(MedalError::CsrfCheckFailed);
1124 }
1125
1126 let registration: String = conn.get_webauthn_passkey_registration(session.id).ok_or(MedalError::WebauthnError)?;
1127
1128 let webauthn = webauthn(self_url);
1129
1130 let registration: webauthn::PasskeyRegistration = serde_json::from_str(®istration).unwrap();
1131 let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1132
1133 let passkey = webauthn.finish_passkey_registration(&credential, ®istration);
1134
1135 match passkey {
1136 Ok(passkey) => {
1137 if let serde_json::Value::String(cred_id) = serde_json::json!(passkey.cred_id()) {
1138 conn.add_webauthn_passkey(session.id, &cred_id, &serde_json::json!(passkey).to_string(), &name);
1139 } else {
1140 println!("Webauthn: Could not unwrap cred_id from webauthn Passkey");
1141 return Err(MedalError::WebauthnError);
1142 }
1143 }
1144 Err(webauthn::WebauthnError::UserNotVerified) => {
1145 println!("Webauthn: UserNotVerified");
1147 return Err(MedalError::WebauthnError);
1148 }
1149 Err(err) => {
1150 println!("Webauthn: Other error: {:#?}", err);
1151 return Err(MedalError::WebauthnError);
1152 }
1153 }
1154
1155 let data = json_val::Map::new();
1156 Ok(data)
1157}
1158
1159pub fn delete_key<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, token_id: i32)
1160 -> JsonValueResult {
1161 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1162
1163 if session.csrf_token != csrf_token {
1164 return Err(MedalError::CsrfCheckFailed);
1165 }
1166
1167 if !conn.get_webauthn_passkey_names_for_user(session.id).into_iter().any(|(id, _name)| id == token_id) {
1168 return Err(MedalError::NotFound);
1169 }
1170
1171 conn.delete_webauthn_passkey(session.id, token_id);
1172
1173 let data = json_val::Map::new();
1174 Ok(data)
1175}
1176
1177pub fn login_with_key_challenge<T: MedalConnection>(conn: &T, self_url: &str) -> JsonValue {
1178 let passkeys = conn.get_all_webauthn_passkeys()
1179 .into_iter()
1180 .map(|passkey| {
1181 let passkey: webauthn::Passkey = serde_json::from_str(&passkey).unwrap();
1182 passkey
1183 })
1184 .collect::<Vec<webauthn::Passkey>>();
1185
1186 let webauthn = webauthn(self_url);
1187
1188 let (challenge, authentication) = webauthn.start_passkey_authentication(&passkeys).unwrap();
1189
1190 let auth_id = conn.store_webauthn_auth_challenge(&serde_json::json!(challenge).to_string(),
1191 &serde_json::json!(authentication).to_string());
1192
1193 let mut data = json_val::Map::new();
1194 data.insert("id".to_string(), to_json(&auth_id));
1195 data.insert("challenge".to_string(), to_json(&challenge));
1196 data
1197}
1198
1199pub fn login_with_key<T: MedalConnection>(conn: &T, self_url: &str, auth_id: i32, credential: &str)
1200 -> Result<String, String> {
1201 let authentication = conn.get_webauthn_auth_challenge_by_id(auth_id).unwrap();
1202
1203 let authentication: webauthn::PasskeyAuthentication = serde_json::from_str(&authentication).unwrap();
1204 let credential: webauthn::PublicKeyCredential = serde_json::from_str(&credential).unwrap();
1205
1206 let webauthn = webauthn(self_url);
1207
1208 match webauthn.finish_passkey_authentication(&credential, &authentication) {
1209 Err(err) => {
1210 println!("Webauthn: Other error: {:#?}", err);
1211 let mut data = json_val::Map::new();
1212 data.insert("reason".to_string(),
1213 to_json(&"Unbekannter Key. Bitte Key zuerst im Profil registrieren.".to_string()));
1214 Err(serde_json::json!(data).to_string())
1215 }
1216 Ok(authresult) => {
1217 if let serde_json::Value::String(cred_id) = serde_json::json!(authresult.cred_id()) {
1218 match conn.login_with_key(None, &cred_id) {
1219 Ok(session_token) => Ok(session_token),
1220 Err(()) => {
1221 let mut data = json_val::Map::new();
1222 println!("Webauthn: Auth fail");
1223 data.insert("reason".to_string(), to_json(&"Key konnte nicht authentifiziert werden. Bitte über anderen Weg einloggen.".to_string()));
1224 Err(serde_json::json!(data).to_string())
1225 }
1226 }
1227 } else {
1228 panic!("Webauthn: Could not unwrap cred_id from webauthn AuthenticationResult")
1229 }
1230 }
1231 }
1232}
1233
1234pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
1235 session_token.map(|token| conn.logout(&token));
1236}
1237
1238#[cfg(feature = "signup")]
1239pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
1240 -> MedalResult<SignupResult> {
1241 let (username, email, password) = signup_data;
1242
1243 if username == "" || email == "" || password == "" {
1244 return Ok(SignupResult::EmptyFields);
1245 }
1246
1247 let salt = helpers::make_salt();
1248 let hash = helpers::hash_password(&password, &salt)?;
1249
1250 let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
1251 Ok(result)
1252}
1253
1254#[cfg(feature = "signup")]
1255pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
1256 let mut data = json_val::Map::new();
1257 if let Some(query) = query_string {
1258 if let Some(status) = query.strip_prefix("status=") {
1259 if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
1260 data.insert((status).to_string(), to_json(&true));
1261 }
1262 }
1263 }
1264 data
1265}
1266
1267pub fn load_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, subtask: Option<String>,
1268 submission_id: Option<i32>)
1269 -> MedalResult<String> {
1270 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1271
1272 match submission_id {
1273 None => match conn.load_submission(&session, task_id, subtask.as_deref()) {
1274 Some(submission) => Ok(submission.value),
1275 None => Ok("{}".to_string()),
1276 },
1277 Some(submission_id) => {
1278 let (submission, _, _, _) =
1279 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1280
1281 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1283 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1284 if !group.admins.contains(&session.id) {
1285 return Err(MedalError::AccessDenied);
1287 }
1288 } else {
1289 return Err(MedalError::AccessDenied);
1291 }
1292 }
1293 Ok(submission.value)
1294 }
1295 }
1296}
1297
1298#[allow(clippy::too_many_arguments)]
1299pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
1300 data: String, grade_percentage: i32, autosave: bool,
1301 subtask: Option<String>)
1302 -> MedalResult<String> {
1303 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1304
1305 if session.csrf_token != csrf_token {
1306 return Err(MedalError::CsrfCheckFailed);
1307 }
1308
1309 let (t, _, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1310
1311 match conn.get_participation(session.id, contest.id.expect("Value from database")) {
1312 None => return Err(MedalError::AccessDenied),
1313 Some(participation) => {
1314 let time_info = check_contest_time_left(&session, &contest, &participation);
1315 if !time_info.can_still_compete && time_info.left_secs_total < -10 {
1316 return Err(MedalError::AccessDenied);
1317 }
1320 if participation.team.is_some() && participation.team != Some(session.id) {
1321 return Err(MedalError::AccessDenied);
1322 }
1323 }
1324 }
1325
1326 let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1341
1342 let submission = Submission { id: None,
1361 user: session.id,
1362 task: task_id,
1363 grade: grade_rounded,
1364 validated: false,
1365 nonvalidated_grade: grade_rounded,
1366 needs_validation: true,
1367 autosave,
1368 latest: Default::default(), highest_grade_latest: Default::default(), subtask_identifier: subtask,
1371 value: data,
1372 date: time::get_time() };
1373
1374 conn.submit_submission(submission);
1375
1376 Ok("{}".to_string())
1377}
1378
1379pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, autosaveinterval: u64)
1380 -> MedalResult<Result<MedalValue, (i32, Option<String>)>> {
1381 let session = conn.get_session_or_new(&session_token).unwrap();
1382
1383 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1384 let grade = conn.get_taskgroup_user_grade(session.id, tg.id.unwrap()); let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; let mut prevtaskgroup: Option<Taskgroup> = None;
1388 let mut nexttaskgroup: Option<Taskgroup> = None;
1389 let mut current_found = false;
1390
1391 let mut subtaskstars = Vec::new();
1392
1393 for taskgroup in tasklist.taskgroups {
1394 if current_found {
1395 nexttaskgroup = Some(taskgroup);
1396 break;
1397 }
1398
1399 if taskgroup.id == tg.id {
1400 current_found = true;
1401 subtaskstars = generate_subtaskstars(&taskgroup, &grade, Some(task_id));
1402 } else {
1403 prevtaskgroup = Some(taskgroup);
1404 }
1405 }
1406
1407 match conn.get_own_participation(session.id, contest.id.expect("Value from database")) {
1408 None => Ok(Err((contest.id.unwrap(), contest.category))),
1409 Some(participation) => {
1410 let mut data = json_val::Map::new();
1411 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1412 data.insert("prevtask".to_string(), to_json(&prevtaskgroup.map(|tg| tg.tasks[0].id)));
1413 data.insert("nexttask".to_string(), to_json(&nexttaskgroup.map(|tg| tg.tasks[0].id))); let time_info = check_contest_time_left(&session, &contest, &participation);
1416 data.insert("time_info".to_string(), to_json(&time_info));
1417
1418 data.insert("time_left_mh_formatted".to_string(),
1419 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1420 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1421
1422 let auto_save_interval_ms = if autosaveinterval > 0 && autosaveinterval < 31536000000 {
1423 autosaveinterval * 1000
1424 } else {
1425 31536000000
1426 };
1427 data.insert("auto_save_interval_ms".to_string(), to_json(&auto_save_interval_ms));
1428
1429 if time_info.can_still_compete && participation.team.is_some() && participation.team != Some(session.id) {
1431 return Ok(Err((contest.id.unwrap(), contest.category)));
1432 }
1433
1434 if time_info.can_still_compete || time_info.is_review {
1435 data.insert("contestname".to_string(), to_json(&contest.name));
1436 data.insert("name".to_string(), to_json(&tg.name));
1437 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1438 data.insert("taskid".to_string(), to_json(&task_id));
1439 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1440 data.insert("contestid".to_string(), to_json(&contest.id));
1441 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1442 data.insert("standalone_task".to_string(), to_json(&contest.standalone_task));
1443 data.insert("category".to_string(), to_json(&contest.category));
1444
1445 let (template, tasklocation) = if let Some(language) = t.language {
1446 match language.as_str() {
1447 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1448 "python" => {
1449 data.insert("tasklang".to_string(), to_json(&"python"));
1450 ("wtask".to_owned(), t.location.as_str())
1451 }
1452 _ => ("task".to_owned(), t.location.as_str()),
1453 }
1454 } else {
1455 match t.location.chars().next() {
1456 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1457 Some('P') => {
1458 data.insert("tasklang".to_string(), to_json(&"python"));
1459 ("wtask".to_owned(), &t.location[1..])
1460 }
1461 _ => ("task".to_owned(), t.location.as_str()),
1462 }
1463 };
1464
1465 let taskpath = format!("{}{}", contest.location, &tasklocation);
1466 data.insert("taskpath".to_string(), to_json(&taskpath));
1467
1468 Ok(Ok((template, data)))
1469 } else {
1470 Ok(Err((contest.id.unwrap(), contest.category)))
1472 }
1473 }
1474 }
1475}
1476
1477pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, submission_id: i32)
1478 -> MedalResult<Result<MedalValue, i32>> {
1479 let session = conn.get_session_or_new(&session_token).unwrap();
1480
1481 let (submission, t, tg, contest) =
1482 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1483
1484 let grade = Grade { taskgroup: tg.id.unwrap(),
1487 user: session.id,
1488 grade: Some(submission.grade),
1489 validated: submission.validated };
1490
1491 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1493 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1494 if !group.admins.contains(&session.id) {
1495 return Err(MedalError::AccessDenied);
1497 }
1498 } else {
1499 return Err(MedalError::AccessDenied);
1501 }
1502 }
1503
1504 let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); let mut data = json_val::Map::new();
1507 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1508
1509 let time_info = ContestTimeInfo { passed_secs_total: 0,
1510 left_secs_total: 0,
1511 left_mins_total: 0,
1512 left_hour: 0,
1513 left_min: 0,
1514 left_sec: 0,
1515 has_timelimit: contest.duration != 0,
1516 is_time_left: false,
1517 exempt_from_timelimit: true,
1518 can_still_compete: false,
1519 review_has_timelimit: false,
1520 has_future_review: false,
1521 has_review_end: false,
1522 is_review: true,
1523 can_still_compete_or_review: true,
1524
1525 until_review_start_day: 0,
1526 until_review_start_hour: 0,
1527 until_review_start_min: 0,
1528
1529 until_review_end_day: 0,
1530 until_review_end_hour: 0,
1531 until_review_end_min: 0 };
1532
1533 data.insert("time_info".to_string(), to_json(&time_info));
1534
1535 data.insert("time_left_mh_formatted".to_string(),
1536 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1537 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1538
1539 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1540
1541 data.insert("name".to_string(), to_json(&tg.name));
1543 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1544 data.insert("taskid".to_string(), to_json(&task_id));
1545 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1546 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1548
1549 data.insert("submission".to_string(), to_json(&submission_id));
1550
1551 let (template, tasklocation) = if let Some(language) = t.language {
1552 match language.as_str() {
1553 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1554 "python" => {
1555 data.insert("tasklang".to_string(), to_json(&"python"));
1556 ("wtask".to_owned(), t.location.as_str())
1557 }
1558 _ => ("task".to_owned(), t.location.as_str()),
1559 }
1560 } else {
1561 match t.location.chars().next() {
1562 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1563 Some('P') => {
1564 data.insert("tasklang".to_string(), to_json(&"python"));
1565 ("wtask".to_owned(), &t.location[1..])
1566 }
1567 _ => ("task".to_owned(), t.location.as_str()),
1568 }
1569 };
1570
1571 let taskpath = format!("{}{}", contest.location, &tasklocation);
1572 data.insert("taskpath".to_string(), to_json(&taskpath));
1573
1574 Ok(Ok((template, data)))
1575}
1576
1577pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> {
1578 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1579
1580 if !contest.public
1582 || contest.duration != 0
1583 || !contest.requires_contests.is_empty()
1584 || contest.requires_login == Some(true)
1585 || contest.standalone_task != Some(true)
1586 {
1587 return Err(MedalError::UnknownId);
1588 }
1589
1590 let time_info = ContestTimeInfo { passed_secs_total: 0,
1591 left_secs_total: 0,
1592 left_mins_total: 0,
1593 left_hour: 0,
1594 left_min: 0,
1595 left_sec: 0,
1596 has_timelimit: contest.duration != 0,
1597 is_time_left: false,
1598 exempt_from_timelimit: true,
1599 can_still_compete: false,
1600 review_has_timelimit: false,
1601 has_future_review: false,
1602 has_review_end: false,
1603 is_review: true,
1604 can_still_compete_or_review: true,
1605
1606 until_review_start_day: 0,
1607 until_review_start_hour: 0,
1608 until_review_start_min: 0,
1609
1610 until_review_end_day: 0,
1611 until_review_end_hour: 0,
1612 until_review_end_min: 0 };
1613
1614 let mut data = json_val::Map::new();
1615
1616 data.insert("time_info".to_string(), to_json(&time_info));
1617
1618 data.insert("time_left_mh_formatted".to_string(),
1619 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1620 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1621
1622 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1623
1624 data.insert("contestname".to_string(), to_json(&contest.name));
1625 data.insert("name".to_string(), to_json(&tg.name));
1626 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1627 data.insert("taskid".to_string(), to_json(&task_id));
1628 data.insert("contestid".to_string(), to_json(&contest.id));
1629 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1630 data.insert("preview".to_string(), to_json(&true));
1631
1632 let (template, tasklocation) = if let Some(language) = t.language {
1633 match language.as_str() {
1634 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1635 "python" => {
1636 data.insert("tasklang".to_string(), to_json(&"python"));
1637 ("wtask".to_owned(), t.location.as_str())
1638 }
1639 _ => ("task".to_owned(), t.location.as_str()),
1640 }
1641 } else {
1642 match t.location.chars().next() {
1643 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1644 Some('P') => {
1645 data.insert("tasklang".to_string(), to_json(&"python"));
1646 ("wtask".to_owned(), &t.location[1..])
1647 }
1648 _ => ("task".to_owned(), t.location.as_str()),
1649 }
1650 };
1651
1652 let taskpath = format!("{}{}", contest.location, &tasklocation);
1653 data.insert("taskpath".to_string(), to_json(&taskpath));
1654
1655 Ok(Ok((template, data)))
1656}
1657
1658#[derive(Serialize, Deserialize)]
1659pub struct GroupInfo {
1660 pub id: i32,
1661 pub name: String,
1662 pub tag: String,
1663 pub code: String,
1664}
1665
1666pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1667 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1668
1669 let mut data = json_val::Map::new();
1670 fill_user_data(&session, &mut data);
1671
1672 let v: Vec<GroupInfo> =
1673 conn.get_groups(session.id)
1674 .iter()
1675 .map(|g| GroupInfo { id: g.id.unwrap(),
1676 name: g.name.clone(),
1677 tag: g.tag.clone(),
1678 code: g.groupcode.clone() })
1679 .collect();
1680 data.insert("group".to_string(), to_json(&v));
1681 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1682
1683 Ok(("groups".to_string(), data))
1684}
1685
1686#[derive(Serialize, Deserialize)]
1687pub struct MemberInfo {
1688 pub id: i32,
1689 pub firstname: String,
1690 pub lastname: String,
1691 pub sex: String,
1692 pub grade: String,
1693 pub logincode: String,
1694 pub anonymous: bool,
1695}
1696
1697pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
1698 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1699 let group = conn.get_group_complete(group_id).unwrap(); let mut data = json_val::Map::new();
1702 fill_user_data(&session, &mut data);
1703
1704 if !group.admins.contains(&session.id) {
1705 return Err(MedalError::AccessDenied);
1706 }
1707
1708 let gi = GroupInfo { id: group.id.unwrap(),
1709 name: group.name.clone(),
1710 tag: group.tag.clone(),
1711 code: group.groupcode.clone() };
1712
1713 let v: Vec<MemberInfo> = group.members
1714 .iter()
1715 .filter_map(|m| {
1716 Some(MemberInfo { id: m.id,
1717 firstname: m.firstname.clone()?,
1718 lastname: m.lastname.clone()?,
1719 sex: (match m.sex {
1720 Some(0) | None => "/",
1721 Some(1) => "m",
1722 Some(2) => "w",
1723 Some(3) => "d",
1724 Some(4) => "…",
1725 _ => "?",
1726 }).to_string(),
1727 grade: grade_to_string(m.grade),
1728 logincode: m.logincode.clone()?,
1729 anonymous: m.anonymous })
1730 })
1731 .collect();
1732
1733 data.insert("group".to_string(), to_json(&gi));
1734 data.insert("member".to_string(), to_json(&v));
1735 data.insert("groupname".to_string(), to_json(&gi.name));
1736
1737 Ok(("group".to_string(), data))
1738}
1739
1740pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
1741 -> MedalResult<i32> {
1742 let session = conn.get_session(&session_token)
1743 .ensure_logged_in()
1744 .ok_or(MedalError::AccessDenied)?
1745 .ensure_teacher_or_admin()
1746 .ok_or(MedalError::AccessDenied)?;
1747
1748 if session.csrf_token != csrf_token {
1749 return Err(MedalError::CsrfCheckFailed);
1750 }
1751
1752 let mut groupcode = String::new();
1753 for i in 0..10 {
1754 if i == 9 {
1755 panic!("ERROR: Too many groupcode collisions! Give up ...");
1756 }
1757 groupcode = helpers::make_groupcode();
1758 if !conn.code_exists(&groupcode) {
1759 break;
1760 }
1761 println!("WARNING: Groupcode collision! Retrying ...");
1762 }
1763
1764 let mut group = Group { id: None, name, groupcode, tag, admins: vec![session.id], members: Vec::new() };
1765
1766 conn.add_group(&mut group);
1767
1768 Ok(group.id.unwrap())
1769}
1770
1771pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
1772 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1773
1774 let mut data = json_val::Map::new();
1775 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1776
1777 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1778 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1779 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1780 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1781
1782 Ok(("groupcsv".to_string(), data))
1783}
1784
1785pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
1787 -> MedalResult<()> {
1788 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1789
1790 if session.csrf_token != csrf_token {
1791 return Err(MedalError::CsrfCheckFailed);
1792 }
1793
1794 let mut v: Vec<Vec<String>> = serde_json::from_str(group_data).or(Err(MedalError::AccessDenied))?; v.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1796
1797 let mut groupcode = String::new();
1798 let mut name = String::new();
1799 let mut group = Group { id: None,
1800 name: name.clone(),
1801 groupcode,
1802 tag: String::new(),
1803 admins: vec![session.id],
1804 members: Vec::new() };
1805
1806 for line in v {
1807 if name != line[0] {
1808 if name != "" {
1809 conn.update_or_create_group_with_users(group, session.id);
1810 }
1811 name = line[0].clone();
1812
1813 groupcode = String::new();
1814 for i in 0..10 {
1815 if i == 9 {
1816 panic!("ERROR: Too many groupcode collisions! Give up ...");
1817 }
1818 groupcode = helpers::make_groupcode();
1819 if !conn.code_exists(&groupcode) {
1820 break;
1821 }
1822 println!("WARNING: Groupcode collision! Retrying ...");
1823 }
1824
1825 group = Group { id: None,
1826 name: name.clone(),
1827 groupcode,
1828 tag: name.clone(),
1829 admins: vec![session.id],
1830 members: Vec::new() };
1831 }
1832
1833 let mut user = SessionUser::group_user_stub();
1834 user.grade = line[1].parse::<i32>().unwrap_or(0);
1835 user.firstname = Some(line[2].clone());
1836 user.lastname = Some(line[3].clone());
1837
1838 use db_objects::Sex;
1839 match line[4].as_str() {
1840 "m" => user.sex = Some(Sex::Male as i32),
1841 "f" => user.sex = Some(Sex::Female as i32),
1842 "d" => user.sex = Some(Sex::Diverse as i32),
1843 _ => user.sex = None,
1844 }
1845
1846 user.anonymous = line[5] == "y";
1847
1848 group.members.push(user);
1849 }
1850 conn.update_or_create_group_with_users(group, session.id);
1851
1852 Ok(())
1853}
1854
1855pub fn contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1856 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1857
1858 let mut data = json_val::Map::new();
1859 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1860
1861 Ok(("admin_admissioncsv".to_string(), data))
1862}
1863
1864pub fn upload_contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
1865 contest_id: i32, admission_data: &str)
1866 -> MedalResult<()> {
1867 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1868
1869 if session.csrf_token != csrf_token {
1870 return Err(MedalError::CsrfCheckFailed);
1871 }
1872
1873 let v: Vec<Vec<String>> = serde_json::from_str(admission_data).or(Err(MedalError::AccessDenied))?; let w: Vec<(i32, Option<String>)> = v.into_iter()
1876 .map(|vv| {
1877 (vv[0].parse().unwrap_or(-1),
1878 if vv[1].len() == 0 && vv[2].len() == 0 && vv[3].len() == 0 {
1879 None
1880 } else {
1881 Some(format!("{}\x1f{}\x1f{}", vv[1], vv[2], vv[3]))
1883 })
1884 })
1885 .collect();
1886
1887 let _annotations_inserted = conn.insert_contest_annotations(contest_id, w);
1888
1889 Ok(())
1890}
1891
1892#[allow(dead_code)]
1893pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
1894 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1895 let _g = conn.get_contest_groups_grades(session.id, contest_id);
1897
1898 let data = json_val::Map::new();
1899
1900 Ok(("groupresults".into(), data))
1901}
1902
1903pub struct SexInformation {
1904 pub require_sex: bool,
1905 pub allow_sex_na: bool,
1906 pub allow_sex_diverse: bool,
1907 pub allow_sex_other: bool,
1908}
1909
1910pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
1911 query_string: Option<String>, sex_infos: SexInformation)
1912 -> MedalValueResult {
1913 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1914
1915 let mut data = json_val::Map::new();
1916 fill_user_data(&session, &mut data);
1917
1918 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1919 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1920 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1921 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1922
1923 match user_id {
1924 None => {
1925 data.insert("profile_firstname".to_string(), to_json(&session.firstname));
1926 data.insert("profile_lastname".to_string(), to_json(&session.lastname));
1927 data.insert("profile_street".to_string(), to_json(&session.street));
1928 data.insert("profile_zip".to_string(), to_json(&session.zip));
1929 data.insert("profile_city".to_string(), to_json(&session.city));
1930 data.insert(format!("sel{}", session.grade), to_json(&"selected"));
1931 if let Some(sex) = session.sex {
1932 data.insert(format!("sex_{}", sex), to_json(&"selected"));
1933 } else {
1934 data.insert("sex_None".to_string(), to_json(&"selected"));
1935 }
1936
1937 data.insert("profile_logincode".to_string(), to_json(&session.logincode));
1938 if session.password.is_some() {
1939 data.insert("profile_username".to_string(), to_json(&session.username));
1940 }
1941 if session.managed_by.is_none() {
1942 data.insert("profile_not_in_group".into(), to_json(&true));
1943 }
1944 if session.oauth_provider != Some("pms".to_string()) {
1945 data.insert("profile_not_pms".into(), to_json(&true));
1946 }
1949 data.insert("ownprofile".into(), to_json(&true));
1950 data.insert("userid".into(), to_json(&session.id));
1951
1952 let webauthn = conn.get_webauthn_passkey_names_for_user(session.id);
1953 data.insert("webauthn".into(), to_json(&webauthn));
1954 data.insert("has_webauthn".into(), to_json(&!webauthn.is_empty()));
1955
1956 if let Some(query) = query_string {
1957 if let Some(status) = query.strip_prefix("status=") {
1958 if ["NothingChanged",
1959 "DataChanged",
1960 "PasswordChanged",
1961 "PasswordMissmatch",
1962 "firstlogin",
1963 "SignedUp"].contains(&status)
1964 {
1965 data.insert((status).to_string(), to_json(&true));
1966 }
1967 }
1968 }
1969
1970 let now = time::get_time();
1971
1972 let all_participations = conn.get_all_participations_complete(session.id);
1973
1974 let mut contest_stars: Option<std::collections::BTreeMap<i32, i32>> = None;
1975
1976 let stickers: Vec<String> =
1977 all_participations.iter()
1978 .filter_map(|(_, contest)| {
1979 if contest.stickers.len() == 0 {
1980 return None;
1981 }
1982 if contest.stickers.len() == 1 && contest.stickers[0].1 == 0 {
1983 return Some(format!("{}/{}", contest.location, contest.stickers[0].0));
1984 }
1985
1986 if contest_stars.is_none() {
1988 contest_stars =
1989 Some(conn.count_all_stars_by_contest(session.id).into_iter().collect());
1990 }
1991 let stars: i32 =
1992 *contest_stars.as_ref().unwrap().get(&contest.id.unwrap()).unwrap_or(&0);
1993
1994 let mut value: Option<String> = None;
1996 let mut min_diff: i32 = -1;
1997
1998 for sticker in &contest.stickers {
1999 let diff = stars - sticker.1;
2000 if diff >= 0 && (min_diff < 0 || diff < min_diff) {
2001 value = Some(format!("{}{}", contest.location, sticker.0));
2002 min_diff = diff;
2003 }
2004 }
2005 value
2006 })
2007 .collect();
2008 data.insert("stickers".into(), to_json(&stickers));
2009
2010 let participations: (Vec<(i32, String, bool, bool, bool, Option<String>)>,
2012 Vec<(i32, String, bool, bool, bool, Option<String>)>) =
2013 all_participations.into_iter()
2014 .rev()
2015 .map(|(participation, contest)| {
2016 let passed_secs = now.sec - participation.start.sec;
2017 let left_secs = i64::from(contest.duration) * 60 - passed_secs;
2018 let is_time_left = contest.duration == 0 || left_secs >= 0;
2019 let has_timelimit = contest.duration != 0;
2020 let requires_login = contest.requires_login == Some(true);
2021 let annotation = participation.annotation.map(|annotation| {
2022 annotation.split('\x1f')
2023 .nth(2)
2024 .unwrap_or("")
2025 .to_string()
2026 });
2027
2028 (contest.id.unwrap(),
2029 contest.name,
2030 has_timelimit,
2031 is_time_left,
2032 requires_login,
2033 annotation)
2034 })
2035 .partition(|contest| contest.2 && !contest.4);
2036 data.insert("participations".into(), to_json(&participations));
2037
2038 let stars_count = conn.count_all_stars(session.id);
2039 data.insert("stars_count".into(), to_json(&stars_count));
2040 let stars_message = match stars_count {
2041 0 => "Auf gehts, dein erster Stern wartet auf dich!",
2042 1..=9 => "Ein hervorragender Anfang!",
2043 10..=99 => "Das ist ziemlich gut!",
2044 100..=999 => "Ein wahrer Meister!",
2045 _ => "Wow! Einfach wow!",
2046 }.to_string();
2047
2048 data.insert("stars_message".into(), to_json(&stars_message));
2049 }
2050 Some(user_id) => {
2052 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2054 let group = opt_group.ok_or(MedalError::AccessDenied)?;
2055 if !group.admins.contains(&session.id) {
2056 return Err(MedalError::AccessDenied);
2057 }
2058
2059 data.insert("profile_firstname".to_string(), to_json(&user.firstname));
2060 data.insert("profile_lastname".to_string(), to_json(&user.lastname));
2061 data.insert("profile_street".to_string(), to_json(&session.street));
2062 data.insert("profile_zip".to_string(), to_json(&session.zip));
2063 data.insert("profile_city".to_string(), to_json(&session.city));
2064 data.insert(format!("sel{}", user.grade), to_json(&"selected"));
2065 if let Some(sex) = user.sex {
2066 data.insert(format!("sex_{}", sex), to_json(&"selected"));
2067 } else {
2068 data.insert("sex_None".to_string(), to_json(&"selected"));
2069 }
2070
2071 data.insert("profile_logincode".to_string(), to_json(&user.logincode));
2072 if user.username.is_some() {
2073 data.insert("profile_username".to_string(), to_json(&user.username));
2074 }
2075 if user.managed_by.is_none() {
2076 data.insert("profile_not_in_group".into(), to_json(&true));
2077 }
2078 if session.oauth_provider != Some("pms".to_string()) {
2079 data.insert("profile_not_pms".into(), to_json(&true));
2080 }
2081 data.insert("ownprofile".into(), to_json(&false));
2082
2083 if let Some(query) = query_string {
2084 if let Some(status) = query.strip_prefix("status=") {
2085 if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
2086 data.insert((status).to_string(), to_json(&true));
2087 }
2088 }
2089 }
2090 }
2091 }
2092
2093 Ok(("profile".to_string(), data))
2094}
2095
2096#[derive(Debug, PartialEq, Eq)]
2097pub enum ProfileStatus {
2098 NothingChanged,
2099 DataChanged,
2100 PasswordChanged,
2101 PasswordMissmatch,
2102}
2103impl From<ProfileStatus> for String {
2104 fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
2105}
2106
2107pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
2108 (firstname,
2109 lastname,
2110 street,
2111 zip,
2112 city,
2113 password,
2114 password_repeat,
2115 grade,
2116 sex): (String,
2117 String,
2118 Option<String>,
2119 Option<String>,
2120 Option<String>,
2121 Option<String>,
2122 Option<String>,
2123 i32,
2124 Option<i32>))
2125 -> MedalResult<ProfileStatus> {
2126 let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2127
2128 if session.csrf_token != csrf_token {
2129 return Err(MedalError::AccessDenied); }
2131
2132 let mut result = ProfileStatus::NothingChanged;
2133
2134 let mut password_and_salt = None;
2135
2136 if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
2137 if password != "" || password_repeat != "" {
2138 if password == password_repeat {
2139 let salt = helpers::make_salt();
2140 let hash = helpers::hash_password(&password, &salt)?;
2141
2142 password_and_salt = Some((hash, salt));
2143 result = ProfileStatus::PasswordChanged;
2144 } else {
2145 result = ProfileStatus::PasswordMissmatch;
2146 }
2147 }
2148 }
2149
2150 if result == ProfileStatus::NothingChanged {
2151 if session.firstname.as_ref() == Some(&firstname)
2152 && session.lastname.as_ref() == Some(&lastname)
2153 && session.street == street
2154 && session.zip == zip
2155 && session.city == city
2156 && session.grade == grade
2157 && session.sex == sex
2158 {
2159 return Ok(ProfileStatus::NothingChanged);
2160 } else {
2161 result = ProfileStatus::DataChanged;
2162 }
2163 }
2164
2165 match user_id {
2166 None => {
2167 session.firstname = Some(firstname);
2168 session.lastname = Some(lastname);
2169 session.grade = grade;
2170 session.sex = sex;
2171
2172 if street.is_some() {
2173 session.street = street;
2174 }
2175 if zip.is_some() {
2176 session.zip = zip;
2177 }
2178 if city.is_some() {
2179 session.city = city;
2180 }
2181
2182 if let Some((password, salt)) = password_and_salt {
2183 session.password = Some(password);
2184 session.salt = Some(salt);
2185 }
2186
2187 conn.save_session(session);
2188 }
2189 Some(user_id) => {
2190 let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2192 let group = opt_group.ok_or(MedalError::AccessDenied)?;
2193 if !group.admins.contains(&session.id) {
2194 return Err(MedalError::AccessDenied);
2195 }
2196
2197 user.firstname = Some(firstname);
2198 user.lastname = Some(lastname);
2199 user.grade = grade;
2200 user.sex = sex;
2201
2202 if street.is_some() {
2203 user.street = street;
2204 }
2205 if zip.is_some() {
2206 user.zip = zip;
2207 }
2208 if city.is_some() {
2209 user.city = city;
2210 }
2211
2212 if let Some((password, salt)) = password_and_salt {
2213 user.password = Some(password);
2214 user.salt = Some(salt);
2215 }
2216
2217 conn.save_session(user);
2218 }
2219 }
2220
2221 Ok(result)
2222}
2223
2224pub fn check_profile<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
2225 (firstname, lastname): (String, String))
2226 -> JsonValueResult {
2227 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2228
2229 if session.csrf_token != csrf_token {
2230 return Err(MedalError::AccessDenied);
2231 }
2232
2233 let mut data = json_val::Map::new();
2234
2235 if session.firstname.map(|n| n.len() > 0).unwrap_or(false) && session.lastname.map(|n| n.len() > 0).unwrap_or(false)
2237 {
2238 data.insert("exists".to_string(), to_json(&false));
2239 return Ok(data);
2240 }
2241
2242 let group =
2243 conn.get_group_complete(session.managed_by.ok_or(MedalError::AccessDenied)?).ok_or(MedalError::AccessDenied)?;
2244
2245 for member in group.members {
2246 if member.lastname.map(|l| l.to_lowercase()) == Some(lastname.to_lowercase())
2247 && member.firstname.map(|l| l.to_lowercase()) == Some(firstname.to_lowercase())
2248 && member.id != session.id
2249 {
2250 data.insert("exists".to_string(), to_json(&true));
2251 return Ok(data);
2252 }
2253 }
2254
2255 data.insert("exists".to_string(), to_json(&false));
2256 Ok(data)
2257}
2258
2259pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2260 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2261 if !session.is_teacher {
2262 return Err(MedalError::AccessDenied);
2263 }
2264
2265 let mut data = json_val::Map::new();
2266 fill_user_data(&session, &mut data);
2267
2268 Ok(("teacher".to_string(), data))
2269}
2270
2271pub fn admin_index<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2272 let session = conn.get_session(&session_token)
2273 .ensure_logged_in()
2274 .ok_or(MedalError::NotLoggedIn)?
2275 .ensure_admin()
2276 .ok_or(MedalError::AccessDenied)?;
2277
2278 let mut data = json_val::Map::new();
2279 fill_user_data(&session, &mut data);
2280
2281 Ok(("admin".to_string(), data))
2282}
2283
2284pub fn admin_search_users<T: MedalConnection>(conn: &T, session_token: &str,
2285 s_data: (Option<i32>,
2286 Option<String>,
2287 Option<String>,
2288 Option<String>,
2289 Option<String>,
2290 Option<String>))
2291 -> MedalValueResult {
2292 let session = conn.get_session(&session_token)
2293 .ensure_logged_in()
2294 .ok_or(MedalError::NotLoggedIn)?
2295 .ensure_admin()
2296 .ok_or(MedalError::AccessDenied)?;
2297
2298 let mut data = json_val::Map::new();
2299 fill_user_data(&session, &mut data);
2300
2301 match conn.get_search_users(s_data) {
2302 Ok(users) => {
2303 data.insert("users".to_string(), to_json(&users));
2304 data.insert("max_results".to_string(), to_json(&200));
2305 data.insert("num_results".to_string(), to_json(&users.len()));
2306 data.insert("no_results".to_string(), to_json(&(users.len() == 0)));
2307 if users.len() > 200 {
2308 data.insert("more_users".to_string(), to_json(&true));
2309 data.insert("more_results".to_string(), to_json(&true));
2310 }
2311 }
2312 Err(groups) => {
2313 data.insert("groups".to_string(), to_json(&groups));
2314 data.insert("max_results".to_string(), to_json(&200));
2315 data.insert("num_results".to_string(), to_json(&groups.len()));
2316 data.insert("no_results".to_string(), to_json(&(groups.len() == 0)));
2317 if groups.len() > 200 {
2318 data.insert("more_groups".to_string(), to_json(&true));
2319 data.insert("more_results".to_string(), to_json(&true));
2320 }
2321 }
2322 };
2323
2324 Ok(("admin_search_results".to_string(), data))
2325}
2326
2327pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str) -> MedalValueResult {
2328 let session = conn.get_session(&session_token)
2329 .ensure_logged_in()
2330 .ok_or(MedalError::NotLoggedIn)?
2331 .ensure_teacher_or_admin()
2332 .ok_or(MedalError::AccessDenied)?;
2333
2334 let mut data = json_val::Map::new();
2335
2336 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2337
2338 if !session.is_admin() {
2339 if let Some(group) = opt_group.clone() {
2341 if !group.admins.contains(&session.id) {
2342 return Err(MedalError::AccessDenied);
2343 }
2344 } else if user_id != session.id {
2345 return Err(MedalError::AccessDenied);
2346 }
2347 }
2348
2349 fill_user_data(&session, &mut data);
2350 fill_user_data_prefix(&user, &mut data, "user_");
2351 data.insert("user_logincode".to_string(), to_json(&user.logincode));
2352 data.insert("user_id".to_string(), to_json(&user.id));
2353 let grade = if user.grade >= 200 {
2354 "Kein Schüler mehr".to_string()
2355 } else if user.grade >= 11 {
2356 format!("{} ({})", user.grade % 100, if user.grade >= 100 { "G9" } else { "G8" })
2357 } else {
2358 format!("{}", user.grade)
2359 };
2360 data.insert("user_grade".to_string(), to_json(&grade));
2361 data.insert("user_oauthid".to_string(), to_json(&user.oauth_foreign_id));
2362 data.insert("user_oauthprovider".to_string(), to_json(&user.oauth_provider));
2363
2364 if let Some(group) = opt_group {
2365 data.insert("user_group_id".to_string(), to_json(&group.id));
2366 data.insert("user_group_name".to_string(), to_json(&group.name));
2367 }
2368
2369 let groups: Vec<GroupInfo> =
2370 conn.get_groups(user_id)
2371 .iter()
2372 .map(|g| GroupInfo { id: g.id.unwrap(),
2373 name: g.name.clone(),
2374 tag: g.tag.clone(),
2375 code: g.groupcode.clone() })
2376 .collect();
2377 data.insert("user_group".to_string(), to_json(&groups));
2378
2379 let parts = conn.get_all_participations_complete(user_id);
2380 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2381 let contest_stars = conn.count_all_stars_by_contest(user_id);
2382
2383 let pi: Vec<(i32, String, String, i32)> =
2384 parts.into_iter()
2385 .map(|(p, c)| {
2386 (c.id.unwrap(),
2387 format!("{}{}{}",
2388 &c.name,
2389 if c.protected { " (geschützt)" } else { "" },
2390 if p.team.is_some() { " (Teamteilnahme)" } else { "" }),
2391 self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(p.start)).unwrap(),
2392 contest_stars.iter()
2393 .filter_map(|(id, stars)| if *id == c.id.unwrap() { Some(*stars) } else { None })
2394 .next()
2395 .unwrap_or(0))
2396 })
2397 .collect();
2398
2399 data.insert("user_participations".to_string(), to_json(&pi));
2400 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2401 data.insert("can_delete".to_string(),
2402 to_json(&((!has_protected_participations || session.is_admin()) && groups.len() == 0)));
2403
2404 Ok(("admin_user".to_string(), data))
2405}
2406
2407pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str, csrf_token: &str)
2408 -> JsonValueResult {
2409 let session = conn.get_session(&session_token)
2410 .ensure_logged_in()
2411 .ok_or(MedalError::NotLoggedIn)?
2412 .ensure_teacher_or_admin()
2413 .ok_or(MedalError::AccessDenied)?;
2414
2415 if session.csrf_token != csrf_token {
2416 return Err(MedalError::CsrfCheckFailed);
2417 }
2418
2419 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2420
2421 if !session.is_admin() {
2422 if let Some(group) = opt_group {
2424 if !group.admins.contains(&session.id) {
2425 return Err(MedalError::AccessDenied);
2426 }
2427 } else {
2428 return Err(MedalError::AccessDenied);
2429 }
2430 }
2431
2432 let parts = conn.get_all_participations_complete(user_id);
2433 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2434 let groups = conn.get_groups(user_id);
2435
2436 let mut data = json_val::Map::new();
2437 if has_protected_participations && !session.is_admin() {
2438 data.insert("reason".to_string(), to_json(&"Benutzer hat Teilnahmen an geschützten Wettbewerben."));
2439 Err(MedalError::ErrorWithJson(data))
2440 } else if groups.len() > 0 {
2441 data.insert("reason".to_string(), to_json(&"Benutzer ist Administrator von Gruppen."));
2442 Err(MedalError::ErrorWithJson(data))
2443 } else {
2444 conn.delete_user(user_id);
2445 Ok(data)
2446 }
2447}
2448
2449pub fn admin_move_user_to_group<T: MedalConnection>(conn: &T, user_id: i32, group_id: i32, session_token: &str,
2450 csrf_token: &str)
2451 -> JsonValueResult {
2452 let session = conn.get_session(&session_token)
2453 .ensure_logged_in()
2454 .ok_or(MedalError::NotLoggedIn)?
2455 .ensure_admin()
2456 .ok_or(MedalError::AccessDenied)?;
2457
2458 if session.csrf_token != csrf_token {
2459 return Err(MedalError::CsrfCheckFailed);
2460 }
2461
2462 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2463
2464 if !session.is_admin() {
2465 if let Some(group) = opt_group {
2467 if !group.admins.contains(&session.id) {
2468 return Err(MedalError::AccessDenied);
2469 }
2470 } else {
2471 return Err(MedalError::AccessDenied);
2472 }
2473 }
2474
2475 let mut data = json_val::Map::new();
2476 if conn.get_group(group_id).is_some() {
2477 if let Some(mut user) = conn.get_user_by_id(user_id) {
2478 user.managed_by = Some(group_id);
2479 conn.save_session(user);
2480 Ok(data)
2481 } else {
2482 data.insert("reason".to_string(), to_json(&"Benutzer existiert nicht."));
2483 Err(MedalError::ErrorWithJson(data))
2484 }
2485 } else {
2486 data.insert("reason".to_string(), to_json(&"Gruppe existiert nicht."));
2487 Err(MedalError::ErrorWithJson(data))
2488 }
2489}
2490
2491#[derive(Serialize, Deserialize)]
2492pub struct AdminInfo {
2493 pub id: i32,
2494 pub firstname: String,
2495 pub lastname: String,
2496}
2497
2498pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2499 let session = conn.get_session(&session_token)
2500 .ensure_logged_in()
2501 .ok_or(MedalError::NotLoggedIn)?
2502 .ensure_teacher_or_admin()
2503 .ok_or(MedalError::AccessDenied)?;
2504
2505 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2508 if !group.admins.contains(&session.id) {
2510 return Err(MedalError::AccessDenied);
2511 }
2512 }
2513
2514 let mut data = json_val::Map::new();
2515 fill_user_data(&session, &mut data);
2516
2517 let gi = GroupInfo { id: group.id.unwrap(),
2518 name: group.name.clone(),
2519 tag: group.tag.clone(),
2520 code: group.groupcode.clone() };
2521
2522 let v: Vec<MemberInfo> =
2523 group.members
2524 .iter()
2525 .filter(|m| session.is_admin() || m.firstname.is_some() || m.lastname.is_some())
2526 .map(|m| MemberInfo { id: m.id,
2527 firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
2528 lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
2529 sex: (match m.sex {
2530 Some(0) | None => "/",
2531 Some(1) => "m",
2532 Some(2) => "w",
2533 Some(3) => "d",
2534 Some(4) => "…",
2535 _ => "?",
2536 }).to_string(),
2537 grade: grade_to_string(m.grade),
2538 logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()),
2539 anonymous: m.anonymous })
2540 .collect();
2541
2542 let has_anonmous_members = group.members.iter().any(|m| m.anonymous);
2543
2544 let has_protected_participations = conn.group_has_protected_participations(group_id);
2545
2546 data.insert("group".to_string(), to_json(&gi));
2547 data.insert("member".to_string(), to_json(&v));
2548 data.insert("groupname".to_string(), to_json(&gi.name));
2549 data.insert("has_anonymous_members".to_string(), to_json(&has_anonmous_members));
2550 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2551 data.insert("can_delete".to_string(), to_json(&(!has_protected_participations || session.is_admin())));
2552
2553 let admins: Vec<AdminInfo> =
2554 group.admins
2555 .iter()
2556 .map(|a| {
2557 let admin = conn.get_user_by_id(*a).ok_or(MedalError::AccessDenied)?;
2558 Ok(AdminInfo { id: admin.id,
2559 firstname: admin.firstname.clone().unwrap_or_else(|| "".to_string()),
2560 lastname: admin.lastname.clone().unwrap_or_else(|| "".to_string()) })
2561 })
2562 .collect::<Result<Vec<_>, _>>()?;
2563
2564 data.insert("group_admin".to_string(), to_json(&admins));
2565 data.insert("morethanoneadmin".to_string(), to_json(&(admins.len() > 1)));
2566
2567 Ok(("admin_group".to_string(), data))
2568}
2569
2570pub fn group_add_admin<T: MedalConnection>(conn: &T, group_id: i32, new_admin_id: i32, session_token: &str,
2571 csrf_token: &str)
2572 -> JsonValueResult {
2573 let session = conn.get_session(&session_token)
2574 .ensure_logged_in()
2575 .ok_or(MedalError::NotLoggedIn)?
2576 .ensure_teacher_or_admin()
2577 .ok_or(MedalError::AccessDenied)?;
2578
2579 if session.csrf_token != csrf_token {
2580 return Err(MedalError::CsrfCheckFailed);
2581 }
2582
2583 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2586 if !group.admins.contains(&session.id) {
2588 return Err(MedalError::AccessDenied);
2589 }
2590 }
2591
2592 if group.admins.contains(&new_admin_id) {
2593 let mut data = json_val::Map::new();
2594 data.insert("reason".to_string(), to_json(&"Benutzer ist bereits Admin."));
2595 return Err(MedalError::ErrorWithJson(data));
2596 }
2597
2598 let new_admin = conn.get_user_by_id(new_admin_id).ok_or(MedalError::NotFound)?;
2599 let first_admin_id = group.admins.first().ok_or(MedalError::NotFound)?;
2600 let first_admin = conn.get_user_by_id(*first_admin_id).ok_or(MedalError::NotFound)?;
2601 if new_admin.oauth_provider == first_admin.oauth_provider {
2602 if let Some((_, new_admin_school)) = new_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2603 {
2604 if let Some((_, first_admin_school)) =
2605 first_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2606 {
2607 if new_admin_school == first_admin_school && new_admin_school.len() >= 1 {
2608 conn.add_admin_to_group(&mut group, new_admin_id);
2609
2610 let data = json_val::Map::new();
2611 return Ok(data);
2612 }
2613 }
2614 }
2615 }
2616
2617 let mut data = json_val::Map::new();
2618 data.insert("reason".to_string(),
2619 to_json(&"Benutzer gehört nicht zur gleichen Schule oder Benutzer nicht als Lehrkraft angemeldet."));
2620 Err(MedalError::ErrorWithJson(data))
2621}
2622
2623pub fn group_delete_admin<T: MedalConnection>(conn: &T, group_id: i32, admin_id: i32, session_token: &str,
2624 csrf_token: &str)
2625 -> JsonValueResult {
2626 let session = conn.get_session(&session_token)
2627 .ensure_logged_in()
2628 .ok_or(MedalError::NotLoggedIn)?
2629 .ensure_teacher_or_admin()
2630 .ok_or(MedalError::AccessDenied)?;
2631
2632 if session.csrf_token != csrf_token {
2633 return Err(MedalError::CsrfCheckFailed);
2634 }
2635
2636 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2639 if !group.admins.contains(&session.id) {
2641 return Err(MedalError::AccessDenied);
2642 }
2643 }
2644
2645 if group.admins.len() == 1 {
2646 let mut data = json_val::Map::new();
2647 data.insert("reason".to_string(), to_json(&"Kann letzten Admin nicht entfernen."));
2648 return Err(MedalError::ErrorWithJson(data));
2649 }
2650
2651 if !group.admins.contains(&admin_id) {
2652 let mut data = json_val::Map::new();
2653 data.insert("reason".to_string(), to_json(&"Benutzer ist kein Admin."));
2654 return Err(MedalError::ErrorWithJson(data));
2655 }
2656
2657 conn.remove_admin_from_group(&mut group, admin_id);
2658 let data = json_val::Map::new();
2659 Ok(data)
2660}
2661
2662pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str)
2663 -> JsonValueResult {
2664 let session = conn.get_session(&session_token)
2665 .ensure_logged_in()
2666 .ok_or(MedalError::NotLoggedIn)?
2667 .ensure_teacher_or_admin()
2668 .ok_or(MedalError::AccessDenied)?;
2669
2670 if session.csrf_token != csrf_token {
2671 return Err(MedalError::CsrfCheckFailed);
2672 }
2673
2674 let group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2677 if !group.admins.contains(&session.id) {
2679 return Err(MedalError::AccessDenied);
2680 }
2681 }
2682
2683 let mut data = json_val::Map::new();
2684 if conn.group_has_protected_participations(group_id) && !session.is_admin() {
2685 data.insert("reason".to_string(), to_json(&"Gruppe hat Mitglieder mit geschützten Teilnahmen."));
2686 Err(MedalError::ErrorWithJson(data))
2687 } else {
2688 conn.delete_all_users_for_group(group_id);
2689 conn.delete_group(group_id);
2690 Ok(data)
2691 }
2692}
2693
2694pub fn admin_show_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2695 let session = conn.get_session(&session_token)
2696 .ensure_logged_in()
2697 .ok_or(MedalError::NotLoggedIn)?
2698 .ensure_teacher_or_admin()
2699 .ok_or(MedalError::AccessDenied)?;
2700
2701 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2704 if !group.admins.contains(&session.id) {
2706 return Err(MedalError::AccessDenied);
2707 }
2708 }
2709
2710 let mut data = json_val::Map::new();
2711 fill_user_data(&session, &mut data);
2712
2713 let gi = GroupInfo { id: group.id.unwrap(),
2714 name: group.name.clone(),
2715 tag: group.tag.clone(),
2716 code: group.groupcode.clone() };
2717
2718 data.insert("group".to_string(), to_json(&gi));
2719 data.insert("groupname".to_string(), to_json(&gi.name));
2720 data.insert("grouptag".to_string(), to_json(&gi.name));
2721
2722 Ok(("admin_edit_group".to_string(), data))
2723}
2724
2725pub fn admin_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str,
2726 name: String, tag: String)
2727 -> MedalResult<()> {
2728 let session = conn.get_session(&session_token)
2729 .ensure_logged_in()
2730 .ok_or(MedalError::NotLoggedIn)?
2731 .ensure_teacher_or_admin()
2732 .ok_or(MedalError::AccessDenied)?;
2733
2734 if session.csrf_token != csrf_token {
2735 return Err(MedalError::CsrfCheckFailed);
2736 }
2737
2738 let mut group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2741 if !group.admins.contains(&session.id) {
2743 return Err(MedalError::AccessDenied);
2744 }
2745 }
2746
2747 group.name = name;
2748 group.tag = tag;
2749
2750 conn.save_group(&mut group);
2751
2752 Ok(())
2753}
2754
2755#[derive(Serialize, Deserialize, Debug)]
2756struct SubmissionResult {
2757 id: i32,
2758 grade: i32,
2759 date: String,
2760}
2761#[derive(Serialize, Deserialize, Debug)]
2762struct TaskResult {
2763 id: i32,
2764 stars: i32,
2765 submissions: Vec<SubmissionResult>,
2766}
2767#[derive(Serialize, Deserialize, Debug)]
2768struct TaskgroupResult {
2769 id: i32,
2770 name: String,
2771 tasks: Vec<TaskResult>,
2772}
2773
2774pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
2775 -> MedalValueResult {
2776 let session = conn.get_session(&session_token)
2777 .ensure_logged_in()
2778 .ok_or(MedalError::NotLoggedIn)?
2779 .ensure_teacher_or_admin()
2780 .ok_or(MedalError::AccessDenied)?;
2781
2782 let user = conn.get_user_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2783 let participation = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2784
2785 let user_or_team = if let Some(team) = participation.team { team } else { user_id };
2786
2787 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2788
2789 if !session.is_admin() {
2790 if let Some(ref group) = opt_group {
2792 if !group.admins.contains(&session.id) {
2793 return Err(MedalError::AccessDenied);
2794 }
2795 } else {
2796 return Err(MedalError::AccessDenied);
2797 }
2798 }
2799
2800 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2801 let grades = conn.get_contest_user_grades(user_or_team, contest_id);
2802
2803 let mut totalgrade = 0;
2804 let mut max_totalgrade = 0;
2805
2806 for taskgroup in &contest.taskgroups {
2807 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
2808 }
2809 for grade in grades {
2810 totalgrade += grade.grade.unwrap_or(0);
2811 }
2812
2813 #[rustfmt::skip]
2814 let subms: Vec<TaskgroupResult> =
2815 contest.taskgroups
2816 .into_iter()
2817 .map(|tg| TaskgroupResult {
2818 id: tg.id.unwrap(),
2819 name: tg.name,
2820 tasks: tg.tasks
2821 .into_iter()
2822 .map(|t| TaskResult {
2823 id: t.id.unwrap(),
2824 stars: t.stars,
2825 submissions: conn.get_all_submissions(user_or_team, t.id.unwrap(), None)
2826 .into_iter()
2827 .map(|s| SubmissionResult {
2828 id: s.id.unwrap(),
2829 grade: s.grade,
2830 date: self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(s.date)).unwrap(),
2831 })
2832 .collect(),
2833 })
2834 .collect(),
2835 })
2836 .collect();
2837
2838 let mut data = json_val::Map::new();
2839
2840 data.insert("submissions".to_string(), to_json(&subms));
2841 data.insert("contestid".to_string(), to_json(&contest.id));
2842 data.insert("contestname".to_string(), to_json(&contest.name));
2843 data.insert("has_timelimit".to_string(), to_json(&(contest.duration > 0)));
2844
2845 data.insert("total_grade".to_string(), to_json(&totalgrade));
2846 data.insert("max_total_grade".to_string(), to_json(&max_totalgrade));
2847
2848 if let Some(group) = opt_group {
2849 data.insert("group_id".to_string(), to_json(&group.id));
2850 data.insert("group_name".to_string(), to_json(&group.name));
2851 }
2852
2853 fill_user_data(&session, &mut data);
2854 fill_user_data_prefix(&user, &mut data, "user_");
2855 data.insert("user_id".to_string(), to_json(&user.id));
2856
2857 data.insert("start_date".to_string(),
2858 to_json(&self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(participation.start)).unwrap()));
2859
2860 data.insert("can_delete".to_string(), to_json(&(!contest.protected || session.is_admin.unwrap_or(false))));
2861 Ok(("admin_participation".to_string(), data))
2862}
2863
2864pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str,
2865 csrf_token: &str)
2866 -> JsonValueResult {
2867 let session = conn.get_session(&session_token)
2868 .ensure_logged_in()
2869 .ok_or(MedalError::NotLoggedIn)?
2870 .ensure_teacher_or_admin()
2871 .ok_or(MedalError::AccessDenied)?;
2872
2873 if session.csrf_token != csrf_token {
2874 return Err(MedalError::CsrfCheckFailed);
2875 }
2876
2877 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2878 let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2879 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2880
2881 if !session.is_admin() {
2882 if contest.protected {
2884 return Err(MedalError::AccessDenied);
2885 }
2886
2887 if let Some(group) = opt_group {
2888 if !group.admins.contains(&session.id) {
2889 return Err(MedalError::AccessDenied);
2890 }
2891 } else {
2892 return Err(MedalError::AccessDenied);
2893 }
2894 }
2895
2896 let mut data = json_val::Map::new();
2897 fill_user_data(&session, &mut data);
2898
2899 conn.delete_participation(user_id, contest_id);
2900 Ok(data)
2901}
2902
2903pub fn admin_show_contests<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2904 let session = conn.get_session(&session_token)
2905 .ensure_logged_in()
2906 .ok_or(MedalError::NotLoggedIn)?
2907 .ensure_admin()
2908 .ok_or(MedalError::AccessDenied)?;
2909
2910 let mut data = json_val::Map::new();
2911 fill_user_data(&session, &mut data);
2912
2913 let mut contests: Vec<_> = conn.get_contest_list().into_iter().map(|contest| (contest.id, contest.name)).collect();
2914 contests.sort(); contests.reverse();
2916
2917 data.insert("contests".to_string(), to_json(&contests));
2918
2919 Ok(("admin_contests".to_string(), data))
2920}
2921
2922pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalResult<String> {
2923 conn.get_session(&session_token)
2924 .ensure_logged_in()
2925 .ok_or(MedalError::NotLoggedIn)?
2926 .ensure_admin()
2927 .ok_or(MedalError::AccessDenied)?;
2928
2929 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2930
2931 let taskgroup_ids: Vec<(i32, String)> =
2932 contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect();
2933 let filename = format!("contest_{}__{}__{}.csv",
2934 contest_id,
2935 self::time::strftime("%F_%H-%M-%S", &self::time::now()).unwrap(),
2936 helpers::make_filename_secret());
2937
2938 conn.export_contest_results_to_file(contest_id, &taskgroup_ids, &format!("./export/{}", filename));
2939
2940 Ok(filename)
2941}
2942
2943pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2944 let session = conn.get_session(&session_token)
2945 .ensure_logged_in()
2946 .ok_or(MedalError::NotLoggedIn)?
2947 .ensure_admin()
2948 .ok_or(MedalError::AccessDenied)?;
2949
2950 let mut data = json_val::Map::new();
2951 fill_user_data(&session, &mut data);
2952
2953 let now = time::get_time();
2954 let maxage = now - time::Duration::days(30); let n_temporary_session = conn.count_temporary_sessions(maxage);
2956 data.insert("temporary_session_count".to_string(), to_json(&n_temporary_session));
2957
2958 Ok(("admin_cleanup".to_string(), data))
2959}
2960
2961pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str) -> JsonValueResult {
2962 let session = conn.get_session(&session_token)
2963 .ensure_logged_in()
2964 .ok_or(MedalError::NotLoggedIn)?
2965 .ensure_admin()
2966 .ok_or(MedalError::AccessDenied)?;
2967
2968 if session.csrf_token != csrf_token {
2969 return Err(MedalError::CsrfCheckFailed);
2970 }
2971
2972 let now = time::get_time();
2973 let maxstudentage = now - time::Duration::days(450); let maxteacherage = now - time::Duration::days(1095); let maxage = now - time::Duration::days(3650); let result = conn.remove_old_users_and_groups(maxstudentage, Some(maxteacherage), Some(maxage));
2978
2979 let mut data = json_val::Map::new();
2980 if let Ok((n_user, n_group, n_teacher, n_other)) = result {
2981 data.insert("n_user".to_string(), to_json(&n_user));
2982 data.insert("n_group".to_string(), to_json(&n_group));
2983 data.insert("n_teacher".to_string(), to_json(&n_teacher));
2984 data.insert("n_other".to_string(), to_json(&n_other));
2985 Ok(data)
2986 } else {
2987 data.insert("reason".to_string(), to_json(&"Datenbank-Fehler."));
2988 Err(MedalError::ErrorWithJson(data))
2989 }
2990}
2991
2992pub fn do_session_cleanup<T: MedalConnection>(conn: &T) -> JsonValueResult {
2993 let now = time::get_time();
2994 let maxage = now - time::Duration::days(30); conn.remove_temporary_sessions(maxage, Some(1000));
2997
2998 let data = json_val::Map::new();
2999 Ok(data)
3000}
3001
3002pub fn move_task_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
3003 new_location: &str, contest: Option<i32>)
3004 -> JsonValueResult {
3005 let session = conn.get_session(&session_token)
3006 .ensure_logged_in()
3007 .ok_or(MedalError::NotLoggedIn)?
3008 .ensure_admin()
3009 .ok_or(MedalError::AccessDenied)?;
3010
3011 if session.csrf_token != csrf_token {
3012 return Err(MedalError::CsrfCheckFailed);
3013 }
3014
3015 let mut data = json_val::Map::new();
3016 if old_location == new_location {
3017 data.insert("reason".to_string(), to_json(&"old and new location identical"));
3018 return Err(MedalError::ErrorWithJson(data));
3019 }
3020
3021 let n_contest = conn.move_task_location(old_location, new_location, contest);
3022
3023 data.insert("contests_modified".to_string(), to_json(&n_contest));
3024
3025 Ok(data)
3026}
3027
3028#[derive(PartialEq, Eq, Clone, Copy)]
3029pub enum UserType {
3030 User,
3031 Teacher,
3032 Admin,
3033}
3034
3035pub enum UserSex {
3036 Female,
3037 Male,
3038 Unknown,
3039}
3040
3041pub struct ForeignUserData {
3042 pub foreign_id: String,
3043 pub foreign_type: UserType,
3044 pub sex: UserSex,
3045 pub firstname: String,
3046 pub lastname: String,
3047 pub school_name: Option<String>,
3048}
3049
3050pub fn login_oauth<T: MedalConnection>(conn: &T, user_data: ForeignUserData, oauth_provider_id: String,
3051 autoclean_submissions: bool)
3052 -> Result<(String, bool), (String, json_val::Map<String, json_val::Value>)> {
3053 match conn.login_foreign(None,
3054 &oauth_provider_id,
3055 &user_data.foreign_id,
3056 (user_data.foreign_type != UserType::User,
3057 user_data.foreign_type == UserType::Admin,
3058 &user_data.firstname,
3059 &user_data.lastname,
3060 match user_data.sex {
3061 UserSex::Male => Some(1),
3062 UserSex::Female => Some(2),
3063 UserSex::Unknown => Some(0),
3064 },
3065 &user_data.school_name))
3066 {
3067 Ok((session_token, last_activity)) => {
3068 let redirect_profile = if let Some(last_activity) = last_activity {
3069 let now = time::get_time();
3070 now - last_activity > time::Duration::days(60)
3071 } else {
3072 true
3073 };
3074
3075 let now = time::get_time();
3079 let maxage = now - time::Duration::days(30);
3080 conn.remove_temporary_sessions(maxage, None); if autoclean_submissions {
3083 let maxage = now - time::Duration::days(30);
3084 conn.remove_autosaved_submissions(maxage, None); let maxage = now - time::Duration::days(90);
3087 conn.remove_all_but_latest_submissions(maxage, None); }
3089
3090 Ok((session_token, redirect_profile))
3091 }
3092 Err(()) => {
3093 let mut data = json_val::Map::new();
3094 data.insert("reason".to_string(), to_json(&"OAuth-Login failed.".to_string()));
3095 Err(("login".to_owned(), data))
3096 }
3097 }
3098}