Skip to main content

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