1use 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 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 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 return None;
508 }
509
510 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 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 if required_contest.required_stars != 0
528 && session.grade >= required_contest.min_grade
529 && session.grade <= required_contest.max_grade
530 {
531 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 }
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 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 if let Some(team) = participation.team {
783 if team != session.id {
785 if time_info.is_time_left {
786 data.insert("block_team_participation".to_string(), to_json(&true));
788 } else {
789 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 let session = conn.get_session_or_new(&session_token).unwrap();
992 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
994
995 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 if session.is_logged_in() && session.csrf_token != csrf_token {
1005 return Err(MedalError::CsrfCheckFailed);
1006 }
1007
1008 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), Some(max_teamsize) => {
1029 if max_teamsize < 2 {
1030 return Err(MedalError::AccessDenied); }
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); }
1052
1053 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), };
1062 } else {
1063 return Ok(Err("invalid_logincode".to_string()));
1064 }
1065 }
1066
1067 match conn.new_participation(session.id, contest_id, None) {
1069 Ok(_) => Ok(Ok(())),
1070 _ => Err(MedalError::AccessDenied), }
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 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 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(®istration).unwrap();
1172 let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1173
1174 let passkey = webauthn.finish_passkey_registration(&credential, ®istration);
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 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 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 return Err(MedalError::AccessDenied);
1334 }
1335 } else {
1336 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 }
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 let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1392
1393 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(), latest: Default::default(), highest_grade_latest: Default::default(), 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()); let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; 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))); 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 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 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 let grade = Grade { taskgroup: tg.id.unwrap(),
1544 user: session.id,
1545 grade: Some(submission.grade),
1546 validated: submission.validated };
1547
1548 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 return Err(MedalError::AccessDenied);
1554 }
1555 } else {
1556 return Err(MedalError::AccessDenied);
1558 }
1559 }
1560
1561 let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); 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("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("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 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(); 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
1851pub 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))?; 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))?; 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 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 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 }
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 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 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 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 Some(user_id) => {
2135 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); }
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 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 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 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 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 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 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 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 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(); if !session.is_admin() {
2685 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(); if !session.is_admin() {
2769 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(); if !session.is_admin() {
2822 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(); if !session.is_admin() {
2860 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(); if !session.is_admin() {
2893 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(); if !session.is_admin() {
2930 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(); if !session.is_admin() {
2968 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 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 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 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 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(); 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); 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); let maxteacherage = now - time::Duration::days(1095); let maxage = now - time::Duration::days(3650); 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); 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 let now = time::get_time();
3492 let maxage = now - time::Duration::days(30);
3493 conn.remove_temporary_sessions(maxage, None); if autoclean_submissions {
3496 let maxage = now - time::Duration::days(30);
3497 conn.remove_autosaved_submissions(maxage, None); let maxage = now - time::Duration::days(90);
3500 conn.remove_all_but_latest_submissions(maxage, None); }
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}