medal/
core.rs

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