Skip to main content

medal/
core.rs

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