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
578 let contest_duration_for_user_seconds = if let Some(additional_contest_time) = session.additional_contest_time {
579 (contest.duration as i64 * 60 * (100 + additional_contest_time as i64)) / 100
580 } else {
581 contest.duration as i64 * 60
582 };
583
584 let left_secs_total = contest_duration_for_user_seconds - passed_secs_total;
585
586 let is_time_left = contest.duration == 0 || left_secs_total >= 0;
587 let exempt_from_timelimit = session.is_teacher() || session.is_admin();
588
589 let can_still_compete = is_time_left || exempt_from_timelimit;
590
591 let review_has_timelimit = contest.review_end.is_none() && contest.review_start.is_some();
592 let has_future_review = (contest.review_start.is_some() || contest.review_end.is_some())
593 && contest.review_end.map(|end| end > now).unwrap_or(true);
594 let has_review_end = contest.review_end.is_some();
595 let is_review = !can_still_compete
596 && (contest.review_start.is_some() || contest.review_end.is_some())
597 && contest.review_start.map(|start| now >= start).unwrap_or(true)
598 && contest.review_end.map(|end| now <= end).unwrap_or(true);
599
600 let until_review_start = contest.review_start.map(|start| start.sec - now.sec).unwrap_or(0);
601 let until_review_end = contest.review_end.map(|end| end.sec - now.sec).unwrap_or(0);
602
603 ContestTimeInfo { passed_secs_total,
604 left_secs_total,
605 left_mins_total: left_secs_total / 60,
606 left_hour: left_secs_total / (60 * 60),
607 left_min: (left_secs_total / 60) % 60,
608 left_sec: left_secs_total % 60,
609 has_timelimit: contest.duration != 0,
610 is_time_left,
611 exempt_from_timelimit,
612 can_still_compete,
613 review_has_timelimit,
614 has_future_review,
615 has_review_end,
616 is_review,
617 can_still_compete_or_review: can_still_compete || is_review,
618
619 until_review_start_day: until_review_start / (60 * 60 * 24),
620 until_review_start_hour: (until_review_start / (60 * 60)) % 24,
621 until_review_start_min: (until_review_start / 60) % 60,
622
623 until_review_end_day: until_review_end / (60 * 60 * 24),
624 until_review_end_hour: (until_review_end / (60 * 60)) % 24,
625 until_review_end_min: (until_review_end / 60) % 60 }
626}
627
628pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
629 query_string: Option<String>, login_info: LoginInfo, secret: Option<String>)
630 -> MedalResult<Result<MedalValue, i32>> {
631 let session = conn.get_session_or_new(&session_token).unwrap();
632
633 if session.logincode.is_some() && session.firstname.is_none() {
634 return Err(MedalError::AccountIncomplete);
635 }
636
637 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
638
639 let mut opt_part = conn.get_participation(session.id, contest_id);
640 let mut grades = if let Some(part) = opt_part.as_ref() {
641 if let Some(team) = part.team {
642 conn.get_contest_user_grades(team, contest_id)
643 } else {
644 conn.get_contest_user_grades(session.id, contest_id)
645 }
646 } else {
647 conn.get_contest_user_grades(session.id, contest_id)
648 };
649
650 let ci = ContestInfo { id: contest.id.unwrap(),
651 name: contest.name.clone(),
652 duration: contest.duration,
653 public: contest.public,
654 requires_login: contest.requires_login.unwrap_or(false),
655 image: None,
656 language: None,
657 category: contest.category.clone(),
658 team_participation: contest.max_teamsize.map(|size| size > 1).unwrap_or(false),
659 colour: contest.colour.clone(),
660 tags: Vec::new() };
661
662 let mut data = json_val::Map::new();
663 data.insert("parent".to_string(), to_json(&"base"));
664 data.insert("empty".to_string(), to_json(&"empty"));
665 data.insert("contest".to_string(), to_json(&ci));
666 data.insert("title".to_string(), to_json(&ci.name));
667 data.insert("message".to_string(), to_json(&contest.message));
668 fill_oauth_data(login_info, &mut data);
669
670 if secret.is_some() && secret != contest.secret {
671 return Err(MedalError::AccessDenied);
672 }
673
674 let has_secret = contest.secret.is_some();
675 let mut require_secret = false;
676 if has_secret {
677 data.insert("secret_field".to_string(), to_json(&true));
678
679 if secret.is_some() {
680 data.insert("secret_field_prefill".to_string(), to_json(&secret));
681 } else {
682 require_secret = true;
683 }
684 }
685
686 let constraints = check_contest_constraints(&session, &contest);
687 let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
688
689 let has_tasks = contest.taskgroups.len() > 0;
690 let can_start = constraints.contest_running
691 && constraints.grade_matching
692 && is_qualified
693 && (has_tasks || has_secret)
694 && (session.is_logged_in() || contest.secret.is_some() && !contest.requires_login.unwrap_or(false));
695
696 let has_duration = contest.duration > 0;
697
698 data.insert("constraints".to_string(), to_json(&constraints));
699 data.insert("is_qualified".to_string(), to_json(&is_qualified));
700 data.insert("has_duration".to_string(), to_json(&has_duration));
701 data.insert("can_start".to_string(), to_json(&can_start));
702 data.insert("has_tasks".to_string(), to_json(&has_tasks));
703 data.insert("no_tasks".to_string(), to_json(&!has_tasks));
704
705 if opt_part.is_none()
710 && contest.duration == 0
711 && constraints.contest_running
712 && constraints.grade_matching
713 && !require_secret
714 && contest.requires_login != Some(true)
715 {
716 conn.new_participation(session.id, contest_id, None).map_err(|_| MedalError::AccessDenied)?;
717 opt_part = Some(Participation { contest: contest_id,
718 user: session.id,
719 start: time::get_time(),
720 team: None,
721 annotation: None });
722 }
723
724 let now = time::get_time();
725 if let Some(start) = contest.start {
726 if now < start {
727 let until = start - now;
728 data.insert("time_until_start".to_string(),
729 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
730 }
731 }
732
733 if let Some(end) = contest.end {
734 if now < end {
735 let until = end - now;
736 data.insert("time_until_end".to_string(),
737 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
738 }
739 }
740
741 if session.is_logged_in() {
742 data.insert("logged_in".to_string(), to_json(&true));
743 data.insert("username".to_string(), to_json(&session.username));
744 data.insert("firstname".to_string(), to_json(&session.firstname));
745 data.insert("lastname".to_string(), to_json(&session.lastname));
746 data.insert("teacher".to_string(), to_json(&session.is_teacher));
747 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
748 }
749
750 if let Some(participation) = opt_part {
751 let time_info = check_contest_time_left(&session, &contest, &participation);
752 data.insert("time_info".to_string(), to_json(&time_info));
753
754 let time_left_formatted =
755 format!("{}:{:02}:{:02}", time_info.left_hour, time_info.left_min, time_info.left_sec);
756 data.insert("time_left_formatted".to_string(), to_json(&time_left_formatted));
757
758 if let Some(team) = participation.team {
760 if team != session.id {
762 if time_info.is_time_left {
763 data.insert("block_team_participation".to_string(), to_json(&true));
765 } else {
766 grades = conn.get_contest_user_grades(team, contest_id);
768 }
769 }
770
771 let names =
772 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
773 .iter()
774 .filter(|user| user.id != session.id)
775 .map(|user| {
776 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
777 })
778 .collect::<Vec<String>>()
779 .join(", ");
780 data.insert("team_partners".to_string(), to_json(&names));
781 }
782
783 let mut totalgrade = 0;
784 let mut max_totalgrade = 0;
785
786 let mut tasks = Vec::new();
787 for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
788 let subtaskstars = generate_subtaskstars(&taskgroup, &grade, None);
789 let ti = TaskInfo { name: taskgroup.name, subtasks: subtaskstars };
790 tasks.push(ti);
791
792 totalgrade += grade.grade.unwrap_or(0);
793 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
794 }
795 let relative_points = if max_totalgrade > 0 { (totalgrade * 100) / max_totalgrade } else { 0 };
796
797 data.insert("tasks".to_string(), to_json(&tasks));
798
799 data.insert("is_started".to_string(), to_json(&true));
800 data.insert("total_points".to_string(), to_json(&totalgrade));
801 data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
802 data.insert("relative_points".to_string(), to_json(&relative_points));
803 data.insert("lean_page".to_string(), to_json(&true));
804
805 if has_tasks && contest.standalone_task.unwrap_or(false) {
806 return Ok(Err(tasks[0].subtasks[0].id));
807 }
808 }
809
810 if let Some(query_string) = query_string {
811 if !query_string.starts_with("bare") {
812 data.insert("not_bare".to_string(), to_json(&true));
813 }
814
815 if query_string.contains("team_participation=no_logincode") {
816 data.insert("team_error".to_string(), to_json(&"Kein Logincode angegeben"));
817 }
818 if query_string.contains("team_participation=invalid_logincode") {
819 data.insert("team_error".to_string(), to_json(&"Ungültiger Logincode angegeben"));
820 }
821 if query_string.contains("team_participation=own_logincode") {
822 data.insert("team_error".to_string(), to_json(&"Eigener Logincode angegeben"));
823 }
824 if query_string.contains("team_participation=logincode_has_participation") {
825 data.insert("team_error".to_string(), to_json(&"Logincode hat diesen Wettbewerb bereits gestartet"));
826 }
827 } else {
828 data.insert("not_bare".to_string(), to_json(&true));
829 }
830
831 Ok(Ok(("contest".to_owned(), data)))
832}
833
834pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
835 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
836 let mut data = json_val::Map::new();
837 fill_user_data(&session, &mut data);
838
839 let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);
840
841 #[derive(Serialize, Deserialize)]
842 struct UserResults {
843 firstname: String,
844 lastname: String,
845 user_id: i32,
846 grade: String,
847 logincode: String,
848 annotation: String,
849 annotation_result: String,
850 team_participants: String,
851 results: Vec<String>,
852 }
853
854 #[derive(Serialize, Deserialize)]
855 struct GroupResults {
856 groupname: String,
857 group_id: i32,
858 groupcode: String,
859 user_results: Vec<UserResults>,
860 }
861
862 let mut results: Vec<GroupResults> = Vec::new();
863 let mut has_annotations = false;
864 let mut has_annotations_result = false;
865 let mut has_teams = false;
866
867 for (group, groupdata) in resultdata {
868 let mut groupresults: Vec<UserResults> = Vec::new();
869
870 for (user, userdata) in groupdata {
871 let mut userresults: Vec<String> = Vec::new();
872
873 userresults.push(String::new());
874 let mut summe = 0;
875
876 for grade in userdata {
877 if let Some(g) = grade.grade {
878 userresults.push(format!("{}", g));
879 summe += g;
880 } else {
881 userresults.push("–".to_string());
882 }
883 }
884
885 userresults[0] = format!("{}", summe);
886
887 let (annotation, annotation_result) = if let Some(annotation) = user.annotation {
888 let mut split = annotation.split('\x1f');
889
890 (split.next()
891 .filter(|s| s.len() > 0)
892 .map(|s| {
893 has_annotations = true;
894 s.to_string()
895 })
896 .unwrap_or_default(),
897 split.next()
898 .filter(|s| s.len() > 0)
899 .map(|s| {
900 has_annotations_result = true;
901 s.to_string()
902 })
903 .unwrap_or_default())
904 } else {
905 (Default::default(), Default::default())
906 };
907
908 let team_participants = if let Some(team) = user.team {
909 has_teams = true;
910 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
911 .iter()
912 .filter(|user| user.id != session.id)
913 .map(|user| {
914 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
915 })
916 .collect::<Vec<String>>()
917 .join(", ")
918 } else {
919 Default::default()
920 };
921
922 groupresults.push(UserResults { firstname: user.firstname.unwrap_or_else(|| "–".to_string()),
923 lastname: user.lastname.unwrap_or_else(|| "–".to_string()),
924 user_id: user.id,
925 grade: grade_to_string(user.grade),
926 logincode: user.logincode.unwrap_or_else(|| "".to_string()),
927 annotation,
928 annotation_result,
929 team_participants,
930 results: userresults });
931 }
932
933 results.push(GroupResults { groupname: group.name.to_string(),
934 group_id: group.id.unwrap_or(0),
935 groupcode: group.groupcode,
936 user_results: groupresults });
937 }
938
939 data.insert("taskname".to_string(), to_json(&tasknames));
940 data.insert("result".to_string(), to_json(&results));
941 data.insert("has_annotations".to_string(), to_json(&has_annotations));
942 data.insert("has_annotations_result".to_string(), to_json(&has_annotations_result));
943 data.insert("has_teams".to_string(), to_json(&has_teams));
944
945 let c = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
946 let ci = ContestInfo { id: c.id.unwrap(),
947 name: c.name.clone(),
948 duration: c.duration,
949 public: c.public,
950 requires_login: c.requires_login.unwrap_or(false),
951 image: None,
952 language: None,
953 category: c.category.clone(),
954 team_participation: false,
955 colour: c.colour.clone(),
956 tags: Vec::new() };
957
958 data.insert("contest".to_string(), to_json(&ci));
959 data.insert("contestname".to_string(), to_json(&c.name));
960
961 Ok(("contestresults".to_owned(), data))
962}
963
964pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str,
965 secret: Option<String>, logincode_team: Option<String>)
966 -> MedalResult<Result<(), String>> {
967 let session = conn.get_session_or_new(&session_token).unwrap();
969 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
971
972 if contest.duration != 0
974 && !session.is_logged_in()
975 && (contest.requires_login.unwrap_or(false) || contest.secret.is_none())
976 {
977 return Err(MedalError::AccessDenied);
978 }
979
980 if session.is_logged_in() && session.csrf_token != csrf_token {
982 return Err(MedalError::CsrfCheckFailed);
983 }
984
985 let constraints = check_contest_constraints(&session, &contest);
987
988 if !(constraints.contest_running && constraints.grade_matching) {
989 return Err(MedalError::AccessDenied);
990 }
991
992 let is_qualified = check_contest_qualification(conn, &session, &contest);
993
994 if is_qualified == Some(false) {
995 return Err(MedalError::AccessDenied);
996 }
997
998 if contest.secret != secret {
999 return Err(MedalError::AccessDenied);
1000 }
1001
1002 if let Some(logincode_team) = logincode_team {
1003 match contest.max_teamsize {
1004 None => return Err(MedalError::AccessDenied), Some(max_teamsize) => {
1006 if max_teamsize < 2 {
1007 return Err(MedalError::AccessDenied); }
1009 }
1010 };
1011
1012 if logincode_team == "" {
1013 return Ok(Err("no_logincode".to_string()));
1014 }
1015
1016 let teampartner = conn.get_user_and_group_by_logincode(&logincode_team);
1017 if let Some((teampartner_user, _)) = teampartner {
1018 if teampartner_user.id == session.id {
1019 return Ok(Err("own_logincode".to_string()));
1020 }
1021
1022 if conn.get_participation(teampartner_user.id, contest_id).is_some() {
1023 return Ok(Err("logincode_has_participation".to_string()));
1024 }
1025
1026 if conn.get_participation(session.id, contest_id).is_some() {
1027 return Err(MedalError::AccessDenied); }
1029
1030 let res = conn.new_participation(session.id, contest_id, Some(session.id));
1034 let _ = conn.new_participation(teampartner_user.id, contest_id, Some(session.id));
1035 return match res {
1036 Ok(_) => Ok(Ok(())),
1037 _ => Err(MedalError::AccessDenied), };
1039 } else {
1040 return Ok(Err("invalid_logincode".to_string()));
1041 }
1042 }
1043
1044 match conn.new_participation(session.id, contest_id, None) {
1046 Ok(_) => Ok(Ok(())),
1047 _ => Err(MedalError::AccessDenied), }
1049}
1050
1051pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String), login_info: LoginInfo)
1052 -> Result<String, MedalValue> {
1053 let (username, password) = login_data;
1054
1055 match conn.login(None, &username, &password) {
1056 Ok(session_token) => Ok(session_token),
1057 Err(()) => {
1058 let mut data = json_val::Map::new();
1059 data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
1060 data.insert("username".to_string(), to_json(&username));
1061 data.insert("parent".to_string(), to_json(&"base"));
1062
1063 fill_oauth_data(login_info, &mut data);
1064
1065 Err(("login".to_owned(), data))
1066 }
1067 }
1068}
1069
1070pub fn login_with_code<T: MedalConnection>(
1071 conn: &T, code: &str, login_info: LoginInfo)
1072 -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
1073 match conn.login_with_code(None, &code.trim()) {
1074 Ok(session_token) => Ok(Ok(session_token)),
1075 Err(()) => match conn.create_user_with_groupcode(None, &code.trim()) {
1076 Ok(session_token) => Ok(Err(session_token)),
1077 Err(()) => {
1078 let mut data = json_val::Map::new();
1079 data.insert("reason".to_string(), to_json(&"Kein gültiger Code. Bitte erneut versuchen.".to_string()));
1080 data.insert("code".to_string(), to_json(&code));
1081 data.insert("parent".to_string(), to_json(&"base"));
1082
1083 fill_oauth_data(login_info, &mut data);
1084
1085 Err(("login".to_owned(), data))
1086 }
1087 },
1088 }
1089}
1090
1091fn webauthn(self_url: &str) -> webauthn::Webauthn {
1092 use webauthn_rs::prelude::*;
1093
1094 let rp_id = self_url.split("://").nth(1).unwrap().split('/').next().unwrap().split(':').next().unwrap();
1096 let rp_origin = Url::parse(self_url).expect("Invalid URL");
1097 WebauthnBuilder::new(rp_id, &rp_origin).expect("Invalid configuration").build().expect("Invalid configuration")
1098}
1099
1100pub fn register_key_challenge<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str) -> JsonValueResult {
1101 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1102
1103 let webauthn = webauthn(self_url);
1104
1105 let cred_ids = conn.get_all_webauthn_credentials();
1106
1107 let (ccr, skr) =
1109 webauthn.start_passkey_registration(webauthn::Uuid::from_u64_pair(0, session.id as u64),
1110 session.firstname.as_ref().unwrap_or(&("".to_string())),
1111 session.firstname.as_ref().unwrap_or(&("".to_string())),
1112 Some(cred_ids.into_iter()
1113 .map(|x| serde_json::from_str(&format!("\"{}\"", x)).unwrap())
1114 .collect()))
1115 .expect("Failed to start webauthn registration.");
1116
1117 conn.set_webauthn_passkey_registration(session.id, &serde_json::json!(skr).to_string());
1118
1119 let mut data = json_val::Map::new();
1120 data.insert("challenge".to_string(), to_json(&ccr));
1121 Ok(data)
1122}
1123
1124pub fn register_key<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str, csrf_token: &str,
1125 credential: String, name: String)
1126 -> JsonValueResult {
1127 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1128
1129 if session.csrf_token != csrf_token {
1130 return Err(MedalError::CsrfCheckFailed);
1131 }
1132
1133 let registration: String = conn.get_webauthn_passkey_registration(session.id).ok_or(MedalError::WebauthnError)?;
1134
1135 let webauthn = webauthn(self_url);
1136
1137 let registration: webauthn::PasskeyRegistration = serde_json::from_str(®istration).unwrap();
1138 let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1139
1140 let passkey = webauthn.finish_passkey_registration(&credential, ®istration);
1141
1142 match passkey {
1143 Ok(passkey) => {
1144 if let serde_json::Value::String(cred_id) = serde_json::json!(passkey.cred_id()) {
1145 conn.add_webauthn_passkey(session.id, &cred_id, &serde_json::json!(passkey).to_string(), &name);
1146 } else {
1147 println!("Webauthn: Could not unwrap cred_id from webauthn Passkey");
1148 return Err(MedalError::WebauthnError);
1149 }
1150 }
1151 Err(webauthn::WebauthnError::UserNotVerified) => {
1152 println!("Webauthn: UserNotVerified");
1154 return Err(MedalError::WebauthnError);
1155 }
1156 Err(err) => {
1157 println!("Webauthn: Other error: {:#?}", err);
1158 return Err(MedalError::WebauthnError);
1159 }
1160 }
1161
1162 let data = json_val::Map::new();
1163 Ok(data)
1164}
1165
1166pub fn delete_key<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, token_id: i32)
1167 -> JsonValueResult {
1168 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1169
1170 if session.csrf_token != csrf_token {
1171 return Err(MedalError::CsrfCheckFailed);
1172 }
1173
1174 if !conn.get_webauthn_passkey_names_for_user(session.id).into_iter().any(|(id, _name)| id == token_id) {
1175 return Err(MedalError::NotFound);
1176 }
1177
1178 conn.delete_webauthn_passkey(session.id, token_id);
1179
1180 let data = json_val::Map::new();
1181 Ok(data)
1182}
1183
1184pub fn login_with_key_challenge<T: MedalConnection>(conn: &T, self_url: &str) -> JsonValue {
1185 let passkeys = conn.get_all_webauthn_passkeys()
1186 .into_iter()
1187 .map(|passkey| {
1188 let passkey: webauthn::Passkey = serde_json::from_str(&passkey).unwrap();
1189 passkey
1190 })
1191 .collect::<Vec<webauthn::Passkey>>();
1192
1193 let webauthn = webauthn(self_url);
1194
1195 let (challenge, authentication) = webauthn.start_passkey_authentication(&passkeys).unwrap();
1196
1197 let auth_id = conn.store_webauthn_auth_challenge(&serde_json::json!(challenge).to_string(),
1198 &serde_json::json!(authentication).to_string());
1199
1200 let mut data = json_val::Map::new();
1201 data.insert("id".to_string(), to_json(&auth_id));
1202 data.insert("challenge".to_string(), to_json(&challenge));
1203 data
1204}
1205
1206pub fn login_with_key<T: MedalConnection>(conn: &T, self_url: &str, auth_id: i32, credential: &str)
1207 -> Result<String, String> {
1208 let authentication = conn.get_webauthn_auth_challenge_by_id(auth_id).unwrap();
1209
1210 let authentication: webauthn::PasskeyAuthentication = serde_json::from_str(&authentication).unwrap();
1211 let credential: webauthn::PublicKeyCredential = serde_json::from_str(&credential).unwrap();
1212
1213 let webauthn = webauthn(self_url);
1214
1215 match webauthn.finish_passkey_authentication(&credential, &authentication) {
1216 Err(err) => {
1217 println!("Webauthn: Other error: {:#?}", err);
1218 let mut data = json_val::Map::new();
1219 data.insert("reason".to_string(),
1220 to_json(&"Unbekannter Key. Bitte Key zuerst im Profil registrieren.".to_string()));
1221 Err(serde_json::json!(data).to_string())
1222 }
1223 Ok(authresult) => {
1224 if let serde_json::Value::String(cred_id) = serde_json::json!(authresult.cred_id()) {
1225 match conn.login_with_key(None, &cred_id) {
1226 Ok(session_token) => Ok(session_token),
1227 Err(()) => {
1228 let mut data = json_val::Map::new();
1229 println!("Webauthn: Auth fail");
1230 data.insert("reason".to_string(), to_json(&"Key konnte nicht authentifiziert werden. Bitte über anderen Weg einloggen.".to_string()));
1231 Err(serde_json::json!(data).to_string())
1232 }
1233 }
1234 } else {
1235 panic!("Webauthn: Could not unwrap cred_id from webauthn AuthenticationResult")
1236 }
1237 }
1238 }
1239}
1240
1241pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
1242 session_token.map(|token| conn.logout(&token));
1243}
1244
1245#[cfg(feature = "signup")]
1246pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
1247 -> MedalResult<SignupResult> {
1248 let (username, email, password) = signup_data;
1249
1250 if username == "" || email == "" || password == "" {
1251 return Ok(SignupResult::EmptyFields);
1252 }
1253
1254 let salt = helpers::make_salt();
1255 let hash = helpers::hash_password(&password, &salt)?;
1256
1257 let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
1258 Ok(result)
1259}
1260
1261#[cfg(feature = "signup")]
1262pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
1263 let mut data = json_val::Map::new();
1264 if let Some(query) = query_string {
1265 if let Some(status) = query.strip_prefix("status=") {
1266 if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
1267 data.insert((status).to_string(), to_json(&true));
1268 }
1269 }
1270 }
1271 data
1272}
1273
1274pub fn load_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, subtask: Option<String>,
1275 submission_id: Option<i32>)
1276 -> MedalResult<String> {
1277 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1278
1279 match submission_id {
1280 None => match conn.load_submission(&session, task_id, subtask.as_deref()) {
1281 Some(submission) => Ok(submission.value),
1282 None => Ok("{}".to_string()),
1283 },
1284 Some(submission_id) => {
1285 let (submission, _, _, _) =
1286 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1287
1288 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1290 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1291 if !group.admins.contains(&session.id) {
1292 return Err(MedalError::AccessDenied);
1294 }
1295 } else {
1296 return Err(MedalError::AccessDenied);
1298 }
1299 }
1300 Ok(submission.value)
1301 }
1302 }
1303}
1304
1305#[allow(clippy::too_many_arguments)]
1306pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
1307 data: String, grade_percentage: i32, autosave: bool,
1308 subtask: Option<String>)
1309 -> MedalResult<String> {
1310 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1311
1312 if session.csrf_token != csrf_token {
1313 return Err(MedalError::CsrfCheckFailed);
1314 }
1315
1316 let (t, _, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1317
1318 match conn.get_participation(session.id, contest.id.expect("Value from database")) {
1319 None => return Err(MedalError::AccessDenied),
1320 Some(participation) => {
1321 let time_info = check_contest_time_left(&session, &contest, &participation);
1322 if !time_info.can_still_compete && time_info.left_secs_total < -10 {
1323 return Err(MedalError::AccessDenied);
1324 }
1327 if participation.team.is_some() && participation.team != Some(session.id) {
1328 return Err(MedalError::AccessDenied);
1329 }
1330 }
1331 }
1332
1333 let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1348
1349 let submission = Submission { id: None,
1368 user: session.id,
1369 task: task_id,
1370 grade: grade_rounded,
1371 validated: false,
1372 nonvalidated_grade: grade_rounded,
1373 needs_validation: true,
1374 autosave,
1375 latest: Default::default(), highest_grade_latest: Default::default(), subtask_identifier: subtask,
1378 value: data,
1379 date: time::get_time() };
1380
1381 conn.submit_submission(submission);
1382
1383 Ok("{}".to_string())
1384}
1385
1386pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, autosaveinterval: u64)
1387 -> MedalResult<Result<MedalValue, (i32, Option<String>)>> {
1388 let session = conn.get_session_or_new(&session_token).unwrap();
1389
1390 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1391 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;
1395 let mut nexttaskgroup: Option<Taskgroup> = None;
1396 let mut current_found = false;
1397
1398 let mut subtaskstars = Vec::new();
1399
1400 for taskgroup in tasklist.taskgroups {
1401 if current_found {
1402 nexttaskgroup = Some(taskgroup);
1403 break;
1404 }
1405
1406 if taskgroup.id == tg.id {
1407 current_found = true;
1408 subtaskstars = generate_subtaskstars(&taskgroup, &grade, Some(task_id));
1409 } else {
1410 prevtaskgroup = Some(taskgroup);
1411 }
1412 }
1413
1414 match conn.get_own_participation(session.id, contest.id.expect("Value from database")) {
1415 None => Ok(Err((contest.id.unwrap(), contest.category))),
1416 Some(participation) => {
1417 let mut data = json_val::Map::new();
1418 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1419 data.insert("prevtask".to_string(), to_json(&prevtaskgroup.map(|tg| tg.tasks[0].id)));
1420 data.insert("nexttask".to_string(), to_json(&nexttaskgroup.map(|tg| tg.tasks[0].id))); let time_info = check_contest_time_left(&session, &contest, &participation);
1423 data.insert("time_info".to_string(), to_json(&time_info));
1424
1425 data.insert("time_left_mh_formatted".to_string(),
1426 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1427 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1428
1429 let auto_save_interval_ms = if autosaveinterval > 0 && autosaveinterval < 31536000000 {
1430 autosaveinterval * 1000
1431 } else {
1432 31536000000
1433 };
1434 data.insert("auto_save_interval_ms".to_string(), to_json(&auto_save_interval_ms));
1435
1436 if time_info.can_still_compete && participation.team.is_some() && participation.team != Some(session.id) {
1438 return Ok(Err((contest.id.unwrap(), contest.category)));
1439 }
1440
1441 if time_info.can_still_compete || time_info.is_review {
1442 data.insert("contestname".to_string(), to_json(&contest.name));
1443 data.insert("name".to_string(), to_json(&tg.name));
1444 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1445 data.insert("taskid".to_string(), to_json(&task_id));
1446 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1447 data.insert("contestid".to_string(), to_json(&contest.id));
1448 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1449 data.insert("standalone_task".to_string(), to_json(&contest.standalone_task));
1450 data.insert("category".to_string(), to_json(&contest.category));
1451
1452 let (template, tasklocation) = if let Some(language) = t.language {
1453 match language.as_str() {
1454 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1455 "python" => {
1456 data.insert("tasklang".to_string(), to_json(&"python"));
1457 ("wtask".to_owned(), t.location.as_str())
1458 }
1459 _ => ("task".to_owned(), t.location.as_str()),
1460 }
1461 } else {
1462 match t.location.chars().next() {
1463 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1464 Some('P') => {
1465 data.insert("tasklang".to_string(), to_json(&"python"));
1466 ("wtask".to_owned(), &t.location[1..])
1467 }
1468 _ => ("task".to_owned(), t.location.as_str()),
1469 }
1470 };
1471
1472 let taskpath = format!("{}{}", contest.location, &tasklocation);
1473 data.insert("taskpath".to_string(), to_json(&taskpath));
1474
1475 Ok(Ok((template, data)))
1476 } else {
1477 Ok(Err((contest.id.unwrap(), contest.category)))
1479 }
1480 }
1481 }
1482}
1483
1484pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, submission_id: i32)
1485 -> MedalResult<Result<MedalValue, i32>> {
1486 let session = conn.get_session_or_new(&session_token).unwrap();
1487
1488 let (submission, t, tg, contest) =
1489 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1490
1491 let grade = Grade { taskgroup: tg.id.unwrap(),
1494 user: session.id,
1495 grade: Some(submission.grade),
1496 validated: submission.validated };
1497
1498 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1500 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1501 if !group.admins.contains(&session.id) {
1502 return Err(MedalError::AccessDenied);
1504 }
1505 } else {
1506 return Err(MedalError::AccessDenied);
1508 }
1509 }
1510
1511 let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); let mut data = json_val::Map::new();
1514 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1515
1516 let time_info = ContestTimeInfo { passed_secs_total: 0,
1517 left_secs_total: 0,
1518 left_mins_total: 0,
1519 left_hour: 0,
1520 left_min: 0,
1521 left_sec: 0,
1522 has_timelimit: contest.duration != 0,
1523 is_time_left: false,
1524 exempt_from_timelimit: true,
1525 can_still_compete: false,
1526 review_has_timelimit: false,
1527 has_future_review: false,
1528 has_review_end: false,
1529 is_review: true,
1530 can_still_compete_or_review: true,
1531
1532 until_review_start_day: 0,
1533 until_review_start_hour: 0,
1534 until_review_start_min: 0,
1535
1536 until_review_end_day: 0,
1537 until_review_end_hour: 0,
1538 until_review_end_min: 0 };
1539
1540 data.insert("time_info".to_string(), to_json(&time_info));
1541
1542 data.insert("time_left_mh_formatted".to_string(),
1543 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1544 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1545
1546 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1547
1548 data.insert("name".to_string(), to_json(&tg.name));
1550 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1551 data.insert("taskid".to_string(), to_json(&task_id));
1552 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1553 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1555
1556 data.insert("submission".to_string(), to_json(&submission_id));
1557
1558 let (template, tasklocation) = if let Some(language) = t.language {
1559 match language.as_str() {
1560 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1561 "python" => {
1562 data.insert("tasklang".to_string(), to_json(&"python"));
1563 ("wtask".to_owned(), t.location.as_str())
1564 }
1565 _ => ("task".to_owned(), t.location.as_str()),
1566 }
1567 } else {
1568 match t.location.chars().next() {
1569 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1570 Some('P') => {
1571 data.insert("tasklang".to_string(), to_json(&"python"));
1572 ("wtask".to_owned(), &t.location[1..])
1573 }
1574 _ => ("task".to_owned(), t.location.as_str()),
1575 }
1576 };
1577
1578 let taskpath = format!("{}{}", contest.location, &tasklocation);
1579 data.insert("taskpath".to_string(), to_json(&taskpath));
1580
1581 Ok(Ok((template, data)))
1582}
1583
1584pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> {
1585 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1586
1587 if !contest.public
1589 || contest.duration != 0
1590 || !contest.requires_contests.is_empty()
1591 || contest.requires_login == Some(true)
1592 || contest.standalone_task != Some(true)
1593 {
1594 return Err(MedalError::UnknownId);
1595 }
1596
1597 let time_info = ContestTimeInfo { passed_secs_total: 0,
1598 left_secs_total: 0,
1599 left_mins_total: 0,
1600 left_hour: 0,
1601 left_min: 0,
1602 left_sec: 0,
1603 has_timelimit: contest.duration != 0,
1604 is_time_left: false,
1605 exempt_from_timelimit: true,
1606 can_still_compete: false,
1607 review_has_timelimit: false,
1608 has_future_review: false,
1609 has_review_end: false,
1610 is_review: true,
1611 can_still_compete_or_review: true,
1612
1613 until_review_start_day: 0,
1614 until_review_start_hour: 0,
1615 until_review_start_min: 0,
1616
1617 until_review_end_day: 0,
1618 until_review_end_hour: 0,
1619 until_review_end_min: 0 };
1620
1621 let mut data = json_val::Map::new();
1622
1623 data.insert("time_info".to_string(), to_json(&time_info));
1624
1625 data.insert("time_left_mh_formatted".to_string(),
1626 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1627 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1628
1629 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1630
1631 data.insert("contestname".to_string(), to_json(&contest.name));
1632 data.insert("name".to_string(), to_json(&tg.name));
1633 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1634 data.insert("taskid".to_string(), to_json(&task_id));
1635 data.insert("contestid".to_string(), to_json(&contest.id));
1636 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1637 data.insert("preview".to_string(), to_json(&true));
1638
1639 let (template, tasklocation) = if let Some(language) = t.language {
1640 match language.as_str() {
1641 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1642 "python" => {
1643 data.insert("tasklang".to_string(), to_json(&"python"));
1644 ("wtask".to_owned(), t.location.as_str())
1645 }
1646 _ => ("task".to_owned(), t.location.as_str()),
1647 }
1648 } else {
1649 match t.location.chars().next() {
1650 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1651 Some('P') => {
1652 data.insert("tasklang".to_string(), to_json(&"python"));
1653 ("wtask".to_owned(), &t.location[1..])
1654 }
1655 _ => ("task".to_owned(), t.location.as_str()),
1656 }
1657 };
1658
1659 let taskpath = format!("{}{}", contest.location, &tasklocation);
1660 data.insert("taskpath".to_string(), to_json(&taskpath));
1661
1662 Ok(Ok((template, data)))
1663}
1664
1665#[derive(Serialize, Deserialize)]
1666pub struct GroupInfo {
1667 pub id: i32,
1668 pub name: String,
1669 pub tag: String,
1670 pub code: String,
1671}
1672
1673pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1674 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1675
1676 let mut data = json_val::Map::new();
1677 fill_user_data(&session, &mut data);
1678
1679 let v: Vec<GroupInfo> =
1680 conn.get_groups(session.id)
1681 .iter()
1682 .map(|g| GroupInfo { id: g.id.unwrap(),
1683 name: g.name.clone(),
1684 tag: g.tag.clone(),
1685 code: g.groupcode.clone() })
1686 .collect();
1687 data.insert("group".to_string(), to_json(&v));
1688 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1689
1690 Ok(("groups".to_string(), data))
1691}
1692
1693#[derive(Serialize, Deserialize)]
1694pub struct MemberInfo {
1695 pub id: i32,
1696 pub firstname: String,
1697 pub lastname: String,
1698 pub sex: String,
1699 pub grade: String,
1700 pub logincode: String,
1701 pub anonymous: bool,
1702 pub data_protection_clearance: bool,
1703}
1704
1705pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
1706 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1707 let group = conn.get_group_complete(group_id).unwrap(); let mut data = json_val::Map::new();
1710 fill_user_data(&session, &mut data);
1711
1712 if !group.admins.contains(&session.id) {
1713 return Err(MedalError::AccessDenied);
1714 }
1715
1716 let gi = GroupInfo { id: group.id.unwrap(),
1717 name: group.name.clone(),
1718 tag: group.tag.clone(),
1719 code: group.groupcode.clone() };
1720
1721 let v: Vec<MemberInfo> = group.members
1722 .iter()
1723 .filter_map(|m| {
1724 Some(MemberInfo { id: m.id,
1725 firstname: m.firstname.clone()?,
1726 lastname: m.lastname.clone()?,
1727 sex: (match m.sex {
1728 Some(0) | None => "/",
1729 Some(1) => "m",
1730 Some(2) => "w",
1731 Some(3) => "d",
1732 Some(4) => "…",
1733 _ => "?",
1734 }).to_string(),
1735 grade: grade_to_string(m.grade),
1736 logincode: m.logincode.clone()?,
1737 anonymous: m.anonymous,
1738 data_protection_clearance: m.data_protection_clearance == 1 })
1739 })
1740 .collect();
1741
1742 data.insert("group".to_string(), to_json(&gi));
1743 data.insert("member".to_string(), to_json(&v));
1744 data.insert("groupname".to_string(), to_json(&gi.name));
1745
1746 Ok(("group".to_string(), data))
1747}
1748
1749pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
1750 -> MedalResult<i32> {
1751 let session = conn.get_session(&session_token)
1752 .ensure_logged_in()
1753 .ok_or(MedalError::AccessDenied)?
1754 .ensure_teacher_or_admin()
1755 .ok_or(MedalError::AccessDenied)?;
1756
1757 if session.csrf_token != csrf_token {
1758 return Err(MedalError::CsrfCheckFailed);
1759 }
1760
1761 let mut groupcode = String::new();
1762 for i in 0..10 {
1763 if i == 9 {
1764 panic!("ERROR: Too many groupcode collisions! Give up ...");
1765 }
1766 groupcode = helpers::make_groupcode();
1767 if !conn.code_exists(&groupcode) {
1768 break;
1769 }
1770 println!("WARNING: Groupcode collision! Retrying ...");
1771 }
1772
1773 let mut group = Group { id: None, name, groupcode, tag, admins: vec![session.id], members: Vec::new() };
1774
1775 conn.add_group(&mut group);
1776
1777 Ok(group.id.unwrap())
1778}
1779
1780pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
1781 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1782
1783 let mut data = json_val::Map::new();
1784 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1785
1786 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1787 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1788 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1789 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1790
1791 Ok(("groupcsv".to_string(), data))
1792}
1793
1794pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
1796 -> MedalResult<()> {
1797 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1798
1799 if session.csrf_token != csrf_token {
1800 return Err(MedalError::CsrfCheckFailed);
1801 }
1802
1803 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());
1805
1806 let mut groupcode = String::new();
1807 let mut name = String::new();
1808 let mut group = Group { id: None,
1809 name: name.clone(),
1810 groupcode,
1811 tag: String::new(),
1812 admins: vec![session.id],
1813 members: Vec::new() };
1814
1815 for line in v {
1816 if name != line[0] {
1817 if name != "" {
1818 conn.update_or_create_group_with_users(group, session.id);
1819 }
1820 name = line[0].clone();
1821
1822 groupcode = String::new();
1823 for i in 0..10 {
1824 if i == 9 {
1825 panic!("ERROR: Too many groupcode collisions! Give up ...");
1826 }
1827 groupcode = helpers::make_groupcode();
1828 if !conn.code_exists(&groupcode) {
1829 break;
1830 }
1831 println!("WARNING: Groupcode collision! Retrying ...");
1832 }
1833
1834 group = Group { id: None,
1835 name: name.clone(),
1836 groupcode,
1837 tag: name.clone(),
1838 admins: vec![session.id],
1839 members: Vec::new() };
1840 }
1841
1842 let mut user = SessionUser::group_user_stub();
1843 user.grade = line[1].parse::<i32>().unwrap_or(0);
1844 user.firstname = Some(line[2].clone());
1845 user.lastname = Some(line[3].clone());
1846
1847 use db_objects::Sex;
1848 match line[4].as_str() {
1849 "m" => user.sex = Some(Sex::Male as i32),
1850 "f" => user.sex = Some(Sex::Female as i32),
1851 "d" => user.sex = Some(Sex::Diverse as i32),
1852 _ => user.sex = None,
1853 }
1854
1855 user.anonymous = line[5] == "y";
1856
1857 group.members.push(user);
1858 }
1859 conn.update_or_create_group_with_users(group, session.id);
1860
1861 Ok(())
1862}
1863
1864pub fn contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1865 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1866
1867 let mut data = json_val::Map::new();
1868 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1869
1870 Ok(("admin_admissioncsv".to_string(), data))
1871}
1872
1873pub fn upload_contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
1874 contest_id: i32, admission_data: &str)
1875 -> MedalResult<()> {
1876 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1877
1878 if session.csrf_token != csrf_token {
1879 return Err(MedalError::CsrfCheckFailed);
1880 }
1881
1882 let v: Vec<Vec<String>> = serde_json::from_str(admission_data).or(Err(MedalError::AccessDenied))?; let w: Vec<(i32, Option<String>)> = v.into_iter()
1885 .map(|vv| {
1886 (vv[0].parse().unwrap_or(-1),
1887 if vv[1].len() == 0 && vv[2].len() == 0 && vv[3].len() == 0 {
1888 None
1889 } else {
1890 Some(format!("{}\x1f{}\x1f{}", vv[1], vv[2], vv[3]))
1892 })
1893 })
1894 .collect();
1895
1896 let _annotations_inserted = conn.insert_contest_annotations(contest_id, w);
1897
1898 Ok(())
1899}
1900
1901#[allow(dead_code)]
1902pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
1903 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1904 let _g = conn.get_contest_groups_grades(session.id, contest_id);
1906
1907 let data = json_val::Map::new();
1908
1909 Ok(("groupresults".into(), data))
1910}
1911
1912pub struct SexInformation {
1913 pub require_sex: bool,
1914 pub allow_sex_na: bool,
1915 pub allow_sex_diverse: bool,
1916 pub allow_sex_other: bool,
1917}
1918
1919pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
1920 query_string: Option<String>, sex_infos: SexInformation)
1921 -> MedalValueResult {
1922 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1923
1924 let mut data = json_val::Map::new();
1925 fill_user_data(&session, &mut data);
1926
1927 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1928 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1929 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1930 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1931
1932 match user_id {
1933 None => {
1934 data.insert("profile_firstname".to_string(), to_json(&session.firstname));
1935 data.insert("profile_lastname".to_string(), to_json(&session.lastname));
1936 data.insert("profile_street".to_string(), to_json(&session.street));
1937 data.insert("profile_zip".to_string(), to_json(&session.zip));
1938 data.insert("profile_city".to_string(), to_json(&session.city));
1939 data.insert(format!("sel{}", session.grade), to_json(&"selected"));
1940 if let Some(sex) = session.sex {
1941 data.insert(format!("sex_{}", sex), to_json(&"selected"));
1942 } else {
1943 data.insert("sex_None".to_string(), to_json(&"selected"));
1944 }
1945
1946 data.insert("profile_logincode".to_string(), to_json(&session.logincode));
1947 if session.password.is_some() {
1948 data.insert("profile_username".to_string(), to_json(&session.username));
1949 }
1950 if session.managed_by.is_none() {
1951 data.insert("profile_not_in_group".into(), to_json(&true));
1952 }
1953 if session.oauth_provider != Some("pms".to_string()) {
1954 data.insert("profile_not_pms".into(), to_json(&true));
1955 }
1958 data.insert("ownprofile".into(), to_json(&true));
1959 data.insert("userid".into(), to_json(&session.id));
1960
1961 let webauthn = conn.get_webauthn_passkey_names_for_user(session.id);
1962 data.insert("webauthn".into(), to_json(&webauthn));
1963 data.insert("has_webauthn".into(), to_json(&!webauthn.is_empty()));
1964
1965 if let Some(query) = query_string {
1966 if let Some(status) = query.strip_prefix("status=") {
1967 if ["NothingChanged",
1968 "DataChanged",
1969 "PasswordChanged",
1970 "PasswordMissmatch",
1971 "firstlogin",
1972 "SignedUp"].contains(&status)
1973 {
1974 data.insert((status).to_string(), to_json(&true));
1975 }
1976 }
1977 }
1978
1979 let now = time::get_time();
1980
1981 let all_participations = conn.get_all_participations_complete(session.id);
1982
1983 let mut contest_stars: Option<std::collections::BTreeMap<i32, i32>> = None;
1984
1985 let stickers: Vec<String> =
1986 all_participations.iter()
1987 .filter_map(|(_, contest)| {
1988 if contest.stickers.len() == 0 {
1989 return None;
1990 }
1991 if contest.stickers.len() == 1 && contest.stickers[0].1 == 0 {
1992 return Some(format!("{}/{}", contest.location, contest.stickers[0].0));
1993 }
1994
1995 if contest_stars.is_none() {
1997 contest_stars =
1998 Some(conn.count_all_stars_by_contest(session.id).into_iter().collect());
1999 }
2000 let stars: i32 =
2001 *contest_stars.as_ref().unwrap().get(&contest.id.unwrap()).unwrap_or(&0);
2002
2003 let mut value: Option<String> = None;
2005 let mut min_diff: i32 = -1;
2006
2007 for sticker in &contest.stickers {
2008 let diff = stars - sticker.1;
2009 if diff >= 0 && (min_diff < 0 || diff < min_diff) {
2010 value = Some(format!("{}{}", contest.location, sticker.0));
2011 min_diff = diff;
2012 }
2013 }
2014 value
2015 })
2016 .collect();
2017 data.insert("stickers".into(), to_json(&stickers));
2018
2019 let participations: (Vec<(i32, String, bool, bool, bool, Option<String>)>,
2021 Vec<(i32, String, bool, bool, bool, Option<String>)>) =
2022 all_participations.into_iter()
2023 .rev()
2024 .map(|(participation, contest)| {
2025 let passed_secs = now.sec - participation.start.sec;
2026 let left_secs = i64::from(contest.duration) * 60 - passed_secs;
2027 let is_time_left = contest.duration == 0 || left_secs >= 0;
2028 let has_timelimit = contest.duration != 0;
2029 let requires_login = contest.requires_login == Some(true);
2030 let annotation = participation.annotation.map(|annotation| {
2031 annotation.split('\x1f')
2032 .nth(2)
2033 .unwrap_or("")
2034 .to_string()
2035 });
2036
2037 (contest.id.unwrap(),
2038 contest.name,
2039 has_timelimit,
2040 is_time_left,
2041 requires_login,
2042 annotation)
2043 })
2044 .partition(|contest| contest.2 && !contest.4);
2045 data.insert("participations".into(), to_json(&participations));
2046
2047 let stars_count = conn.count_all_stars(session.id);
2048 data.insert("stars_count".into(), to_json(&stars_count));
2049 let stars_message = match stars_count {
2050 0 => "Auf gehts, dein erster Stern wartet auf dich!",
2051 1..=9 => "Ein hervorragender Anfang!",
2052 10..=99 => "Das ist ziemlich gut!",
2053 100..=999 => "Ein wahrer Meister!",
2054 _ => "Wow! Einfach wow!",
2055 }.to_string();
2056
2057 data.insert("stars_message".into(), to_json(&stars_message));
2058 }
2059 Some(user_id) => {
2061 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2063 let group = opt_group.ok_or(MedalError::AccessDenied)?;
2064 if !group.admins.contains(&session.id) {
2065 return Err(MedalError::AccessDenied);
2066 }
2067
2068 data.insert("profile_firstname".to_string(), to_json(&user.firstname));
2069 data.insert("profile_lastname".to_string(), to_json(&user.lastname));
2070 data.insert("profile_street".to_string(), to_json(&session.street));
2071 data.insert("profile_zip".to_string(), to_json(&session.zip));
2072 data.insert("profile_city".to_string(), to_json(&session.city));
2073 data.insert(format!("sel{}", user.grade), to_json(&"selected"));
2074 if let Some(sex) = user.sex {
2075 data.insert(format!("sex_{}", sex), to_json(&"selected"));
2076 } else {
2077 data.insert("sex_None".to_string(), to_json(&"selected"));
2078 }
2079
2080 data.insert("profile_logincode".to_string(), to_json(&user.logincode));
2081 if user.username.is_some() {
2082 data.insert("profile_username".to_string(), to_json(&user.username));
2083 }
2084 if user.managed_by.is_none() {
2085 data.insert("profile_not_in_group".into(), to_json(&true));
2086 }
2087 if session.oauth_provider != Some("pms".to_string()) {
2088 data.insert("profile_not_pms".into(), to_json(&true));
2089 }
2090 data.insert("ownprofile".into(), to_json(&false));
2091
2092 if let Some(query) = query_string {
2093 if let Some(status) = query.strip_prefix("status=") {
2094 if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
2095 data.insert((status).to_string(), to_json(&true));
2096 }
2097 }
2098 }
2099 }
2100 }
2101
2102 Ok(("profile".to_string(), data))
2103}
2104
2105#[derive(Debug, PartialEq, Eq)]
2106pub enum ProfileStatus {
2107 NothingChanged,
2108 DataChanged,
2109 PasswordChanged,
2110 PasswordMissmatch,
2111}
2112impl From<ProfileStatus> for String {
2113 fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
2114}
2115
2116pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
2117 (firstname,
2118 lastname,
2119 street,
2120 zip,
2121 city,
2122 password,
2123 password_repeat,
2124 grade,
2125 sex): (String,
2126 String,
2127 Option<String>,
2128 Option<String>,
2129 Option<String>,
2130 Option<String>,
2131 Option<String>,
2132 i32,
2133 Option<i32>))
2134 -> MedalResult<ProfileStatus> {
2135 let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2136
2137 if session.csrf_token != csrf_token {
2138 return Err(MedalError::AccessDenied); }
2140
2141 let mut result = ProfileStatus::NothingChanged;
2142
2143 let mut password_and_salt = None;
2144
2145 if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
2146 if password != "" || password_repeat != "" {
2147 if password == password_repeat {
2148 let salt = helpers::make_salt();
2149 let hash = helpers::hash_password(&password, &salt)?;
2150
2151 password_and_salt = Some((hash, salt));
2152 result = ProfileStatus::PasswordChanged;
2153 } else {
2154 result = ProfileStatus::PasswordMissmatch;
2155 }
2156 }
2157 }
2158
2159 if result == ProfileStatus::NothingChanged {
2160 if session.firstname.as_ref() == Some(&firstname)
2161 && session.lastname.as_ref() == Some(&lastname)
2162 && session.street == street
2163 && session.zip == zip
2164 && session.city == city
2165 && session.grade == grade
2166 && session.sex == sex
2167 {
2168 return Ok(ProfileStatus::NothingChanged);
2169 } else {
2170 result = ProfileStatus::DataChanged;
2171 }
2172 }
2173
2174 match user_id {
2175 None => {
2176 session.firstname = Some(firstname);
2177 session.lastname = Some(lastname);
2178 session.grade = grade;
2179 session.sex = sex;
2180
2181 if street.is_some() {
2182 session.street = street;
2183 }
2184 if zip.is_some() {
2185 session.zip = zip;
2186 }
2187 if city.is_some() {
2188 session.city = city;
2189 }
2190
2191 if let Some((password, salt)) = password_and_salt {
2192 session.password = Some(password);
2193 session.salt = Some(salt);
2194 }
2195
2196 conn.save_session(session);
2197 }
2198 Some(user_id) => {
2199 let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2201 let group = opt_group.ok_or(MedalError::AccessDenied)?;
2202 if !group.admins.contains(&session.id) {
2203 return Err(MedalError::AccessDenied);
2204 }
2205
2206 user.firstname = Some(firstname);
2207 user.lastname = Some(lastname);
2208 user.grade = grade;
2209 user.sex = sex;
2210
2211 if street.is_some() {
2212 user.street = street;
2213 }
2214 if zip.is_some() {
2215 user.zip = zip;
2216 }
2217 if city.is_some() {
2218 user.city = city;
2219 }
2220
2221 if let Some((password, salt)) = password_and_salt {
2222 user.password = Some(password);
2223 user.salt = Some(salt);
2224 }
2225
2226 conn.save_session(user);
2227 }
2228 }
2229
2230 Ok(result)
2231}
2232
2233pub fn check_profile<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
2234 (firstname, lastname): (String, String))
2235 -> JsonValueResult {
2236 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2237
2238 if session.csrf_token != csrf_token {
2239 return Err(MedalError::AccessDenied);
2240 }
2241
2242 let mut data = json_val::Map::new();
2243
2244 if session.firstname.map(|n| n.len() > 0).unwrap_or(false) && session.lastname.map(|n| n.len() > 0).unwrap_or(false)
2246 {
2247 data.insert("exists".to_string(), to_json(&false));
2248 return Ok(data);
2249 }
2250
2251 let group =
2252 conn.get_group_complete(session.managed_by.ok_or(MedalError::AccessDenied)?).ok_or(MedalError::AccessDenied)?;
2253
2254 for member in group.members {
2255 if member.lastname.map(|l| l.to_lowercase()) == Some(lastname.to_lowercase())
2256 && member.firstname.map(|l| l.to_lowercase()) == Some(firstname.to_lowercase())
2257 && member.id != session.id
2258 {
2259 data.insert("exists".to_string(), to_json(&true));
2260 return Ok(data);
2261 }
2262 }
2263
2264 data.insert("exists".to_string(), to_json(&false));
2265 Ok(data)
2266}
2267
2268pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2269 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2270 if !session.is_teacher {
2271 return Err(MedalError::AccessDenied);
2272 }
2273
2274 let mut data = json_val::Map::new();
2275 fill_user_data(&session, &mut data);
2276
2277 Ok(("teacher".to_string(), data))
2278}
2279
2280pub fn admin_index<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2281 let session = conn.get_session(&session_token)
2282 .ensure_logged_in()
2283 .ok_or(MedalError::NotLoggedIn)?
2284 .ensure_admin()
2285 .ok_or(MedalError::AccessDenied)?;
2286
2287 let mut data = json_val::Map::new();
2288 fill_user_data(&session, &mut data);
2289
2290 Ok(("admin".to_string(), data))
2291}
2292
2293pub fn admin_search_users<T: MedalConnection>(conn: &T, session_token: &str,
2294 s_data: (Option<i32>,
2295 Option<String>,
2296 Option<String>,
2297 Option<String>,
2298 Option<String>,
2299 Option<String>))
2300 -> MedalValueResult {
2301 let session = conn.get_session(&session_token)
2302 .ensure_logged_in()
2303 .ok_or(MedalError::NotLoggedIn)?
2304 .ensure_admin()
2305 .ok_or(MedalError::AccessDenied)?;
2306
2307 let mut data = json_val::Map::new();
2308 fill_user_data(&session, &mut data);
2309
2310 match conn.get_search_users(s_data) {
2311 Ok(users) => {
2312 data.insert("users".to_string(), to_json(&users));
2313 data.insert("max_results".to_string(), to_json(&200));
2314 data.insert("num_results".to_string(), to_json(&users.len()));
2315 data.insert("no_results".to_string(), to_json(&(users.len() == 0)));
2316 if users.len() > 200 {
2317 data.insert("more_users".to_string(), to_json(&true));
2318 data.insert("more_results".to_string(), to_json(&true));
2319 }
2320 }
2321 Err(groups) => {
2322 data.insert("groups".to_string(), to_json(&groups));
2323 data.insert("max_results".to_string(), to_json(&200));
2324 data.insert("num_results".to_string(), to_json(&groups.len()));
2325 data.insert("no_results".to_string(), to_json(&(groups.len() == 0)));
2326 if groups.len() > 200 {
2327 data.insert("more_groups".to_string(), to_json(&true));
2328 data.insert("more_results".to_string(), to_json(&true));
2329 }
2330 }
2331 };
2332
2333 Ok(("admin_search_results".to_string(), data))
2334}
2335
2336pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str) -> MedalValueResult {
2337 let session = conn.get_session(&session_token)
2338 .ensure_logged_in()
2339 .ok_or(MedalError::NotLoggedIn)?
2340 .ensure_teacher_or_admin()
2341 .ok_or(MedalError::AccessDenied)?;
2342
2343 let mut data = json_val::Map::new();
2344
2345 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2346
2347 if !session.is_admin() {
2348 if let Some(group) = opt_group.clone() {
2350 if !group.admins.contains(&session.id) {
2351 return Err(MedalError::AccessDenied);
2352 }
2353 } else if user_id != session.id {
2354 return Err(MedalError::AccessDenied);
2355 }
2356 }
2357
2358 fill_user_data(&session, &mut data);
2359 fill_user_data_prefix(&user, &mut data, "user_");
2360 data.insert("user_logincode".to_string(), to_json(&user.logincode));
2361 data.insert("user_id".to_string(), to_json(&user.id));
2362 let grade = if user.grade >= 200 {
2363 "Kein Schüler mehr".to_string()
2364 } else if user.grade >= 11 {
2365 format!("{} ({})", user.grade % 100, if user.grade >= 100 { "G9" } else { "G8" })
2366 } else {
2367 format!("{}", user.grade)
2368 };
2369 data.insert("user_grade".to_string(), to_json(&grade));
2370 data.insert("user_oauthid".to_string(), to_json(&user.oauth_foreign_id));
2371 data.insert("user_oauthprovider".to_string(), to_json(&user.oauth_provider));
2372
2373 if let Some(group) = opt_group {
2374 data.insert("user_group_id".to_string(), to_json(&group.id));
2375 data.insert("user_group_name".to_string(), to_json(&group.name));
2376 }
2377
2378 if let Some(additional_time) = user.additional_contest_time {
2379 data.insert("additional_time".to_string(), to_json(&true));
2380 data.insert(format!("additional{}", additional_time), to_json(&true));
2381 }
2382
2383 let groups: Vec<GroupInfo> =
2384 conn.get_groups(user_id)
2385 .iter()
2386 .map(|g| GroupInfo { id: g.id.unwrap(),
2387 name: g.name.clone(),
2388 tag: g.tag.clone(),
2389 code: g.groupcode.clone() })
2390 .collect();
2391 data.insert("user_group".to_string(), to_json(&groups));
2392
2393 let parts = conn.get_all_participations_complete(user_id);
2394 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2395 let contest_stars = conn.count_all_stars_by_contest(user_id);
2396
2397 let pi: Vec<(i32, String, String, i32)> =
2398 parts.into_iter()
2399 .map(|(p, c)| {
2400 (c.id.unwrap(),
2401 format!("{}{}{}",
2402 &c.name,
2403 if c.protected { " (geschützt)" } else { "" },
2404 if p.team.is_some() { " (Teamteilnahme)" } else { "" }),
2405 self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(p.start)).unwrap(),
2406 contest_stars.iter()
2407 .filter_map(|(id, stars)| if *id == c.id.unwrap() { Some(*stars) } else { None })
2408 .next()
2409 .unwrap_or(0))
2410 })
2411 .collect();
2412
2413 data.insert("user_participations".to_string(), to_json(&pi));
2414 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2415 data.insert("can_delete".to_string(),
2416 to_json(&((!has_protected_participations || session.is_admin()) && groups.len() == 0)));
2417
2418 Ok(("admin_user".to_string(), data))
2419}
2420
2421pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str, csrf_token: &str)
2422 -> JsonValueResult {
2423 let session = conn.get_session(&session_token)
2424 .ensure_logged_in()
2425 .ok_or(MedalError::NotLoggedIn)?
2426 .ensure_teacher_or_admin()
2427 .ok_or(MedalError::AccessDenied)?;
2428
2429 if session.csrf_token != csrf_token {
2430 return Err(MedalError::CsrfCheckFailed);
2431 }
2432
2433 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2434
2435 if !session.is_admin() {
2436 if let Some(group) = opt_group {
2438 if !group.admins.contains(&session.id) {
2439 return Err(MedalError::AccessDenied);
2440 }
2441 } else {
2442 return Err(MedalError::AccessDenied);
2443 }
2444 }
2445
2446 let parts = conn.get_all_participations_complete(user_id);
2447 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2448 let groups = conn.get_groups(user_id);
2449
2450 let mut data = json_val::Map::new();
2451 if has_protected_participations && !session.is_admin() {
2452 data.insert("reason".to_string(), to_json(&"Benutzer hat Teilnahmen an geschützten Wettbewerben."));
2453 Err(MedalError::ErrorWithJson(data))
2454 } else if groups.len() > 0 {
2455 data.insert("reason".to_string(), to_json(&"Benutzer ist Administrator von Gruppen."));
2456 Err(MedalError::ErrorWithJson(data))
2457 } else {
2458 conn.delete_user(user_id);
2459 Ok(data)
2460 }
2461}
2462
2463pub fn admin_move_user_to_group<T: MedalConnection>(conn: &T, user_id: i32, group_id: i32, session_token: &str,
2464 csrf_token: &str)
2465 -> JsonValueResult {
2466 let session = conn.get_session(&session_token)
2467 .ensure_logged_in()
2468 .ok_or(MedalError::NotLoggedIn)?
2469 .ensure_admin()
2470 .ok_or(MedalError::AccessDenied)?;
2471
2472 if session.csrf_token != csrf_token {
2473 return Err(MedalError::CsrfCheckFailed);
2474 }
2475
2476 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2477
2478 if !session.is_admin() {
2479 if let Some(group) = opt_group {
2481 if !group.admins.contains(&session.id) {
2482 return Err(MedalError::AccessDenied);
2483 }
2484 } else {
2485 return Err(MedalError::AccessDenied);
2486 }
2487 }
2488
2489 let mut data = json_val::Map::new();
2490 if conn.get_group(group_id).is_some() {
2491 if let Some(mut user) = conn.get_user_by_id(user_id) {
2492 user.managed_by = Some(group_id);
2493 conn.save_session(user);
2494 Ok(data)
2495 } else {
2496 data.insert("reason".to_string(), to_json(&"Benutzer existiert nicht."));
2497 Err(MedalError::ErrorWithJson(data))
2498 }
2499 } else {
2500 data.insert("reason".to_string(), to_json(&"Gruppe existiert nicht."));
2501 Err(MedalError::ErrorWithJson(data))
2502 }
2503}
2504
2505pub fn admin_set_user_additional_contest_time<T: MedalConnection>(conn: &T, user_id: i32,
2506 additional_contest_time: i32, session_token: &str,
2507 csrf_token: &str)
2508 -> JsonValueResult {
2509 let session = conn.get_session(&session_token)
2510 .ensure_logged_in()
2511 .ok_or(MedalError::NotLoggedIn)?
2512 .ensure_teacher_or_admin()
2513 .ok_or(MedalError::AccessDenied)?;
2514
2515 if session.csrf_token != csrf_token {
2516 return Err(MedalError::CsrfCheckFailed);
2517 }
2518
2519 let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2520
2521 if !session.is_admin() {
2522 if let Some(group) = opt_group {
2524 if !group.admins.contains(&session.id) {
2525 return Err(MedalError::AccessDenied);
2526 }
2527 } else {
2528 return Err(MedalError::AccessDenied);
2529 }
2530 }
2531
2532 let mut data = json_val::Map::new();
2533
2534 if additional_contest_time < 0 || additional_contest_time > 50 {
2535 data.insert("reason".to_string(), to_json(&"Ungültige Angabe."));
2536 return Err(MedalError::ErrorWithJson(data));
2537 }
2538
2539 user.additional_contest_time = if additional_contest_time == 0 { None } else { Some(additional_contest_time) };
2540 conn.save_session(user);
2541
2542 Ok(data)
2543}
2544
2545#[derive(Serialize, Deserialize)]
2546pub struct AdminInfo {
2547 pub id: i32,
2548 pub firstname: String,
2549 pub lastname: String,
2550}
2551
2552pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2553 let session = conn.get_session(&session_token)
2554 .ensure_logged_in()
2555 .ok_or(MedalError::NotLoggedIn)?
2556 .ensure_teacher_or_admin()
2557 .ok_or(MedalError::AccessDenied)?;
2558
2559 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2562 if !group.admins.contains(&session.id) {
2564 return Err(MedalError::AccessDenied);
2565 }
2566 }
2567
2568 let mut data = json_val::Map::new();
2569 fill_user_data(&session, &mut data);
2570
2571 let gi = GroupInfo { id: group.id.unwrap(),
2572 name: group.name.clone(),
2573 tag: group.tag.clone(),
2574 code: group.groupcode.clone() };
2575
2576 let v: Vec<MemberInfo> =
2577 group.members
2578 .iter()
2579 .filter(|m| session.is_admin() || m.firstname.is_some() || m.lastname.is_some())
2580 .map(|m| MemberInfo { id: m.id,
2581 firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
2582 lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
2583 sex: (match m.sex {
2584 Some(0) | None => "/",
2585 Some(1) => "m",
2586 Some(2) => "w",
2587 Some(3) => "d",
2588 Some(4) => "…",
2589 _ => "?",
2590 }).to_string(),
2591 grade: grade_to_string(m.grade),
2592 logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()),
2593 anonymous: m.anonymous,
2594 data_protection_clearance: m.data_protection_clearance == 1 })
2595 .collect();
2596
2597 let data_protection_clearance_all = group.members.iter().all(|m| m.data_protection_clearance == 1);
2598 let has_anonmous_members = group.members.iter().any(|m| m.anonymous);
2599
2600 let has_protected_participations = conn.group_has_protected_participations(group_id);
2601
2602 data.insert("group".to_string(), to_json(&gi));
2603 data.insert("member".to_string(), to_json(&v));
2604 data.insert("groupname".to_string(), to_json(&gi.name));
2605 data.insert("has_anonymous_members".to_string(), to_json(&has_anonmous_members));
2606 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2607 data.insert("can_delete".to_string(), to_json(&(!has_protected_participations || session.is_admin())));
2608 data.insert("data_protection_clearance_all".to_string(), to_json(&data_protection_clearance_all));
2609
2610 let admins: Vec<AdminInfo> =
2611 group.admins
2612 .iter()
2613 .map(|a| {
2614 let admin = conn.get_user_by_id(*a).ok_or(MedalError::AccessDenied)?;
2615 Ok(AdminInfo { id: admin.id,
2616 firstname: admin.firstname.clone().unwrap_or_else(|| "".to_string()),
2617 lastname: admin.lastname.clone().unwrap_or_else(|| "".to_string()) })
2618 })
2619 .collect::<Result<Vec<_>, _>>()?;
2620
2621 data.insert("group_admin".to_string(), to_json(&admins));
2622 data.insert("morethanoneadmin".to_string(), to_json(&(admins.len() > 1)));
2623
2624 Ok(("admin_group".to_string(), data))
2625}
2626
2627pub fn group_add_admin<T: MedalConnection>(conn: &T, group_id: i32, new_admin_id: i32, session_token: &str,
2628 csrf_token: &str)
2629 -> JsonValueResult {
2630 let session = conn.get_session(&session_token)
2631 .ensure_logged_in()
2632 .ok_or(MedalError::NotLoggedIn)?
2633 .ensure_teacher_or_admin()
2634 .ok_or(MedalError::AccessDenied)?;
2635
2636 if session.csrf_token != csrf_token {
2637 return Err(MedalError::CsrfCheckFailed);
2638 }
2639
2640 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2643 if !group.admins.contains(&session.id) {
2645 return Err(MedalError::AccessDenied);
2646 }
2647 }
2648
2649 if group.admins.contains(&new_admin_id) {
2650 let mut data = json_val::Map::new();
2651 data.insert("reason".to_string(), to_json(&"Benutzer ist bereits Admin."));
2652 return Err(MedalError::ErrorWithJson(data));
2653 }
2654
2655 let new_admin = conn.get_user_by_id(new_admin_id).ok_or(MedalError::NotFound)?;
2656 let first_admin_id = group.admins.first().ok_or(MedalError::NotFound)?;
2657 let first_admin = conn.get_user_by_id(*first_admin_id).ok_or(MedalError::NotFound)?;
2658 if new_admin.oauth_provider == first_admin.oauth_provider {
2659 if let Some((_, new_admin_school)) = new_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2660 {
2661 if let Some((_, first_admin_school)) =
2662 first_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2663 {
2664 if new_admin_school == first_admin_school && new_admin_school.len() >= 1 {
2665 conn.add_admin_to_group(&mut group, new_admin_id);
2666
2667 let data = json_val::Map::new();
2668 return Ok(data);
2669 }
2670 }
2671 }
2672 }
2673
2674 let mut data = json_val::Map::new();
2675 data.insert("reason".to_string(),
2676 to_json(&"Benutzer gehört nicht zur gleichen Schule oder Benutzer nicht als Lehrkraft angemeldet."));
2677 Err(MedalError::ErrorWithJson(data))
2678}
2679
2680pub fn group_delete_admin<T: MedalConnection>(conn: &T, group_id: i32, admin_id: i32, session_token: &str,
2681 csrf_token: &str)
2682 -> JsonValueResult {
2683 let session = conn.get_session(&session_token)
2684 .ensure_logged_in()
2685 .ok_or(MedalError::NotLoggedIn)?
2686 .ensure_teacher_or_admin()
2687 .ok_or(MedalError::AccessDenied)?;
2688
2689 if session.csrf_token != csrf_token {
2690 return Err(MedalError::CsrfCheckFailed);
2691 }
2692
2693 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2696 if !group.admins.contains(&session.id) {
2698 return Err(MedalError::AccessDenied);
2699 }
2700 }
2701
2702 if group.admins.len() == 1 {
2703 let mut data = json_val::Map::new();
2704 data.insert("reason".to_string(), to_json(&"Kann letzten Admin nicht entfernen."));
2705 return Err(MedalError::ErrorWithJson(data));
2706 }
2707
2708 if !group.admins.contains(&admin_id) {
2709 let mut data = json_val::Map::new();
2710 data.insert("reason".to_string(), to_json(&"Benutzer ist kein Admin."));
2711 return Err(MedalError::ErrorWithJson(data));
2712 }
2713
2714 conn.remove_admin_from_group(&mut group, admin_id);
2715 let data = json_val::Map::new();
2716 Ok(data)
2717}
2718
2719pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str)
2720 -> JsonValueResult {
2721 let session = conn.get_session(&session_token)
2722 .ensure_logged_in()
2723 .ok_or(MedalError::NotLoggedIn)?
2724 .ensure_teacher_or_admin()
2725 .ok_or(MedalError::AccessDenied)?;
2726
2727 if session.csrf_token != csrf_token {
2728 return Err(MedalError::CsrfCheckFailed);
2729 }
2730
2731 let group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2734 if !group.admins.contains(&session.id) {
2736 return Err(MedalError::AccessDenied);
2737 }
2738 }
2739
2740 let mut data = json_val::Map::new();
2741 if conn.group_has_protected_participations(group_id) && !session.is_admin() {
2742 data.insert("reason".to_string(), to_json(&"Gruppe hat Mitglieder mit geschützten Teilnahmen."));
2743 Err(MedalError::ErrorWithJson(data))
2744 } else {
2745 conn.delete_all_users_for_group(group_id);
2746 conn.delete_group(group_id);
2747 Ok(data)
2748 }
2749}
2750
2751pub fn admin_set_data_clearance<T: MedalConnection>(conn: &T, group_id: i32, clearance_state: bool,
2752 user_id: Option<i32>, session_token: &str, csrf_token: &str)
2753 -> JsonValueResult {
2754 let session = conn.get_session(&session_token)
2755 .ensure_logged_in()
2756 .ok_or(MedalError::NotLoggedIn)?
2757 .ensure_teacher_or_admin()
2758 .ok_or(MedalError::AccessDenied)?;
2759
2760 if session.csrf_token != csrf_token {
2761 return Err(MedalError::CsrfCheckFailed);
2762 }
2763
2764 let group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2767 if !group.admins.contains(&session.id) {
2769 return Err(MedalError::AccessDenied);
2770 }
2771 }
2772
2773 if let Some(user_id) = user_id {
2774 let user = conn.get_user_by_id(user_id);
2775 if let Some(user) = user {
2776 if user.managed_by != Some(group_id) {
2777 return Err(MedalError::AccessDenied);
2778 }
2779 } else {
2780 return Err(MedalError::AccessDenied);
2781 }
2782 }
2783
2784 if let Some(user_id) = user_id {
2785 conn.set_data_clearance_for_user(user_id, clearance_state);
2786 } else {
2787 conn.set_data_clearance_for_group(group_id, clearance_state);
2788 }
2789
2790 let data = json_val::Map::new();
2791 Ok(data)
2792}
2793
2794pub fn admin_show_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2795 let session = conn.get_session(&session_token)
2796 .ensure_logged_in()
2797 .ok_or(MedalError::NotLoggedIn)?
2798 .ensure_teacher_or_admin()
2799 .ok_or(MedalError::AccessDenied)?;
2800
2801 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2804 if !group.admins.contains(&session.id) {
2806 return Err(MedalError::AccessDenied);
2807 }
2808 }
2809
2810 let mut data = json_val::Map::new();
2811 fill_user_data(&session, &mut data);
2812
2813 let gi = GroupInfo { id: group.id.unwrap(),
2814 name: group.name.clone(),
2815 tag: group.tag.clone(),
2816 code: group.groupcode.clone() };
2817
2818 data.insert("group".to_string(), to_json(&gi));
2819 data.insert("groupname".to_string(), to_json(&gi.name));
2820 data.insert("grouptag".to_string(), to_json(&gi.name));
2821
2822 Ok(("admin_edit_group".to_string(), data))
2823}
2824
2825pub fn admin_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str,
2826 name: String, tag: String)
2827 -> MedalResult<()> {
2828 let session = conn.get_session(&session_token)
2829 .ensure_logged_in()
2830 .ok_or(MedalError::NotLoggedIn)?
2831 .ensure_teacher_or_admin()
2832 .ok_or(MedalError::AccessDenied)?;
2833
2834 if session.csrf_token != csrf_token {
2835 return Err(MedalError::CsrfCheckFailed);
2836 }
2837
2838 let mut group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2841 if !group.admins.contains(&session.id) {
2843 return Err(MedalError::AccessDenied);
2844 }
2845 }
2846
2847 group.name = name;
2848 group.tag = tag;
2849
2850 conn.save_group(&mut group);
2851
2852 Ok(())
2853}
2854
2855#[derive(Serialize, Deserialize, Debug)]
2856struct SubmissionResult {
2857 id: i32,
2858 grade: i32,
2859 date: String,
2860}
2861#[derive(Serialize, Deserialize, Debug)]
2862struct TaskResult {
2863 id: i32,
2864 stars: i32,
2865 submissions: Vec<SubmissionResult>,
2866}
2867#[derive(Serialize, Deserialize, Debug)]
2868struct TaskgroupResult {
2869 id: i32,
2870 name: String,
2871 tasks: Vec<TaskResult>,
2872}
2873
2874pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
2875 -> MedalValueResult {
2876 let session = conn.get_session(&session_token)
2877 .ensure_logged_in()
2878 .ok_or(MedalError::NotLoggedIn)?
2879 .ensure_teacher_or_admin()
2880 .ok_or(MedalError::AccessDenied)?;
2881
2882 let user = conn.get_user_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2883 let participation = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2884
2885 let user_or_team = if let Some(team) = participation.team { team } else { user_id };
2886
2887 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2888
2889 if !session.is_admin() {
2890 if let Some(ref group) = opt_group {
2892 if !group.admins.contains(&session.id) {
2893 return Err(MedalError::AccessDenied);
2894 }
2895 } else {
2896 return Err(MedalError::AccessDenied);
2897 }
2898 }
2899
2900 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2901 let grades = conn.get_contest_user_grades(user_or_team, contest_id);
2902
2903 let mut totalgrade = 0;
2904 let mut max_totalgrade = 0;
2905
2906 for taskgroup in &contest.taskgroups {
2907 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
2908 }
2909 for grade in grades {
2910 totalgrade += grade.grade.unwrap_or(0);
2911 }
2912
2913 #[rustfmt::skip]
2914 let subms: Vec<TaskgroupResult> =
2915 contest.taskgroups
2916 .into_iter()
2917 .map(|tg| TaskgroupResult {
2918 id: tg.id.unwrap(),
2919 name: tg.name,
2920 tasks: tg.tasks
2921 .into_iter()
2922 .map(|t| TaskResult {
2923 id: t.id.unwrap(),
2924 stars: t.stars,
2925 submissions: conn.get_all_submissions(user_or_team, t.id.unwrap(), None)
2926 .into_iter()
2927 .map(|s| SubmissionResult {
2928 id: s.id.unwrap(),
2929 grade: s.grade,
2930 date: self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(s.date)).unwrap(),
2931 })
2932 .collect(),
2933 })
2934 .collect(),
2935 })
2936 .collect();
2937
2938 let mut data = json_val::Map::new();
2939
2940 data.insert("submissions".to_string(), to_json(&subms));
2941 data.insert("contestid".to_string(), to_json(&contest.id));
2942 data.insert("contestname".to_string(), to_json(&contest.name));
2943 data.insert("has_timelimit".to_string(), to_json(&(contest.duration > 0)));
2944
2945 data.insert("total_grade".to_string(), to_json(&totalgrade));
2946 data.insert("max_total_grade".to_string(), to_json(&max_totalgrade));
2947
2948 if let Some(group) = opt_group {
2949 data.insert("group_id".to_string(), to_json(&group.id));
2950 data.insert("group_name".to_string(), to_json(&group.name));
2951 }
2952
2953 fill_user_data(&session, &mut data);
2954 fill_user_data_prefix(&user, &mut data, "user_");
2955 data.insert("user_id".to_string(), to_json(&user.id));
2956
2957 data.insert("start_date".to_string(),
2958 to_json(&self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(participation.start)).unwrap()));
2959
2960 data.insert("can_delete".to_string(), to_json(&(!contest.protected || session.is_admin.unwrap_or(false))));
2961 Ok(("admin_participation".to_string(), data))
2962}
2963
2964pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str,
2965 csrf_token: &str)
2966 -> JsonValueResult {
2967 let session = conn.get_session(&session_token)
2968 .ensure_logged_in()
2969 .ok_or(MedalError::NotLoggedIn)?
2970 .ensure_teacher_or_admin()
2971 .ok_or(MedalError::AccessDenied)?;
2972
2973 if session.csrf_token != csrf_token {
2974 return Err(MedalError::CsrfCheckFailed);
2975 }
2976
2977 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2978 let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2979 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2980
2981 if !session.is_admin() {
2982 if contest.protected {
2984 return Err(MedalError::AccessDenied);
2985 }
2986
2987 if let Some(group) = opt_group {
2988 if !group.admins.contains(&session.id) {
2989 return Err(MedalError::AccessDenied);
2990 }
2991 } else {
2992 return Err(MedalError::AccessDenied);
2993 }
2994 }
2995
2996 let mut data = json_val::Map::new();
2997 fill_user_data(&session, &mut data);
2998
2999 conn.delete_participation(user_id, contest_id);
3000 Ok(data)
3001}
3002
3003pub fn admin_show_contests<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
3004 let session = conn.get_session(&session_token)
3005 .ensure_logged_in()
3006 .ok_or(MedalError::NotLoggedIn)?
3007 .ensure_admin()
3008 .ok_or(MedalError::AccessDenied)?;
3009
3010 let mut data = json_val::Map::new();
3011 fill_user_data(&session, &mut data);
3012
3013 let mut contests: Vec<_> = conn.get_contest_list().into_iter().map(|contest| (contest.id, contest.name)).collect();
3014 contests.sort(); contests.reverse();
3016
3017 data.insert("contests".to_string(), to_json(&contests));
3018
3019 Ok(("admin_contests".to_string(), data))
3020}
3021
3022pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalResult<String> {
3023 conn.get_session(&session_token)
3024 .ensure_logged_in()
3025 .ok_or(MedalError::NotLoggedIn)?
3026 .ensure_admin()
3027 .ok_or(MedalError::AccessDenied)?;
3028
3029 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
3030
3031 let taskgroup_ids: Vec<(i32, String)> =
3032 contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect();
3033 let filename = format!("contest_{}__{}__{}.csv",
3034 contest_id,
3035 self::time::strftime("%F_%H-%M-%S", &self::time::now()).unwrap(),
3036 helpers::make_filename_secret());
3037
3038 conn.export_contest_results_to_file(contest_id, &taskgroup_ids, &format!("./export/{}", filename));
3039
3040 Ok(filename)
3041}
3042
3043pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
3044 let session = conn.get_session(&session_token)
3045 .ensure_logged_in()
3046 .ok_or(MedalError::NotLoggedIn)?
3047 .ensure_admin()
3048 .ok_or(MedalError::AccessDenied)?;
3049
3050 let mut data = json_val::Map::new();
3051 fill_user_data(&session, &mut data);
3052
3053 let now = time::get_time();
3054 let maxage = now - time::Duration::days(30); let n_temporary_session = conn.count_temporary_sessions(maxage);
3056 data.insert("temporary_session_count".to_string(), to_json(&n_temporary_session));
3057
3058 Ok(("admin_cleanup".to_string(), data))
3059}
3060
3061pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str) -> JsonValueResult {
3062 let session = conn.get_session(&session_token)
3063 .ensure_logged_in()
3064 .ok_or(MedalError::NotLoggedIn)?
3065 .ensure_admin()
3066 .ok_or(MedalError::AccessDenied)?;
3067
3068 if session.csrf_token != csrf_token {
3069 return Err(MedalError::CsrfCheckFailed);
3070 }
3071
3072 let now = time::get_time();
3073 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));
3078
3079 let mut data = json_val::Map::new();
3080 if let Ok((n_user, n_group, n_teacher, n_other)) = result {
3081 data.insert("n_user".to_string(), to_json(&n_user));
3082 data.insert("n_group".to_string(), to_json(&n_group));
3083 data.insert("n_teacher".to_string(), to_json(&n_teacher));
3084 data.insert("n_other".to_string(), to_json(&n_other));
3085 Ok(data)
3086 } else {
3087 data.insert("reason".to_string(), to_json(&"Datenbank-Fehler."));
3088 Err(MedalError::ErrorWithJson(data))
3089 }
3090}
3091
3092pub fn do_session_cleanup<T: MedalConnection>(conn: &T) -> JsonValueResult {
3093 let now = time::get_time();
3094 let maxage = now - time::Duration::days(30); conn.remove_temporary_sessions(maxage, Some(1000));
3097
3098 let data = json_val::Map::new();
3099 Ok(data)
3100}
3101
3102pub fn move_task_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
3103 new_location: &str, contest: Option<i32>)
3104 -> JsonValueResult {
3105 let session = conn.get_session(&session_token)
3106 .ensure_logged_in()
3107 .ok_or(MedalError::NotLoggedIn)?
3108 .ensure_admin()
3109 .ok_or(MedalError::AccessDenied)?;
3110
3111 if session.csrf_token != csrf_token {
3112 return Err(MedalError::CsrfCheckFailed);
3113 }
3114
3115 let mut data = json_val::Map::new();
3116 if old_location == new_location {
3117 data.insert("reason".to_string(), to_json(&"old and new location identical"));
3118 return Err(MedalError::ErrorWithJson(data));
3119 }
3120
3121 let n_contest = conn.move_task_location(old_location, new_location, contest);
3122
3123 data.insert("contests_modified".to_string(), to_json(&n_contest));
3124
3125 Ok(data)
3126}
3127
3128#[derive(PartialEq, Eq, Clone, Copy)]
3129pub enum UserType {
3130 User,
3131 Teacher,
3132 Admin,
3133}
3134
3135pub enum UserSex {
3136 Female,
3137 Male,
3138 Unknown,
3139}
3140
3141pub struct ForeignUserData {
3142 pub foreign_id: String,
3143 pub foreign_type: UserType,
3144 pub sex: UserSex,
3145 pub firstname: String,
3146 pub lastname: String,
3147 pub school_name: Option<String>,
3148}
3149
3150pub fn login_oauth<T: MedalConnection>(conn: &T, user_data: ForeignUserData, oauth_provider_id: String,
3151 autoclean_submissions: bool)
3152 -> Result<(String, bool), (String, json_val::Map<String, json_val::Value>)> {
3153 match conn.login_foreign(None,
3154 &oauth_provider_id,
3155 &user_data.foreign_id,
3156 (user_data.foreign_type != UserType::User,
3157 user_data.foreign_type == UserType::Admin,
3158 &user_data.firstname,
3159 &user_data.lastname,
3160 match user_data.sex {
3161 UserSex::Male => Some(1),
3162 UserSex::Female => Some(2),
3163 UserSex::Unknown => Some(0),
3164 },
3165 &user_data.school_name))
3166 {
3167 Ok((session_token, last_activity)) => {
3168 let redirect_profile = if let Some(last_activity) = last_activity {
3169 let now = time::get_time();
3170 now - last_activity > time::Duration::days(60)
3171 } else {
3172 true
3173 };
3174
3175 let now = time::get_time();
3179 let maxage = now - time::Duration::days(30);
3180 conn.remove_temporary_sessions(maxage, None); if autoclean_submissions {
3183 let maxage = now - time::Duration::days(30);
3184 conn.remove_autosaved_submissions(maxage, None); let maxage = now - time::Duration::days(90);
3187 conn.remove_all_but_latest_submissions(maxage, None); }
3189
3190 Ok((session_token, redirect_profile))
3191 }
3192 Err(()) => {
3193 let mut data = json_val::Map::new();
3194 data.insert("reason".to_string(), to_json(&"OAuth-Login failed.".to_string()));
3195 Err(("login".to_owned(), data))
3196 }
3197 }
3198}