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