Skip to main content

medal/
core.rs

1/*  medal                                                                                                            *\
2 *  Copyright (C) 2022  Bundesweite Informatikwettbewerbe, Robert Czechowski                                         *
3 *                                                                                                                   *
4 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
5 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
6 *  your option) any later version.                                                                                  *
7 *                                                                                                                   *
8 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
9 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
10 *  License for more details.                                                                                        *
11 *                                                                                                                   *
12 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
13\*  <http://www.gnu.org/licenses/>.                                                                                  */
14
15use time;
16
17use config::OauthProvider;
18use db_conn::MedalConnection;
19#[cfg(feature = "signup")]
20use db_conn::SignupResult;
21use db_objects::OptionSession;
22use db_objects::SessionUser;
23use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
24use helpers;
25use webauthn_rs::prelude as webauthn;
26use webfw_iron::{json_val, to_json};
27
28#[derive(Serialize, Deserialize)]
29pub struct SubTaskInfo {
30    pub id: 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_v2".to_owned(), t.location.as_str()),
1466                        "python" => {
1467                            data.insert("tasklang".to_string(), to_json(&"python"));
1468                            ("wtask_v2".to_owned(), t.location.as_str())
1469                        }
1470                        "legacy_blockly" => ("wtask".to_owned(), t.location.as_str()),
1471                        "legacy_python" => {
1472                            data.insert("tasklang".to_string(), to_json(&"python"));
1473                            ("wtask".to_owned(), t.location.as_str())
1474                        }
1475                        _ => ("task".to_owned(), t.location.as_str()),
1476                    }
1477                } else {
1478                    match t.location.chars().next() {
1479                        Some('B') => ("wtask".to_owned(), &t.location[1..]),
1480                        Some('P') => {
1481                            data.insert("tasklang".to_string(), to_json(&"python"));
1482                            ("wtask".to_owned(), &t.location[1..])
1483                        }
1484                        _ => ("task".to_owned(), t.location.as_str()),
1485                    }
1486                };
1487
1488                let taskpath = format!("{}{}", contest.location, tasklocation);
1489                data.insert("taskpath".to_string(), to_json(&taskpath));
1490
1491                Ok(Ok((template, data)))
1492            } else {
1493                // Contest over
1494                Ok(Err((contest.id.unwrap(), contest.category)))
1495            }
1496        }
1497    }
1498}
1499
1500pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, submission_id: i32)
1501                                       -> MedalResult<Result<MedalValue, i32>> {
1502    let session = conn.get_session_or_new(&session_token).unwrap();
1503
1504    let (submission, t, tg, contest) =
1505        conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1506
1507    // TODO: We make a fake grade here, that represents this very submission, but maybe it is more sensible to retrieve
1508    // the actual grade here? If yes, use conn.get_taskgroup_user_grade(session.id, tg.id.unwrap());
1509    let grade = Grade { taskgroup: tg.id.unwrap(),
1510                        user: session.id,
1511                        grade: Some(submission.grade),
1512                        validated: submission.validated };
1513
1514    // Is it not our own submission?
1515    if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1516        if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1517            if !group.admins.contains(&session.id) {
1518                // We are not admin of the user's group
1519                return Err(MedalError::AccessDenied);
1520            }
1521        } else {
1522            // The user has no group
1523            return Err(MedalError::AccessDenied);
1524        }
1525    }
1526
1527    let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); // TODO does this work in general?
1528
1529    let mut data = json_val::Map::new();
1530    data.insert("subtasks".to_string(), to_json(&subtaskstars));
1531
1532    let time_info = ContestTimeInfo { passed_secs_total: 0,
1533                                      left_secs_total: 0,
1534                                      left_mins_total: 0,
1535                                      left_hour: 0,
1536                                      left_min: 0,
1537                                      left_sec: 0,
1538                                      has_timelimit: contest.duration != 0,
1539                                      is_time_left: false,
1540                                      exempt_from_timelimit: true,
1541                                      can_still_compete: false,
1542                                      review_has_timelimit: false,
1543                                      has_future_review: false,
1544                                      has_review_end: false,
1545                                      is_review: true,
1546                                      can_still_compete_or_review: true,
1547
1548                                      until_review_start_day: 0,
1549                                      until_review_start_hour: 0,
1550                                      until_review_start_min: 0,
1551
1552                                      until_review_end_day: 0,
1553                                      until_review_end_hour: 0,
1554                                      until_review_end_min: 0 };
1555
1556    data.insert("time_info".to_string(), to_json(&time_info));
1557
1558    data.insert("time_left_mh_formatted".to_string(),
1559                to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1560    data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1561
1562    data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1563
1564    //data.insert("contestname".to_string(), to_json(&contest.name));
1565    data.insert("name".to_string(), to_json(&tg.name));
1566    data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", tg.name, contest.name)));
1567    data.insert("taskid".to_string(), to_json(&task_id));
1568    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1569    //data.insert("contestid".to_string(), to_json(&contest.id));
1570    data.insert("readonly".to_string(), to_json(&time_info.is_review));
1571
1572    data.insert("submission".to_string(), to_json(&submission_id));
1573
1574    let (template, tasklocation) = if let Some(language) = t.language {
1575        match language.as_str() {
1576            "blockly" => ("wtask".to_owned(), t.location.as_str()),
1577            "python" => {
1578                data.insert("tasklang".to_string(), to_json(&"python"));
1579                ("wtask".to_owned(), t.location.as_str())
1580            }
1581            _ => ("task".to_owned(), t.location.as_str()),
1582        }
1583    } else {
1584        match t.location.chars().next() {
1585            Some('B') => ("wtask".to_owned(), &t.location[1..]),
1586            Some('P') => {
1587                data.insert("tasklang".to_string(), to_json(&"python"));
1588                ("wtask".to_owned(), &t.location[1..])
1589            }
1590            _ => ("task".to_owned(), t.location.as_str()),
1591        }
1592    };
1593
1594    let taskpath = format!("{}{}", contest.location, tasklocation);
1595    data.insert("taskpath".to_string(), to_json(&taskpath));
1596
1597    Ok(Ok((template, data)))
1598}
1599
1600pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> {
1601    let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1602
1603    // Require a public, accessible standalone task
1604    if !contest.public
1605       || contest.duration != 0
1606       || !contest.requires_contests.is_empty()
1607       || contest.requires_login == Some(true)
1608       || contest.standalone_task != Some(true)
1609    {
1610        return Err(MedalError::UnknownId);
1611    }
1612
1613    let time_info = ContestTimeInfo { passed_secs_total: 0,
1614                                      left_secs_total: 0,
1615                                      left_mins_total: 0,
1616                                      left_hour: 0,
1617                                      left_min: 0,
1618                                      left_sec: 0,
1619                                      has_timelimit: contest.duration != 0,
1620                                      is_time_left: false,
1621                                      exempt_from_timelimit: true,
1622                                      can_still_compete: false,
1623                                      review_has_timelimit: false,
1624                                      has_future_review: false,
1625                                      has_review_end: false,
1626                                      is_review: true,
1627                                      can_still_compete_or_review: true,
1628
1629                                      until_review_start_day: 0,
1630                                      until_review_start_hour: 0,
1631                                      until_review_start_min: 0,
1632
1633                                      until_review_end_day: 0,
1634                                      until_review_end_hour: 0,
1635                                      until_review_end_min: 0 };
1636
1637    let mut data = json_val::Map::new();
1638
1639    data.insert("time_info".to_string(), to_json(&time_info));
1640
1641    data.insert("time_left_mh_formatted".to_string(),
1642                to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1643    data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1644
1645    data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1646
1647    data.insert("contestname".to_string(), to_json(&contest.name));
1648    data.insert("name".to_string(), to_json(&tg.name));
1649    data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", tg.name, contest.name)));
1650    data.insert("taskid".to_string(), to_json(&task_id));
1651    data.insert("contestid".to_string(), to_json(&contest.id));
1652    data.insert("readonly".to_string(), to_json(&time_info.is_review));
1653    data.insert("preview".to_string(), to_json(&true));
1654
1655    let (template, tasklocation) = if let Some(language) = t.language {
1656        match language.as_str() {
1657            "blockly" => ("wtask".to_owned(), t.location.as_str()),
1658            "python" => {
1659                data.insert("tasklang".to_string(), to_json(&"python"));
1660                ("wtask".to_owned(), t.location.as_str())
1661            }
1662            _ => ("task".to_owned(), t.location.as_str()),
1663        }
1664    } else {
1665        match t.location.chars().next() {
1666            Some('B') => ("wtask".to_owned(), &t.location[1..]),
1667            Some('P') => {
1668                data.insert("tasklang".to_string(), to_json(&"python"));
1669                ("wtask".to_owned(), &t.location[1..])
1670            }
1671            _ => ("task".to_owned(), t.location.as_str()),
1672        }
1673    };
1674
1675    let taskpath = format!("{}{}", contest.location, tasklocation);
1676    data.insert("taskpath".to_string(), to_json(&taskpath));
1677
1678    Ok(Ok((template, data)))
1679}
1680
1681#[derive(Serialize, Deserialize)]
1682pub struct GroupInfo {
1683    pub id: i32,
1684    pub name: String,
1685    pub tag: String,
1686    pub code: String,
1687}
1688
1689pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1690    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1691
1692    let mut data = json_val::Map::new();
1693    fill_user_data(&session, &mut data);
1694
1695    let v: Vec<GroupInfo> =
1696        conn.get_groups(session.id)
1697            .iter()
1698            .map(|g| GroupInfo { id: g.id.unwrap(),
1699                                 name: g.name.clone(),
1700                                 tag: g.tag.clone(),
1701                                 code: g.groupcode.clone() })
1702            .collect();
1703    data.insert("group".to_string(), to_json(&v));
1704    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1705
1706    Ok(("groups".to_string(), data))
1707}
1708
1709#[derive(Serialize, Deserialize)]
1710pub struct MemberInfo {
1711    pub id: i32,
1712    pub firstname: String,
1713    pub lastname: String,
1714    pub sex: String,
1715    pub grade: String,
1716    pub logincode: String,
1717    pub anonymous: bool,
1718    pub data_protection_clearance: bool,
1719}
1720
1721pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
1722    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1723    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
1724
1725    let mut data = json_val::Map::new();
1726    fill_user_data(&session, &mut data);
1727
1728    if !group.admins.contains(&session.id) {
1729        return Err(MedalError::AccessDenied);
1730    }
1731
1732    let gi = GroupInfo { id: group.id.unwrap(),
1733                         name: group.name.clone(),
1734                         tag: group.tag.clone(),
1735                         code: group.groupcode.clone() };
1736
1737    let v: Vec<MemberInfo> = group.members
1738                                  .iter()
1739                                  .filter_map(|m| {
1740                                      Some(MemberInfo { id: m.id,
1741                                                        firstname: m.firstname.clone()?,
1742                                                        lastname: m.lastname.clone()?,
1743                                                        sex: (match m.sex {
1744                                                                 Some(0) | None => "/",
1745                                                                 Some(1) => "m",
1746                                                                 Some(2) => "w",
1747                                                                 Some(3) => "d",
1748                                                                 Some(4) => "…",
1749                                                                 _ => "?",
1750                                                             }).to_string(),
1751                                                        grade: grade_to_string(m.grade),
1752                                                        logincode: m.logincode.clone()?,
1753                                                        anonymous: m.anonymous,
1754                                                        data_protection_clearance: m.data_protection_clearance == 1 })
1755                                  })
1756                                  .collect();
1757
1758    data.insert("group".to_string(), to_json(&gi));
1759    data.insert("member".to_string(), to_json(&v));
1760    data.insert("groupname".to_string(), to_json(&gi.name));
1761
1762    Ok(("group".to_string(), data))
1763}
1764
1765pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
1766                                     -> MedalResult<i32> {
1767    let session = conn.get_session(&session_token)
1768                      .ensure_logged_in()
1769                      .ok_or(MedalError::AccessDenied)?
1770                      .ensure_teacher_or_admin()
1771                      .ok_or(MedalError::AccessDenied)?;
1772
1773    if session.csrf_token != csrf_token {
1774        return Err(MedalError::CsrfCheckFailed);
1775    }
1776
1777    let mut groupcode = String::new();
1778    for i in 0..10 {
1779        if i == 9 {
1780            panic!("ERROR: Too many groupcode collisions! Give up ...");
1781        }
1782        groupcode = helpers::make_groupcode();
1783        if !conn.code_exists(&groupcode) {
1784            break;
1785        }
1786        println!("WARNING: Groupcode collision! Retrying ...");
1787    }
1788
1789    let mut group = Group { id: None, name, groupcode, tag, admins: vec![session.id], members: Vec::new() };
1790
1791    conn.add_group(&mut group);
1792
1793    Ok(group.id.unwrap())
1794}
1795
1796pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
1797    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1798
1799    let mut data = json_val::Map::new();
1800    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1801
1802    data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1803    data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1804    data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1805    data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1806
1807    Ok(("groupcsv".to_string(), data))
1808}
1809
1810// TODO: Should creating the users and groups happen in a batch operation to speed things up?
1811pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
1812                                         -> MedalResult<()> {
1813    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1814
1815    if session.csrf_token != csrf_token {
1816        return Err(MedalError::CsrfCheckFailed);
1817    }
1818
1819    let mut v: Vec<Vec<String>> = serde_json::from_str(group_data).or(Err(MedalError::AccessDenied))?; // TODO: Change error type
1820    v.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1821
1822    let mut groupcode = String::new();
1823    let mut name = String::new();
1824    let mut group = Group { id: None,
1825                            name: name.clone(),
1826                            groupcode,
1827                            tag: String::new(),
1828                            admins: vec![session.id],
1829                            members: Vec::new() };
1830
1831    for line in v {
1832        if name != line[0] {
1833            if name != "" {
1834                conn.update_or_create_group_with_users(group, session.id);
1835            }
1836            name = line[0].clone();
1837
1838            groupcode = String::new();
1839            for i in 0..10 {
1840                if i == 9 {
1841                    panic!("ERROR: Too many groupcode collisions! Give up ...");
1842                }
1843                groupcode = helpers::make_groupcode();
1844                if !conn.code_exists(&groupcode) {
1845                    break;
1846                }
1847                println!("WARNING: Groupcode collision! Retrying ...");
1848            }
1849
1850            group = Group { id: None,
1851                            name: name.clone(),
1852                            groupcode,
1853                            tag: name.clone(),
1854                            admins: vec![session.id],
1855                            members: Vec::new() };
1856        }
1857
1858        let mut user = SessionUser::group_user_stub();
1859        user.grade = line[1].parse::<i32>().unwrap_or(0);
1860        user.firstname = Some(line[2].clone());
1861        user.lastname = Some(line[3].clone());
1862
1863        use db_objects::Sex;
1864        match line[4].as_str() {
1865            "m" => user.sex = Some(Sex::Male as i32),
1866            "f" => user.sex = Some(Sex::Female as i32),
1867            "d" => user.sex = Some(Sex::Diverse as i32),
1868            _ => user.sex = None,
1869        }
1870
1871        user.anonymous = line[5] == "y";
1872
1873        group.members.push(user);
1874    }
1875    conn.update_or_create_group_with_users(group, session.id);
1876
1877    Ok(())
1878}
1879
1880pub fn contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1881    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1882
1883    let mut data = json_val::Map::new();
1884    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1885
1886    Ok(("admin_admissioncsv".to_string(), data))
1887}
1888
1889pub fn upload_contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
1890                                                     contest_id: i32, admission_data: &str)
1891                                                     -> MedalResult<()> {
1892    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1893
1894    if session.csrf_token != csrf_token {
1895        return Err(MedalError::CsrfCheckFailed);
1896    }
1897
1898    let v: Vec<Vec<String>> = serde_json::from_str(admission_data).or(Err(MedalError::AccessDenied))?; // TODO: Change error type
1899
1900    let w: Vec<(i32, Option<String>)> = v.into_iter()
1901                                         .map(|vv| {
1902                                             (vv[0].parse().unwrap_or(-1),
1903                                              if vv[1].len() == 0 && vv[2].len() == 0 && vv[3].len() == 0 {
1904                                                  None
1905                                              } else {
1906                                                  // Using 0x1f (ACSII 31, unit seperator) as seperator
1907                                                  Some(format!("{}\x1f{}\x1f{}", vv[1], vv[2], vv[3]))
1908                                              })
1909                                         })
1910                                         .collect();
1911
1912    let _annotations_inserted = conn.insert_contest_annotations(contest_id, w);
1913
1914    Ok(())
1915}
1916
1917#[allow(dead_code)]
1918pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
1919    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1920    //TODO: use g
1921    let _g = conn.get_contest_groups_grades(session.id, contest_id);
1922
1923    let data = json_val::Map::new();
1924
1925    Ok(("groupresults".into(), data))
1926}
1927
1928pub struct SexInformation {
1929    pub require_sex: bool,
1930    pub allow_sex_na: bool,
1931    pub allow_sex_diverse: bool,
1932    pub allow_sex_other: bool,
1933}
1934
1935pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
1936                                        query_string: Option<String>, sex_infos: SexInformation)
1937                                        -> MedalValueResult {
1938    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1939
1940    let mut data = json_val::Map::new();
1941    fill_user_data(&session, &mut data);
1942
1943    data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1944    data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1945    data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1946    data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1947
1948    match user_id {
1949        None => {
1950            data.insert("profile_firstname".to_string(), to_json(&session.firstname));
1951            data.insert("profile_lastname".to_string(), to_json(&session.lastname));
1952            data.insert("profile_street".to_string(), to_json(&session.street));
1953            data.insert("profile_zip".to_string(), to_json(&session.zip));
1954            data.insert("profile_city".to_string(), to_json(&session.city));
1955            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
1956            if let Some(sex) = session.sex {
1957                data.insert(format!("sex_{}", sex), to_json(&"selected"));
1958            } else {
1959                data.insert("sex_None".to_string(), to_json(&"selected"));
1960            }
1961
1962            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
1963            if session.password.is_some() {
1964                data.insert("profile_username".to_string(), to_json(&session.username));
1965            }
1966            if session.managed_by.is_none() {
1967                data.insert("profile_not_in_group".into(), to_json(&true));
1968            }
1969            if session.oauth_provider != Some("pms".to_string()) {
1970                data.insert("profile_not_pms".into(), to_json(&true));
1971                // This should be changed so that it can be configured if
1972                // addresses can be obtained from OAuth provider
1973            }
1974            data.insert("ownprofile".into(), to_json(&true));
1975            data.insert("userid".into(), to_json(&session.id));
1976
1977            let webauthn = conn.get_webauthn_passkey_names_for_user(session.id);
1978            data.insert("webauthn".into(), to_json(&webauthn));
1979            data.insert("has_webauthn".into(), to_json(&!webauthn.is_empty()));
1980
1981            if let Some(query) = query_string {
1982                if let Some(status) = query.strip_prefix("status=") {
1983                    if ["NothingChanged",
1984                        "DataChanged",
1985                        "PasswordChanged",
1986                        "PasswordMissmatch",
1987                        "firstlogin",
1988                        "SignedUp"].contains(&status)
1989                    {
1990                        data.insert((status).to_string(), to_json(&true));
1991                    }
1992                }
1993            }
1994
1995            let now = time::get_time();
1996
1997            let all_participations = conn.get_all_participations_complete(session.id);
1998
1999            let mut contest_stars: Option<std::collections::BTreeMap<i32, i32>> = None;
2000
2001            let stickers: Vec<String> =
2002                all_participations.iter()
2003                                  .filter_map(|(_, contest)| {
2004                                      if contest.stickers.len() == 0 {
2005                                          return None;
2006                                      }
2007                                      if contest.stickers.len() == 1 && contest.stickers[0].1 == 0 {
2008                                          return Some(format!("{}/{}", contest.location, contest.stickers[0].0));
2009                                      }
2010
2011                                      // Now we need to get the stars
2012                                      if contest_stars.is_none() {
2013                                          contest_stars =
2014                                              Some(conn.count_all_stars_by_contest(session.id).into_iter().collect());
2015                                      }
2016                                      let stars: i32 =
2017                                          *contest_stars.as_ref().unwrap().get(&contest.id.unwrap()).unwrap_or(&0);
2018
2019                                      // Find the correct sticker
2020                                      let mut value: Option<String> = None;
2021                                      let mut min_diff: i32 = -1;
2022
2023                                      for sticker in &contest.stickers {
2024                                          let diff = stars - sticker.1;
2025                                          if diff >= 0 && (min_diff < 0 || diff < min_diff) {
2026                                              value = Some(format!("{}{}", contest.location, sticker.0));
2027                                              min_diff = diff;
2028                                          }
2029                                      }
2030                                      value
2031                                  })
2032                                  .collect();
2033            data.insert("stickers".into(), to_json(&stickers));
2034
2035            // TODO: Needs to be filtered
2036            let participations: (Vec<(i32, String, bool, bool, bool, Option<String>)>,
2037                                 Vec<(i32, String, bool, bool, bool, Option<String>)>) =
2038                all_participations.into_iter()
2039                                  .rev()
2040                                  .map(|(participation, contest)| {
2041                                      let passed_secs = now.sec - participation.start.sec;
2042                                      let left_secs = i64::from(contest.duration) * 60 - passed_secs;
2043                                      let is_time_left = contest.duration == 0 || left_secs >= 0;
2044                                      let has_timelimit = contest.duration != 0;
2045                                      let requires_login = contest.requires_login == Some(true);
2046                                      let annotation = participation.annotation.map(|annotation| {
2047                                                                                   annotation.split('\x1f')
2048                                                                                             .nth(2)
2049                                                                                             .unwrap_or("")
2050                                                                                             .to_string()
2051                                                                               });
2052
2053                                      (contest.id.unwrap(),
2054                                       contest.name,
2055                                       has_timelimit,
2056                                       is_time_left,
2057                                       requires_login,
2058                                       annotation)
2059                                  })
2060                                  .partition(|contest| contest.2 && !contest.4);
2061            data.insert("participations".into(), to_json(&participations));
2062
2063            let stars_count = conn.count_all_stars(session.id);
2064            data.insert("stars_count".into(), to_json(&stars_count));
2065            let stars_message = match stars_count {
2066                                    0 => "Auf gehts, dein erster Stern wartet auf dich!",
2067                                    1..=9 => "Ein hervorragender Anfang!",
2068                                    10..=99 => "Das ist ziemlich gut!",
2069                                    100..=999 => "Ein wahrer Meister!",
2070                                    _ => "Wow! Einfach wow!",
2071                                }.to_string();
2072
2073            data.insert("stars_message".into(), to_json(&stars_message));
2074        }
2075        // Case user_id: teacher modifing a students profile
2076        Some(user_id) => {
2077            // TODO: Add test to check if this access restriction works
2078            let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2079            let group = opt_group.ok_or(MedalError::AccessDenied)?;
2080            if !group.admins.contains(&session.id) {
2081                return Err(MedalError::AccessDenied);
2082            }
2083
2084            data.insert("profile_firstname".to_string(), to_json(&user.firstname));
2085            data.insert("profile_lastname".to_string(), to_json(&user.lastname));
2086            data.insert("profile_street".to_string(), to_json(&session.street));
2087            data.insert("profile_zip".to_string(), to_json(&session.zip));
2088            data.insert("profile_city".to_string(), to_json(&session.city));
2089            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
2090            if let Some(sex) = user.sex {
2091                data.insert(format!("sex_{}", sex), to_json(&"selected"));
2092            } else {
2093                data.insert("sex_None".to_string(), to_json(&"selected"));
2094            }
2095
2096            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
2097            if user.username.is_some() {
2098                data.insert("profile_username".to_string(), to_json(&user.username));
2099            }
2100            if user.managed_by.is_none() {
2101                data.insert("profile_not_in_group".into(), to_json(&true));
2102            }
2103            if session.oauth_provider != Some("pms".to_string()) {
2104                data.insert("profile_not_pms".into(), to_json(&true));
2105            }
2106            data.insert("ownprofile".into(), to_json(&false));
2107
2108            if let Some(query) = query_string {
2109                if let Some(status) = query.strip_prefix("status=") {
2110                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
2111                        data.insert((status).to_string(), to_json(&true));
2112                    }
2113                }
2114            }
2115        }
2116    }
2117
2118    Ok(("profile".to_string(), data))
2119}
2120
2121#[derive(Debug, PartialEq, Eq)]
2122pub enum ProfileStatus {
2123    NothingChanged,
2124    DataChanged,
2125    PasswordChanged,
2126    PasswordMissmatch,
2127}
2128impl From<ProfileStatus> for String {
2129    fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
2130}
2131
2132pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
2133                                        (firstname,
2134                                         lastname,
2135                                         street,
2136                                         zip,
2137                                         city,
2138                                         password,
2139                                         password_repeat,
2140                                         grade,
2141                                         sex): (String,
2142                                         String,
2143                                         Option<String>,
2144                                         Option<String>,
2145                                         Option<String>,
2146                                         Option<String>,
2147                                         Option<String>,
2148                                         i32,
2149                                         Option<i32>))
2150                                        -> MedalResult<ProfileStatus> {
2151    let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2152
2153    if session.csrf_token != csrf_token {
2154        return Err(MedalError::AccessDenied); // CsrfError
2155    }
2156
2157    let mut result = ProfileStatus::NothingChanged;
2158
2159    let mut password_and_salt = None;
2160
2161    if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
2162        if password != "" || password_repeat != "" {
2163            if password == password_repeat {
2164                let salt = helpers::make_salt();
2165                let hash = helpers::hash_password(&password, &salt)?;
2166
2167                password_and_salt = Some((hash, salt));
2168                result = ProfileStatus::PasswordChanged;
2169            } else {
2170                result = ProfileStatus::PasswordMissmatch;
2171            }
2172        }
2173    }
2174
2175    if result == ProfileStatus::NothingChanged {
2176        if session.firstname.as_ref() == Some(&firstname)
2177           && session.lastname.as_ref() == Some(&lastname)
2178           && session.street == street
2179           && session.zip == zip
2180           && session.city == city
2181           && session.grade == grade
2182           && session.sex == sex
2183        {
2184            return Ok(ProfileStatus::NothingChanged);
2185        } else {
2186            result = ProfileStatus::DataChanged;
2187        }
2188    }
2189
2190    match user_id {
2191        None => {
2192            session.firstname = Some(firstname);
2193            session.lastname = Some(lastname);
2194            session.grade = grade;
2195            session.sex = sex;
2196
2197            if street.is_some() {
2198                session.street = street;
2199            }
2200            if zip.is_some() {
2201                session.zip = zip;
2202            }
2203            if city.is_some() {
2204                session.city = city;
2205            }
2206
2207            if let Some((password, salt)) = password_and_salt {
2208                session.password = Some(password);
2209                session.salt = Some(salt);
2210            }
2211
2212            conn.save_session(session);
2213        }
2214        Some(user_id) => {
2215            // TODO: Add test to check if this access restriction works
2216            let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2217            let group = opt_group.ok_or(MedalError::AccessDenied)?;
2218            if !group.admins.contains(&session.id) {
2219                return Err(MedalError::AccessDenied);
2220            }
2221
2222            user.firstname = Some(firstname);
2223            user.lastname = Some(lastname);
2224            user.grade = grade;
2225            user.sex = sex;
2226
2227            if street.is_some() {
2228                user.street = street;
2229            }
2230            if zip.is_some() {
2231                user.zip = zip;
2232            }
2233            if city.is_some() {
2234                user.city = city;
2235            }
2236
2237            if let Some((password, salt)) = password_and_salt {
2238                user.password = Some(password);
2239                user.salt = Some(salt);
2240            }
2241
2242            conn.save_session(user);
2243        }
2244    }
2245
2246    Ok(result)
2247}
2248
2249pub fn check_profile<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
2250                                         (firstname, lastname): (String, String))
2251                                         -> JsonValueResult {
2252    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2253
2254    if session.csrf_token != csrf_token {
2255        return Err(MedalError::AccessDenied);
2256    }
2257
2258    let mut data = json_val::Map::new();
2259
2260    // Only activate for first login
2261    if session.firstname.map(|n| n.len() > 0).unwrap_or(false) && session.lastname.map(|n| n.len() > 0).unwrap_or(false)
2262    {
2263        data.insert("exists".to_string(), to_json(&false));
2264        return Ok(data);
2265    }
2266
2267    let group =
2268        conn.get_group_complete(session.managed_by.ok_or(MedalError::AccessDenied)?).ok_or(MedalError::AccessDenied)?;
2269
2270    for member in group.members {
2271        if member.lastname.map(|l| l.to_lowercase()) == Some(lastname.to_lowercase())
2272           && member.firstname.map(|l| l.to_lowercase()) == Some(firstname.to_lowercase())
2273           && member.id != session.id
2274        {
2275            data.insert("exists".to_string(), to_json(&true));
2276            return Ok(data);
2277        }
2278    }
2279
2280    data.insert("exists".to_string(), to_json(&false));
2281    Ok(data)
2282}
2283
2284pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2285    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2286    if !session.is_teacher {
2287        return Err(MedalError::AccessDenied);
2288    }
2289
2290    let mut data = json_val::Map::new();
2291    fill_user_data(&session, &mut data);
2292
2293    Ok(("teacher".to_string(), data))
2294}
2295
2296pub fn admin_index<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2297    let session = conn.get_session(&session_token)
2298                      .ensure_logged_in()
2299                      .ok_or(MedalError::NotLoggedIn)?
2300                      .ensure_admin()
2301                      .ok_or(MedalError::AccessDenied)?;
2302
2303    let mut data = json_val::Map::new();
2304    fill_user_data(&session, &mut data);
2305
2306    Ok(("admin".to_string(), data))
2307}
2308
2309pub fn admin_search_users<T: MedalConnection>(conn: &T, session_token: &str,
2310                                              s_data: (Option<i32>,
2311                                               Option<String>,
2312                                               Option<String>,
2313                                               Option<String>,
2314                                               Option<String>,
2315                                               Option<String>,
2316                                               Option<String>,
2317                                               Option<String>))
2318                                              -> MedalValueResult {
2319    let session = conn.get_session(&session_token)
2320                      .ensure_logged_in()
2321                      .ok_or(MedalError::NotLoggedIn)?
2322                      .ensure_admin()
2323                      .ok_or(MedalError::AccessDenied)?;
2324
2325    let mut data = json_val::Map::new();
2326    fill_user_data(&session, &mut data);
2327
2328    let (users, groups) = conn.get_search_users(s_data);
2329
2330    data.insert("users".to_string(), to_json(&users));
2331    data.insert("users_num_results".to_string(), to_json(&users.len()));
2332    data.insert("user_has_results".to_string(), to_json(&(users.len() != 0)));
2333    data.insert("user_no_results".to_string(), to_json(&(users.len() == 0)));
2334
2335    data.insert("groups".to_string(), to_json(&groups));
2336    data.insert("groups_num_results".to_string(), to_json(&groups.len()));
2337    data.insert("groups_has_results".to_string(), to_json(&(groups.len() != 0)));
2338    data.insert("groups_no_results".to_string(), to_json(&(groups.len() == 0)));
2339
2340    data.insert("max_results".to_string(), to_json(&200));
2341    if users.len() > 200 {
2342        data.insert("more_users".to_string(), to_json(&true));
2343        data.insert("more_results".to_string(), to_json(&true));
2344    }
2345    if groups.len() > 200 {
2346        data.insert("more_groups".to_string(), to_json(&true));
2347        data.insert("more_results".to_string(), to_json(&true));
2348    }
2349
2350    data.insert("exactly_one_result".to_string(), to_json(&(users.len() + groups.len() == 1)));
2351    data.insert("no_results".to_string(), to_json(&(users.len() + groups.len() == 0)));
2352
2353    Ok(("admin_search_results".to_string(), data))
2354}
2355
2356pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str) -> MedalValueResult {
2357    let session = conn.get_session(&session_token)
2358                      .ensure_logged_in()
2359                      .ok_or(MedalError::NotLoggedIn)?
2360                      .ensure_teacher_or_admin()
2361                      .ok_or(MedalError::AccessDenied)?;
2362
2363    let mut data = json_val::Map::new();
2364
2365    let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2366
2367    if !session.is_admin() {
2368        // Check access for teachers
2369        if let Some(group) = opt_group.clone() {
2370            if !group.admins.contains(&session.id) {
2371                return Err(MedalError::AccessDenied);
2372            }
2373        } else if user_id != session.id {
2374            return Err(MedalError::AccessDenied);
2375        }
2376    }
2377
2378    fill_user_data(&session, &mut data);
2379    fill_user_data_prefix(&user, &mut data, "user_");
2380    data.insert("user_logincode".to_string(), to_json(&user.logincode));
2381    data.insert("user_id".to_string(), to_json(&user.id));
2382    let grade = if user.grade >= 200 {
2383        "Kein Schüler mehr".to_string()
2384    } else if user.grade >= 11 {
2385        format!("{} ({})", user.grade % 100, if user.grade >= 100 { "G9" } else { "G8" })
2386    } else {
2387        format!("{}", user.grade)
2388    };
2389    data.insert("user_grade".to_string(), to_json(&grade));
2390    data.insert("user_oauthid".to_string(), to_json(&user.oauth_foreign_id));
2391    data.insert("user_oauthprovider".to_string(), to_json(&user.oauth_provider));
2392
2393    if let Some(group) = opt_group {
2394        data.insert("user_group_id".to_string(), to_json(&group.id));
2395        data.insert("user_group_name".to_string(), to_json(&group.name));
2396    }
2397
2398    if let Some(additional_time) = user.additional_contest_time {
2399        data.insert("additional_time".to_string(), to_json(&true));
2400        data.insert(format!("additional{}", additional_time), to_json(&true));
2401    }
2402
2403    let groups: Vec<GroupInfo> =
2404        conn.get_groups(user_id)
2405            .iter()
2406            .map(|g| GroupInfo { id: g.id.unwrap(),
2407                                 name: g.name.clone(),
2408                                 tag: g.tag.clone(),
2409                                 code: g.groupcode.clone() })
2410            .collect();
2411    data.insert("user_group".to_string(), to_json(&groups));
2412
2413    let parts = conn.get_all_participations_complete(user_id);
2414    let has_protected_participations = parts.iter().any(|p| p.1.protected);
2415    let contest_stars = conn.count_all_stars_by_contest(user_id);
2416
2417    let pi: Vec<(i32, String, String, i32)> =
2418        parts.into_iter()
2419             .map(|(p, c)| {
2420                 (c.id.unwrap(),
2421                  format!("{}{}{}",
2422                          c.name,
2423                          if c.protected { " (geschützt)" } else { "" },
2424                          if p.team.is_some() { " (Teamteilnahme)" } else { "" }),
2425                  self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(p.start)).unwrap(),
2426                  contest_stars.iter()
2427                               .filter_map(|(id, stars)| if *id == c.id.unwrap() { Some(*stars) } else { None })
2428                               .next()
2429                               .unwrap_or(0))
2430             })
2431             .collect();
2432
2433    data.insert("user_participations".to_string(), to_json(&pi));
2434    data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2435    data.insert("can_delete".to_string(),
2436                to_json(&((!has_protected_participations || session.is_admin()) && groups.len() == 0)));
2437
2438    Ok(("admin_user".to_string(), data))
2439}
2440
2441pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str, csrf_token: &str)
2442                                             -> JsonValueResult {
2443    let session = conn.get_session(&session_token)
2444                      .ensure_logged_in()
2445                      .ok_or(MedalError::NotLoggedIn)?
2446                      .ensure_teacher_or_admin()
2447                      .ok_or(MedalError::AccessDenied)?;
2448
2449    if session.csrf_token != csrf_token {
2450        return Err(MedalError::CsrfCheckFailed);
2451    }
2452
2453    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2454
2455    if !session.is_admin() {
2456        // Check access for teachers
2457        if let Some(group) = opt_group {
2458            if !group.admins.contains(&session.id) {
2459                return Err(MedalError::AccessDenied);
2460            }
2461        } else {
2462            return Err(MedalError::AccessDenied);
2463        }
2464    }
2465
2466    let parts = conn.get_all_participations_complete(user_id);
2467    let has_protected_participations = parts.iter().any(|p| p.1.protected);
2468    let groups = conn.get_groups(user_id);
2469
2470    let mut data = json_val::Map::new();
2471    if has_protected_participations && !session.is_admin() {
2472        data.insert("reason".to_string(), to_json(&"Benutzer hat Teilnahmen an geschützten Wettbewerben."));
2473        Err(MedalError::ErrorWithJson(data))
2474    } else if groups.len() > 0 {
2475        data.insert("reason".to_string(), to_json(&"Benutzer ist Administrator von Gruppen."));
2476        Err(MedalError::ErrorWithJson(data))
2477    } else {
2478        conn.delete_user(user_id);
2479        Ok(data)
2480    }
2481}
2482
2483pub fn admin_move_user_to_group<T: MedalConnection>(conn: &T, user_id: i32, group_id: i32, session_token: &str,
2484                                                    csrf_token: &str)
2485                                                    -> JsonValueResult {
2486    let session = conn.get_session(&session_token)
2487                      .ensure_logged_in()
2488                      .ok_or(MedalError::NotLoggedIn)?
2489                      .ensure_admin()
2490                      .ok_or(MedalError::AccessDenied)?;
2491
2492    if session.csrf_token != csrf_token {
2493        return Err(MedalError::CsrfCheckFailed);
2494    }
2495
2496    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2497
2498    if !session.is_admin() {
2499        // Check access for teachers
2500        if let Some(group) = opt_group {
2501            if !group.admins.contains(&session.id) {
2502                return Err(MedalError::AccessDenied);
2503            }
2504        } else {
2505            return Err(MedalError::AccessDenied);
2506        }
2507    }
2508
2509    let mut data = json_val::Map::new();
2510    if conn.get_group(group_id).is_some() {
2511        if let Some(mut user) = conn.get_user_by_id(user_id) {
2512            user.managed_by = Some(group_id);
2513            conn.save_session(user);
2514            Ok(data)
2515        } else {
2516            data.insert("reason".to_string(), to_json(&"Benutzer existiert nicht."));
2517            Err(MedalError::ErrorWithJson(data))
2518        }
2519    } else {
2520        data.insert("reason".to_string(), to_json(&"Gruppe existiert nicht."));
2521        Err(MedalError::ErrorWithJson(data))
2522    }
2523}
2524
2525pub fn admin_set_user_additional_contest_time<T: MedalConnection>(conn: &T, user_id: i32,
2526                                                                  additional_contest_time: i32, session_token: &str,
2527                                                                  csrf_token: &str)
2528                                                                  -> JsonValueResult {
2529    let session = conn.get_session(&session_token)
2530                      .ensure_logged_in()
2531                      .ok_or(MedalError::NotLoggedIn)?
2532                      .ensure_teacher_or_admin()
2533                      .ok_or(MedalError::AccessDenied)?;
2534
2535    if session.csrf_token != csrf_token {
2536        return Err(MedalError::CsrfCheckFailed);
2537    }
2538
2539    let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2540
2541    if !session.is_admin() {
2542        // Check access for teachers
2543        if let Some(group) = opt_group {
2544            if !group.admins.contains(&session.id) {
2545                return Err(MedalError::AccessDenied);
2546            }
2547        } else {
2548            return Err(MedalError::AccessDenied);
2549        }
2550    }
2551
2552    let mut data = json_val::Map::new();
2553
2554    if additional_contest_time < 0 || additional_contest_time > 50 {
2555        data.insert("reason".to_string(), to_json(&"Ungültige Angabe."));
2556        return Err(MedalError::ErrorWithJson(data));
2557    }
2558
2559    user.additional_contest_time = if additional_contest_time == 0 { None } else { Some(additional_contest_time) };
2560    conn.save_session(user);
2561
2562    Ok(data)
2563}
2564
2565#[derive(Serialize, Deserialize)]
2566pub struct AdminInfo {
2567    pub id: i32,
2568    pub firstname: String,
2569    pub lastname: String,
2570}
2571
2572pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2573    let session = conn.get_session(&session_token)
2574                      .ensure_logged_in()
2575                      .ok_or(MedalError::NotLoggedIn)?
2576                      .ensure_teacher_or_admin()
2577                      .ok_or(MedalError::AccessDenied)?;
2578
2579    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2580
2581    if !session.is_admin() {
2582        // Check access for teachers
2583        if !group.admins.contains(&session.id) {
2584            return Err(MedalError::AccessDenied);
2585        }
2586    }
2587
2588    let mut data = json_val::Map::new();
2589    fill_user_data(&session, &mut data);
2590
2591    let gi = GroupInfo { id: group.id.unwrap(),
2592                         name: group.name.clone(),
2593                         tag: group.tag.clone(),
2594                         code: group.groupcode.clone() };
2595
2596    let v: Vec<MemberInfo> =
2597        group.members
2598             .iter()
2599             .filter(|m| session.is_admin() || m.firstname.is_some() || m.lastname.is_some())
2600             .map(|m| MemberInfo { id: m.id,
2601                                   firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
2602                                   lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
2603                                   sex: (match m.sex {
2604                                            Some(0) | None => "/",
2605                                            Some(1) => "m",
2606                                            Some(2) => "w",
2607                                            Some(3) => "d",
2608                                            Some(4) => "…",
2609                                            _ => "?",
2610                                        }).to_string(),
2611                                   grade: grade_to_string(m.grade),
2612                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()),
2613                                   anonymous: m.anonymous,
2614                                   data_protection_clearance: m.data_protection_clearance == 1 })
2615             .collect();
2616
2617    let data_protection_clearance_all = group.members.iter().all(|m| m.data_protection_clearance == 1);
2618    let has_anonmous_members = group.members.iter().any(|m| m.anonymous);
2619
2620    let has_protected_participations = conn.group_has_protected_participations(group_id);
2621
2622    data.insert("group".to_string(), to_json(&gi));
2623    data.insert("member".to_string(), to_json(&v));
2624    data.insert("groupname".to_string(), to_json(&gi.name));
2625    data.insert("has_anonymous_members".to_string(), to_json(&has_anonmous_members));
2626    data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2627    data.insert("can_delete".to_string(), to_json(&(!has_protected_participations || session.is_admin())));
2628    data.insert("data_protection_clearance_all".to_string(), to_json(&data_protection_clearance_all));
2629
2630    let admins: Vec<AdminInfo> =
2631        group.admins
2632             .iter()
2633             .map(|a| {
2634                 let admin = conn.get_user_by_id(*a).ok_or(MedalError::AccessDenied)?;
2635                 Ok(AdminInfo { id: admin.id,
2636                                firstname: admin.firstname.clone().unwrap_or_else(|| "".to_string()),
2637                                lastname: admin.lastname.clone().unwrap_or_else(|| "".to_string()) })
2638             })
2639             .collect::<Result<Vec<_>, _>>()?;
2640
2641    data.insert("group_admin".to_string(), to_json(&admins));
2642    data.insert("morethanoneadmin".to_string(), to_json(&(admins.len() > 1)));
2643
2644    Ok(("admin_group".to_string(), data))
2645}
2646
2647pub fn group_add_admin<T: MedalConnection>(conn: &T, group_id: i32, new_admin_id: i32, session_token: &str,
2648                                           csrf_token: &str)
2649                                           -> JsonValueResult {
2650    let session = conn.get_session(&session_token)
2651                      .ensure_logged_in()
2652                      .ok_or(MedalError::NotLoggedIn)?
2653                      .ensure_teacher_or_admin()
2654                      .ok_or(MedalError::AccessDenied)?;
2655
2656    if session.csrf_token != csrf_token {
2657        return Err(MedalError::CsrfCheckFailed);
2658    }
2659
2660    let mut group = conn.get_group(group_id).unwrap(); // TODO handle error
2661
2662    if !session.is_admin() {
2663        // If user is not a server admin, he must be a group admin
2664        if !group.admins.contains(&session.id) {
2665            return Err(MedalError::AccessDenied);
2666        }
2667    }
2668
2669    if group.admins.contains(&new_admin_id) {
2670        let mut data = json_val::Map::new();
2671        data.insert("reason".to_string(), to_json(&"Benutzer ist bereits Admin."));
2672        return Err(MedalError::ErrorWithJson(data));
2673    }
2674
2675    let new_admin = conn.get_user_by_id(new_admin_id).ok_or(MedalError::NotFound)?;
2676    let first_admin_id = group.admins.first().ok_or(MedalError::NotFound)?;
2677    let first_admin = conn.get_user_by_id(*first_admin_id).ok_or(MedalError::NotFound)?;
2678    if new_admin.oauth_provider == first_admin.oauth_provider {
2679        if let Some((_, new_admin_school)) = new_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2680        {
2681            if let Some((_, first_admin_school)) =
2682                first_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2683            {
2684                if new_admin_school == first_admin_school && new_admin_school.len() >= 1 {
2685                    conn.add_admin_to_group(&mut group, new_admin_id);
2686
2687                    let data = json_val::Map::new();
2688                    return Ok(data);
2689                }
2690            }
2691        }
2692    }
2693
2694    let mut data = json_val::Map::new();
2695    data.insert("reason".to_string(),
2696                to_json(&"Benutzer gehört nicht zur gleichen Schule oder Benutzer nicht als Lehrkraft angemeldet."));
2697    Err(MedalError::ErrorWithJson(data))
2698}
2699
2700pub fn group_delete_admin<T: MedalConnection>(conn: &T, group_id: i32, admin_id: i32, session_token: &str,
2701                                              csrf_token: &str)
2702                                              -> JsonValueResult {
2703    let session = conn.get_session(&session_token)
2704                      .ensure_logged_in()
2705                      .ok_or(MedalError::NotLoggedIn)?
2706                      .ensure_teacher_or_admin()
2707                      .ok_or(MedalError::AccessDenied)?;
2708
2709    if session.csrf_token != csrf_token {
2710        return Err(MedalError::CsrfCheckFailed);
2711    }
2712
2713    let mut group = conn.get_group(group_id).unwrap(); // TODO handle error
2714
2715    if !session.is_admin() {
2716        // If user is not a server admin, he must be a group admin
2717        if !group.admins.contains(&session.id) {
2718            return Err(MedalError::AccessDenied);
2719        }
2720    }
2721
2722    if group.admins.len() == 1 {
2723        let mut data = json_val::Map::new();
2724        data.insert("reason".to_string(), to_json(&"Kann letzten Admin nicht entfernen."));
2725        return Err(MedalError::ErrorWithJson(data));
2726    }
2727
2728    if !group.admins.contains(&admin_id) {
2729        let mut data = json_val::Map::new();
2730        data.insert("reason".to_string(), to_json(&"Benutzer ist kein Admin."));
2731        return Err(MedalError::ErrorWithJson(data));
2732    }
2733
2734    conn.remove_admin_from_group(&mut group, admin_id);
2735    let data = json_val::Map::new();
2736    Ok(data)
2737}
2738
2739pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str)
2740                                              -> JsonValueResult {
2741    let session = conn.get_session(&session_token)
2742                      .ensure_logged_in()
2743                      .ok_or(MedalError::NotLoggedIn)?
2744                      .ensure_teacher_or_admin()
2745                      .ok_or(MedalError::AccessDenied)?;
2746
2747    if session.csrf_token != csrf_token {
2748        return Err(MedalError::CsrfCheckFailed);
2749    }
2750
2751    let group = conn.get_group(group_id).unwrap(); // TODO handle error
2752
2753    if !session.is_admin() {
2754        // Check access for teachers
2755        if !group.admins.contains(&session.id) {
2756            return Err(MedalError::AccessDenied);
2757        }
2758    }
2759
2760    let mut data = json_val::Map::new();
2761    if conn.group_has_protected_participations(group_id) && !session.is_admin() {
2762        data.insert("reason".to_string(), to_json(&"Gruppe hat Mitglieder mit geschützten Teilnahmen."));
2763        Err(MedalError::ErrorWithJson(data))
2764    } else {
2765        conn.delete_all_users_for_group(group_id);
2766        conn.delete_group(group_id);
2767        Ok(data)
2768    }
2769}
2770
2771pub fn admin_set_data_clearance<T: MedalConnection>(conn: &T, group_id: i32, clearance_state: bool,
2772                                                    user_id: Option<i32>, session_token: &str, csrf_token: &str)
2773                                                    -> JsonValueResult {
2774    let session = conn.get_session(&session_token)
2775                      .ensure_logged_in()
2776                      .ok_or(MedalError::NotLoggedIn)?
2777                      .ensure_teacher_or_admin()
2778                      .ok_or(MedalError::AccessDenied)?;
2779
2780    if session.csrf_token != csrf_token {
2781        return Err(MedalError::CsrfCheckFailed);
2782    }
2783
2784    let group = conn.get_group(group_id).unwrap(); // TODO handle error
2785
2786    if !session.is_admin() {
2787        // Check access for teachers
2788        if !group.admins.contains(&session.id) {
2789            return Err(MedalError::AccessDenied);
2790        }
2791    }
2792
2793    if let Some(user_id) = user_id {
2794        let user = conn.get_user_by_id(user_id);
2795        if let Some(user) = user {
2796            if user.managed_by != Some(group_id) {
2797                return Err(MedalError::AccessDenied);
2798            }
2799        } else {
2800            return Err(MedalError::AccessDenied);
2801        }
2802    }
2803
2804    if let Some(user_id) = user_id {
2805        conn.set_data_clearance_for_user(user_id, clearance_state);
2806    } else {
2807        conn.set_data_clearance_for_group(group_id, clearance_state);
2808    }
2809
2810    let data = json_val::Map::new();
2811    Ok(data)
2812}
2813
2814pub fn admin_show_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2815    let session = conn.get_session(&session_token)
2816                      .ensure_logged_in()
2817                      .ok_or(MedalError::NotLoggedIn)?
2818                      .ensure_teacher_or_admin()
2819                      .ok_or(MedalError::AccessDenied)?;
2820
2821    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2822
2823    if !session.is_admin() {
2824        // Check access for teachers
2825        if !group.admins.contains(&session.id) {
2826            return Err(MedalError::AccessDenied);
2827        }
2828    }
2829
2830    let mut data = json_val::Map::new();
2831    fill_user_data(&session, &mut data);
2832
2833    let gi = GroupInfo { id: group.id.unwrap(),
2834                         name: group.name.clone(),
2835                         tag: group.tag.clone(),
2836                         code: group.groupcode.clone() };
2837
2838    data.insert("group".to_string(), to_json(&gi));
2839    data.insert("groupname".to_string(), to_json(&gi.name));
2840    data.insert("grouptag".to_string(), to_json(&gi.name));
2841
2842    Ok(("admin_edit_group".to_string(), data))
2843}
2844
2845pub fn admin_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str,
2846                                            name: String, tag: String)
2847                                            -> MedalResult<()> {
2848    let session = conn.get_session(&session_token)
2849                      .ensure_logged_in()
2850                      .ok_or(MedalError::NotLoggedIn)?
2851                      .ensure_teacher_or_admin()
2852                      .ok_or(MedalError::AccessDenied)?;
2853
2854    if session.csrf_token != csrf_token {
2855        return Err(MedalError::CsrfCheckFailed);
2856    }
2857
2858    let mut group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2859
2860    if !session.is_admin() {
2861        // Check access for teachers
2862        if !group.admins.contains(&session.id) {
2863            return Err(MedalError::AccessDenied);
2864        }
2865    }
2866
2867    group.name = name;
2868    group.tag = tag;
2869
2870    conn.save_group(&mut group);
2871
2872    Ok(())
2873}
2874
2875#[derive(Serialize, Deserialize, Debug)]
2876struct SubmissionResult {
2877    id: i32,
2878    grade: i32,
2879    date: String,
2880    autosave: bool,
2881    higher_grade: bool,
2882    latest: bool,
2883    highest_grade_latest: bool,
2884}
2885#[derive(Serialize, Deserialize, Debug)]
2886struct TaskResult {
2887    id: i32,
2888    stars: i32,
2889    submissions: Vec<SubmissionResult>,
2890}
2891#[derive(Serialize, Deserialize, Debug)]
2892struct TaskgroupResult {
2893    id: i32,
2894    name: String,
2895    tasks: Vec<TaskResult>,
2896}
2897
2898pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
2899                                                    -> MedalValueResult {
2900    let session = conn.get_session(&session_token)
2901                      .ensure_logged_in()
2902                      .ok_or(MedalError::NotLoggedIn)?
2903                      .ensure_teacher_or_admin()
2904                      .ok_or(MedalError::AccessDenied)?;
2905
2906    let user = conn.get_user_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2907    let participation = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2908
2909    let user_or_team = if let Some(team) = participation.team { team } else { user_id };
2910
2911    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2912
2913    if !session.is_admin() {
2914        // Check access for teachers
2915        if let Some(ref group) = opt_group {
2916            if !group.admins.contains(&session.id) {
2917                return Err(MedalError::AccessDenied);
2918            }
2919        } else {
2920            return Err(MedalError::AccessDenied);
2921        }
2922    }
2923
2924    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2925    let grades = conn.get_contest_user_grades(user_or_team, contest_id);
2926
2927    let mut totalgrade = 0;
2928    let mut max_totalgrade = 0;
2929
2930    for taskgroup in &contest.taskgroups {
2931        max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
2932    }
2933    for grade in grades {
2934        totalgrade += grade.grade.unwrap_or(0);
2935    }
2936
2937    #[rustfmt::skip]
2938    let subms: Vec<TaskgroupResult> =
2939        contest.taskgroups
2940            .into_iter()
2941            .map(|tg| TaskgroupResult {
2942                id: tg.id.unwrap(),
2943                name: tg.name,
2944                tasks: tg.tasks
2945                    .into_iter()
2946                    .map(|t| TaskResult {
2947                        id: t.id.unwrap(),
2948                        stars: t.stars,
2949                        submissions: conn.get_all_submissions(user_or_team, t.id.unwrap(), None)
2950                            .into_iter()
2951                            .map(|s| SubmissionResult {
2952                                id: s.id.unwrap(),
2953                                grade: s.grade,
2954                                autosave: s.autosave,
2955                                higher_grade: s.higher_grade,
2956                                latest: s.latest,
2957                                highest_grade_latest: s.highest_grade_latest,
2958                                date: self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(s.date)).unwrap(),
2959                            })
2960                            .collect(),
2961                      })
2962                      .collect(),
2963               })
2964               .collect();
2965
2966    let mut data = json_val::Map::new();
2967
2968    data.insert("submissions".to_string(), to_json(&subms));
2969    data.insert("contestid".to_string(), to_json(&contest.id));
2970    data.insert("contestname".to_string(), to_json(&contest.name));
2971    data.insert("has_timelimit".to_string(), to_json(&(contest.duration > 0)));
2972
2973    data.insert("total_grade".to_string(), to_json(&totalgrade));
2974    data.insert("max_total_grade".to_string(), to_json(&max_totalgrade));
2975
2976    if let Some(group) = opt_group {
2977        data.insert("group_id".to_string(), to_json(&group.id));
2978        data.insert("group_name".to_string(), to_json(&group.name));
2979    }
2980
2981    fill_user_data(&session, &mut data);
2982    fill_user_data_prefix(&user, &mut data, "user_");
2983    data.insert("user_id".to_string(), to_json(&user.id));
2984
2985    data.insert("start_date".to_string(),
2986                to_json(&self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(participation.start)).unwrap()));
2987
2988    data.insert("can_delete".to_string(), to_json(&(!contest.protected || session.is_admin.unwrap_or(false))));
2989    Ok(("admin_participation".to_string(), data))
2990}
2991
2992pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str,
2993                                                      csrf_token: &str)
2994                                                      -> JsonValueResult {
2995    let session = conn.get_session(&session_token)
2996                      .ensure_logged_in()
2997                      .ok_or(MedalError::NotLoggedIn)?
2998                      .ensure_teacher_or_admin()
2999                      .ok_or(MedalError::AccessDenied)?;
3000
3001    if session.csrf_token != csrf_token {
3002        return Err(MedalError::CsrfCheckFailed);
3003    }
3004
3005    let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
3006    let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
3007    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
3008
3009    if !session.is_admin() {
3010        // Check access for teachers
3011        if contest.protected {
3012            return Err(MedalError::AccessDenied);
3013        }
3014
3015        if let Some(group) = opt_group {
3016            if !group.admins.contains(&session.id) {
3017                return Err(MedalError::AccessDenied);
3018            }
3019        } else {
3020            return Err(MedalError::AccessDenied);
3021        }
3022    }
3023
3024    let mut data = json_val::Map::new();
3025    fill_user_data(&session, &mut data);
3026
3027    conn.delete_participation(user_id, contest_id);
3028    Ok(data)
3029}
3030
3031pub fn admin_show_contests<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
3032    let session = conn.get_session(&session_token)
3033                      .ensure_logged_in()
3034                      .ok_or(MedalError::NotLoggedIn)?
3035                      .ensure_admin()
3036                      .ok_or(MedalError::AccessDenied)?;
3037
3038    let mut data = json_val::Map::new();
3039    fill_user_data(&session, &mut data);
3040
3041    let mut contests: Vec<_> = conn.get_contest_list().into_iter().map(|contest| (contest.id, contest.name)).collect();
3042    contests.sort(); // Sort by id (sorts by natural order on fields)
3043    contests.reverse();
3044
3045    data.insert("contests".to_string(), to_json(&contests));
3046
3047    Ok(("admin_contests".to_string(), data))
3048}
3049
3050pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalResult<String> {
3051    conn.get_session(&session_token)
3052        .ensure_logged_in()
3053        .ok_or(MedalError::NotLoggedIn)?
3054        .ensure_admin()
3055        .ok_or(MedalError::AccessDenied)?;
3056
3057    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
3058
3059    let taskgroup_ids: Vec<(i32, String)> =
3060        contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect();
3061    let filename = format!("contest_{}__{}__{}.csv",
3062                           contest_id,
3063                           self::time::strftime("%F_%H-%M-%S", &self::time::now()).unwrap(),
3064                           helpers::make_filename_secret());
3065
3066    conn.export_contest_results_to_file(contest_id, &taskgroup_ids, &format!("./export/{}", filename));
3067
3068    Ok(filename)
3069}
3070
3071pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
3072    let session = conn.get_session(&session_token)
3073                      .ensure_logged_in()
3074                      .ok_or(MedalError::NotLoggedIn)?
3075                      .ensure_admin()
3076                      .ok_or(MedalError::AccessDenied)?;
3077
3078    let mut data = json_val::Map::new();
3079    fill_user_data(&session, &mut data);
3080
3081    let now = time::get_time();
3082    let maxage = now - time::Duration::days(30); // Count all temporary sessions with 30 days of inactivity
3083    let n_temporary_session = conn.count_temporary_sessions(maxage);
3084    data.insert("temporary_session_count".to_string(), to_json(&n_temporary_session));
3085
3086    Ok(("admin_cleanup".to_string(), data))
3087}
3088
3089pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str) -> JsonValueResult {
3090    let session = conn.get_session(&session_token)
3091                      .ensure_logged_in()
3092                      .ok_or(MedalError::NotLoggedIn)?
3093                      .ensure_admin()
3094                      .ok_or(MedalError::AccessDenied)?;
3095
3096    if session.csrf_token != csrf_token {
3097        return Err(MedalError::CsrfCheckFailed);
3098    }
3099
3100    let now = time::get_time();
3101    let maxstudentage = now - time::Duration::days(450); // Delete managed users after 450 days of inactivity
3102    let maxteacherage = now - time::Duration::days(1095); // Delete teachers after 3 years of inactivity
3103    let maxage = now - time::Duration::days(3650); // Delete every user after 10 years of inactivity
3104
3105    let result = conn.remove_old_users_and_groups(maxstudentage, Some(maxteacherage), Some(maxage));
3106
3107    let mut data = json_val::Map::new();
3108    if let Ok((n_user, n_group, n_teacher, n_other)) = result {
3109        data.insert("n_user".to_string(), to_json(&n_user));
3110        data.insert("n_group".to_string(), to_json(&n_group));
3111        data.insert("n_teacher".to_string(), to_json(&n_teacher));
3112        data.insert("n_other".to_string(), to_json(&n_other));
3113        Ok(data)
3114    } else {
3115        data.insert("reason".to_string(), to_json(&"Datenbank-Fehler."));
3116        Err(MedalError::ErrorWithJson(data))
3117    }
3118}
3119
3120pub fn do_session_cleanup<T: MedalConnection>(conn: &T) -> JsonValueResult {
3121    let now = time::get_time();
3122    let maxage = now - time::Duration::days(30); // Delete all temporary sessions after 30 days of inactivity
3123
3124    conn.remove_temporary_sessions(maxage, Some(1000));
3125
3126    let data = json_val::Map::new();
3127    Ok(data)
3128}
3129
3130pub fn housekeeping<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
3131    let session = conn.get_session(&session_token)
3132                      .ensure_logged_in()
3133                      .ok_or(MedalError::NotLoggedIn)?
3134                      .ensure_admin()
3135                      .ok_or(MedalError::AccessDenied)?;
3136
3137    let mut data = json_val::Map::new();
3138    fill_user_data(&session, &mut data);
3139    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
3140
3141    Ok(("housekeeping".to_string(), data))
3142}
3143
3144pub fn move_task_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
3145                                              new_location: &str, contest: &str)
3146                                              -> JsonValueResult {
3147    let session = conn.get_session(&session_token)
3148                      .ensure_logged_in()
3149                      .ok_or(MedalError::NotLoggedIn)?
3150                      .ensure_admin()
3151                      .ok_or(MedalError::AccessDenied)?;
3152
3153    if session.csrf_token != csrf_token {
3154        return Err(MedalError::CsrfCheckFailed);
3155    }
3156
3157    let mut data = json_val::Map::new();
3158    if old_location == new_location {
3159        data.insert("reason".to_string(), to_json(&"old and new location identical"));
3160        return Err(MedalError::ErrorWithJson(data));
3161    }
3162
3163    let status = conn.move_task_location(old_location, new_location, contest);
3164
3165    data.insert("contests_modified".to_string(), to_json(&status.is_some()));
3166
3167    Ok(data)
3168}
3169
3170pub fn move_task_contest<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
3171                                             new_location: &str, old_contest: &str, new_contest: &str)
3172                                             -> JsonValueResult {
3173    let session = conn.get_session(&session_token)
3174                      .ensure_logged_in()
3175                      .ok_or(MedalError::NotLoggedIn)?
3176                      .ensure_admin()
3177                      .ok_or(MedalError::AccessDenied)?;
3178
3179    if session.csrf_token != csrf_token {
3180        return Err(MedalError::CsrfCheckFailed);
3181    }
3182
3183    let mut data = json_val::Map::new();
3184    if old_contest == new_contest && old_location == new_location {
3185        data.insert("reason".to_string(), to_json(&"old and new location identical"));
3186        return Err(MedalError::ErrorWithJson(data));
3187    }
3188
3189    let status = conn.move_task_contest(old_location, new_location, old_contest, new_contest);
3190
3191    data.insert("contests_modified".to_string(), to_json(&status.is_some()));
3192
3193    Ok(data)
3194}
3195
3196pub fn move_contest_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
3197                                                 old_location: &str, new_location: &str)
3198                                                 -> JsonValueResult {
3199    let session = conn.get_session(&session_token)
3200                      .ensure_logged_in()
3201                      .ok_or(MedalError::NotLoggedIn)?
3202                      .ensure_admin()
3203                      .ok_or(MedalError::AccessDenied)?;
3204
3205    if session.csrf_token != csrf_token {
3206        return Err(MedalError::CsrfCheckFailed);
3207    }
3208
3209    let mut data = json_val::Map::new();
3210    if old_location == new_location {
3211        data.insert("reason".to_string(), to_json(&"old and new location identical"));
3212        return Err(MedalError::ErrorWithJson(data));
3213    }
3214
3215    let status = conn.move_contest_location(old_location, new_location);
3216
3217    data.insert("contests_modified".to_string(), to_json(&status.is_some()));
3218
3219    Ok(data)
3220}
3221
3222pub fn move_task_submissions_grades<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
3223                                                        old_location: &str, new_location: &str, old_contest: &str,
3224                                                        new_contest: &str)
3225                                                        -> JsonValueResult {
3226    let session = conn.get_session(&session_token)
3227                      .ensure_logged_in()
3228                      .ok_or(MedalError::NotLoggedIn)?
3229                      .ensure_admin()
3230                      .ok_or(MedalError::AccessDenied)?;
3231
3232    if session.csrf_token != csrf_token {
3233        return Err(MedalError::CsrfCheckFailed);
3234    }
3235
3236    let mut data = json_val::Map::new();
3237    if old_location == new_location {
3238        data.insert("reason".to_string(), to_json(&"old and new location identical"));
3239        return Err(MedalError::ErrorWithJson(data));
3240    }
3241
3242    let status = conn.move_task_submissions_grades(old_location, new_location, old_contest, new_contest);
3243
3244    data.insert("tasks_modified".to_string(), to_json(&status.is_some()));
3245
3246    Ok(data)
3247}
3248
3249#[derive(PartialEq, Eq, Clone, Copy)]
3250pub enum UserType {
3251    User,
3252    Teacher,
3253    Admin,
3254}
3255
3256pub enum UserSex {
3257    Female,
3258    Male,
3259    Unknown,
3260}
3261
3262pub struct ForeignUserData {
3263    pub foreign_id: String,
3264    pub foreign_type: UserType,
3265    pub sex: UserSex,
3266    pub firstname: String,
3267    pub lastname: String,
3268    pub school_name: Option<String>,
3269}
3270
3271pub fn login_oauth<T: MedalConnection>(conn: &T, user_data: ForeignUserData, oauth_provider_id: String,
3272                                       autoclean_submissions: bool)
3273                                       -> Result<(String, bool), (String, json_val::Map<String, json_val::Value>)> {
3274    match conn.login_foreign(None,
3275                             &oauth_provider_id,
3276                             &user_data.foreign_id,
3277                             (user_data.foreign_type != UserType::User,
3278                              user_data.foreign_type == UserType::Admin,
3279                              &user_data.firstname,
3280                              &user_data.lastname,
3281                              match user_data.sex {
3282                                  UserSex::Male => Some(1),
3283                                  UserSex::Female => Some(2),
3284                                  UserSex::Unknown => Some(0),
3285                              },
3286                              &user_data.school_name))
3287    {
3288        Ok((session_token, last_activity)) => {
3289            let redirect_profile = if let Some(last_activity) = last_activity {
3290                let now = time::get_time();
3291                now - last_activity > time::Duration::days(60)
3292            } else {
3293                true
3294            };
3295
3296            // While we're at it, lets also remove some old sessions! OAuth took
3297            // some time anyway, so we can afford spending some additional
3298            // milliseconds …
3299            let now = time::get_time();
3300            let maxage = now - time::Duration::days(30);
3301            conn.remove_temporary_sessions(maxage, None); // Delete all temporary sessions after 30 days of inactivity
3302
3303            if autoclean_submissions {
3304                let maxage = now - time::Duration::days(30);
3305                conn.remove_autosaved_submissions(maxage, None); // Delete all autosaved submissions after 30 days
3306
3307                let maxage = now - time::Duration::days(90);
3308                conn.remove_all_but_latest_submissions(maxage, None); // Delete all but latest submissions after 90 days
3309            }
3310
3311            Ok((session_token, redirect_profile))
3312        }
3313        Err(()) => {
3314            let mut data = json_val::Map::new();
3315            data.insert("reason".to_string(), to_json(&"OAuth-Login failed.".to_string()));
3316            Err(("login".to_owned(), data))
3317        }
3318    }
3319}