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