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