medal/
core.rs

1/*  medal                                                                                                            *\
2 *  Copyright (C) 2022  Bundesweite Informatikwettbewerbe, Robert Czechowski                                         *
3 *                                                                                                                   *
4 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
5 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
6 *  your option) any later version.                                                                                  *
7 *                                                                                                                   *
8 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
9 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
10 *  License for more details.                                                                                        *
11 *                                                                                                                   *
12 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
13\*  <http://www.gnu.org/licenses/>.                                                                                  */
14
15use 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                // Same end date
219
220                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                // Same end date
264
265                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        // For compatibility with the older code we return None here
493        // We could also return true and drop the Option<…>, but would need to check the template logic
494        return None;
495    }
496
497    for required_contest in &contest.requires_contests {
498        if required_contest.required_stars == 0 {
499            // TODO: maybe rewrite as filter?
500            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            // TODO: maybe rewrite as filter?
509            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        // Handle inconsistent server time
576    }
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    // Autostart if appropriate
706    // TODO: Should participation start automatically for teacher? Even before the contest start?
707    // Should teachers have all time access or only the same limited amount of time?
708    // if opt_part.is_none() && (contest.duration == 0 || session.is_teacher) {
709    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        // Is team participation?
759        if let Some(team) = participation.team {
760            // Not relevant for team lead
761            if team != session.id {
762                if time_info.is_time_left {
763                    // Do not allow to participate!
764                    data.insert("block_team_participation".to_string(), to_json(&true));
765                } else {
766                    // Show grades of team participation instead!
767                    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    // TODO: Is _or_new the right semantic? We need a CSRF token anyway …
968    let session = conn.get_session_or_new(&session_token).unwrap();
969    // TODO: We only need the required contests here, maybe add them to get_contest_by_id?
970    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
971
972    // Check logged in or open contest
973    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    // Check CSRF token
981    if session.is_logged_in() && session.csrf_token != csrf_token {
982        return Err(MedalError::CsrfCheckFailed);
983    }
984
985    // Check other constraints
986    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), // Contest does not allow team participation
1005            Some(max_teamsize) => {
1006                if max_teamsize < 2 {
1007                    return Err(MedalError::AccessDenied); // Contest does not allow team participation
1008                }
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); // Contest already started TODO: Maybe redirect to page with hint
1028            }
1029
1030            // Ok, we want a team participation and have a valid logincode and neither the user
1031            // nor the team partner started the contest yet. We can now be relatively sure this
1032            // will work out:
1033            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), // Contest already started TODO: Maybe redirect to page with hint
1038            };
1039        } else {
1040            return Ok(Err("invalid_logincode".to_string()));
1041        }
1042    }
1043
1044    // Start contest
1045    match conn.new_participation(session.id, contest_id, None) {
1046        Ok(_) => Ok(Ok(())),
1047        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
1048    }
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    // Get Domain name; make 'example.com' from 'https://example.com:8080/example/path'
1095    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    // Initiate a basic registration flow to enroll a cryptographic authenticator
1108    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(&registration).unwrap();
1138    let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1139
1140    let passkey = webauthn.finish_passkey_registration(&credential, &registration);
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            // Happens if no pin is set e.g.
1153            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            // Is it not our own submission?
1289            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                        // We are not admin of the user's group
1293                        return Err(MedalError::AccessDenied);
1294                    }
1295                } else {
1296                    // The user has no group
1297                    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                // Contest over
1325                // TODO: Nicer message!
1326            }
1327            if participation.team.is_some() && participation.team != Some(session.id) {
1328                return Err(MedalError::AccessDenied);
1329            }
1330        }
1331    }
1332
1333    /* Here, two variants of the grade are calculated. Which one is correct depends on how the percentage value is
1334     * calculated in the task. Currently, grade_rounded is the correct one, but if that ever changes, the other code
1335     * can just be used.
1336     *
1337     * Switch to grade_truncated, when a user scores 98/99 but only gets 97/99 awarded.
1338     * Switch to grade_rounded, when a user scores 5/7 but only gets 4/7 awarded.
1339     */
1340
1341    /* Code for percentages calculated with integer rounding.
1342     *
1343     * This is a poor man's rounding that only works for division by 100.
1344     *
1345     *   floor((floor((x*10)/100)+5)/10) = round(x/100)
1346     */
1347    let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1348
1349    /* Code for percentages calculated with integer truncation.
1350     *
1351     * Why add one to grade_percentage and divide by 101?
1352     *
1353     * For all m in 1..100 and all n in 0..n, this holds:
1354     *
1355     *   floor( ((floor(n / m * 100)+1) * m ) / 101 ) = n
1356     *
1357     * Thus, when percentages are calculated as
1358     *
1359     *   p = floor(n / m * 100)
1360     *
1361     * we can recover n by using
1362     *
1363     *   n = floor( ((p+1) * m) / 101 )
1364     */
1365    // let grade_truncated = ((grade_percentage+1) * t.stars) / 101;
1366
1367    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(), // will be overwritten depending on existing grade!
1376                                  highest_grade_latest: Default::default(), // will be overwritten depending on existing grade!
1377                                  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()); // TODO: Unwrap?
1392    let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; // TODO: Unwrap?
1393
1394    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))); // TODO: fail better
1421
1422            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            // Show contest page if this is a team participation but the current user is not the team lead
1437            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                // Contest over
1478                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    // TODO: We make a fake grade here, that represents this very submission, but maybe it is more sensible to retrieve
1492    // the actual grade here? If yes, use conn.get_taskgroup_user_grade(session.id, tg.id.unwrap());
1493    let grade = Grade { taskgroup: tg.id.unwrap(),
1494                        user: session.id,
1495                        grade: Some(submission.grade),
1496                        validated: submission.validated };
1497
1498    // Is it not our own submission?
1499    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                // We are not admin of the user's group
1503                return Err(MedalError::AccessDenied);
1504            }
1505        } else {
1506            // The user has no group
1507            return Err(MedalError::AccessDenied);
1508        }
1509    }
1510
1511    let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); // TODO does this work in general?
1512
1513    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("contestname".to_string(), to_json(&contest.name));
1549    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("contestid".to_string(), to_json(&contest.id));
1554    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    // Require a public, accessible standalone task
1588    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(); // TODO handle error
1708
1709    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
1794// TODO: Should creating the users and groups happen in a batch operation to speed things up?
1795pub 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))?; // TODO: Change error type
1804    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))?; // TODO: Change error type
1883
1884    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                                                  // Using 0x1f (ACSII 31, unit seperator) as seperator
1891                                                  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    //TODO: use g
1905    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                // This should be changed so that it can be configured if
1956                // addresses can be obtained from OAuth provider
1957            }
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                                      // Now we need to get the stars
1996                                      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                                      // Find the correct sticker
2004                                      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            // TODO: Needs to be filtered
2020            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        // Case user_id: teacher modifing a students profile
2060        Some(user_id) => {
2061            // TODO: Add test to check if this access restriction works
2062            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); // CsrfError
2139    }
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            // TODO: Add test to check if this access restriction works
2200            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    // Only activate for first login
2245    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        // Check access for teachers
2349        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        // Check access for teachers
2437        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        // Check access for teachers
2480        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        // Check access for teachers
2523        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(); // TODO handle error
2560
2561    if !session.is_admin() {
2562        // Check access for teachers
2563        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(); // TODO handle error
2641
2642    if !session.is_admin() {
2643        // If user is not a server admin, he must be a group admin
2644        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(); // TODO handle error
2694
2695    if !session.is_admin() {
2696        // If user is not a server admin, he must be a group admin
2697        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(); // TODO handle error
2732
2733    if !session.is_admin() {
2734        // Check access for teachers
2735        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(); // TODO handle error
2765
2766    if !session.is_admin() {
2767        // Check access for teachers
2768        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(); // TODO handle error
2802
2803    if !session.is_admin() {
2804        // Check access for teachers
2805        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(); // TODO handle error
2839
2840    if !session.is_admin() {
2841        // Check access for teachers
2842        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        // Check access for teachers
2891        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        // Check access for teachers
2983        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(); // Sort by id (sorts by natural order on fields)
3015    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); // Count all temporary sessions with 30 days of inactivity
3055    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); // Delete managed users after 450 days of inactivity
3074    let maxteacherage = now - time::Duration::days(1095); // Delete teachers after 3 years of inactivity
3075    let maxage = now - time::Duration::days(3650); // Delete every user after 10 years of inactivity
3076
3077    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); // Delete all temporary sessions after 30 days of inactivity
3095
3096    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            // While we're at it, lets also remove some old sessions! OAuth took
3176            // some time anyway, so we can afford spending some additional
3177            // milliseconds …
3178            let now = time::get_time();
3179            let maxage = now - time::Duration::days(30);
3180            conn.remove_temporary_sessions(maxage, None); // Delete all temporary sessions after 30 days of inactivity
3181
3182            if autoclean_submissions {
3183                let maxage = now - time::Duration::days(30);
3184                conn.remove_autosaved_submissions(maxage, None); // Delete all autosaved submissions after 30 days
3185
3186                let maxage = now - time::Duration::days(90);
3187                conn.remove_all_but_latest_submissions(maxage, None); // Delete all but latest submissions after 90 days
3188            }
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}