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