diff --git a/backend/src/models/email_verification_request.rs b/backend/src/models/email_verification_request.rs index ab56eb568..41ffac2df 100644 --- a/backend/src/models/email_verification_request.rs +++ b/backend/src/models/email_verification_request.rs @@ -11,7 +11,11 @@ use uuid::Uuid; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case", tag = "name", content = "data")] pub enum EmailVerificationAction { - ConfirmEmailAddress { user_information: UserInformation }, + ConfirmEmailAddress { + user_information: UserInformation, + #[serde(default)] + callback: Option, + }, ResetPassword, RegisterTeam { organization: String }, } @@ -122,6 +126,7 @@ mod tests { name: NAME.to_string(), ..Default::default() }, + callback: None, }; let (name, data) = action.get(); assert_eq!(name, "confirm_email_address"); @@ -135,6 +140,7 @@ mod tests { name: NAME.to_string(), ..Default::default() }, + callback: Some("/panel".to_string()), }; let (action_type, additional_data) = action.get(); @@ -147,8 +153,9 @@ mod tests { let restored = model.get_action().unwrap(); match restored { - EmailVerificationAction::ConfirmEmailAddress { user_information } => { + EmailVerificationAction::ConfirmEmailAddress { user_information, callback } => { assert_eq!(NAME, user_information.name); + assert_eq!(callback, Some("/panel".to_string())); } _ => panic!("Unexpected variant"), } diff --git a/backend/src/routes/auth/confirm.rs b/backend/src/routes/auth/confirm.rs index 195e0dd04..d3cd32297 100644 --- a/backend/src/routes/auth/confirm.rs +++ b/backend/src/routes/auth/confirm.rs @@ -21,11 +21,15 @@ pub async fn confirm_email( confirmation_code: web::Path, ) -> Result { match AuthenticationService::confirm_email(&app_state, confirmation_code.into_inner()).await { - Ok(()) => { - let url = EnvConfig::get() + Ok(callback) => { + let mut url = EnvConfig::get() .frontend_url .join("/login?redirect_from_confirmation=true")?; + if let Some(callback) = callback { + url.query_pairs_mut().append_pair("callback", &callback); + } + let mut response = common_responses::create_redirect_response(url)?; Ok(response.body("Email successfully confirmed. Redirecting...")) diff --git a/backend/src/routes/auth/register.rs b/backend/src/routes/auth/register.rs index cf9738afb..7e1741893 100644 --- a/backend/src/routes/auth/register.rs +++ b/backend/src/routes/auth/register.rs @@ -3,6 +3,7 @@ use crate::models::user::validate_name_chars; use crate::services::authentication::AuthenticationService; use crate::utils::app_state; use crate::utils::error::Error; +use crate::utils::validation::validate_callback; use actix_web::web::Json; use actix_web::{HttpResponse, post, web}; use actix_web_validation::Validated; @@ -20,6 +21,8 @@ pub struct RegisterModel { pub email: String, #[validate(length(min = 8, max = 32))] pub password: Password, + #[validate(length(max = 256), custom(function = "validate_callback"))] + pub callback: Option, } #[utoipa::path( diff --git a/backend/src/services/authentication.rs b/backend/src/services/authentication.rs index bf213463c..203fb0982 100644 --- a/backend/src/services/authentication.rs +++ b/backend/src/services/authentication.rs @@ -44,7 +44,10 @@ impl AuthenticationService { let confirmation_code = email_verification_request::Model::create( &app_state.database, - EmailVerificationAction::ConfirmEmailAddress { user_information }, + EmailVerificationAction::ConfirmEmailAddress { + user_information, + callback: credentials.callback.clone(), + }, credentials.email.clone(), Some(Duration::minutes(30)), ) @@ -119,14 +122,16 @@ impl AuthenticationService { pub async fn confirm_email( app_state: &app_state::AppState, confirmation_code: Uuid, - ) -> Result<(), Error> { + ) -> Result, Error> { let email_confirmation = email_verification_request::Model::find_and_verify( &app_state.database, confirmation_code, ) .await?; - let EmailVerificationAction::ConfirmEmailAddress { user_information } = - email_confirmation.get_action()? + let EmailVerificationAction::ConfirmEmailAddress { + user_information, + callback, + } = email_confirmation.get_action()? else { return Err(Error::InvalidEmailConfirmationCode); }; @@ -135,7 +140,7 @@ impl AuthenticationService { email_confirmation.delete(&app_state.database).await?; - Ok(()) + Ok(callback) } fn create_email_confirmation_link(confirmation_code: &str) -> Result { diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs index e014812fa..d8a8a8abb 100644 --- a/backend/src/utils/mod.rs +++ b/backend/src/utils/mod.rs @@ -15,3 +15,4 @@ pub mod qr_code; pub mod real_ip; pub mod sse_event; pub mod success_response; +pub mod validation; diff --git a/backend/src/utils/validation.rs b/backend/src/utils/validation.rs new file mode 100644 index 000000000..5447718f7 --- /dev/null +++ b/backend/src/utils/validation.rs @@ -0,0 +1,9 @@ +use validator::ValidationError; + +pub fn validate_callback(callback: &str) -> Result<(), ValidationError> { + if callback.starts_with('/') { + return Ok(()); + } + Err(ValidationError::new("invalid_callback") + .with_message("Callback URL must start with '/'".into())) +} diff --git a/backend/tests/routes/auth.rs b/backend/tests/routes/auth.rs index d3fc9f202..1735abc8b 100644 --- a/backend/tests/routes/auth.rs +++ b/backend/tests/routes/auth.rs @@ -144,7 +144,6 @@ async fn email_confirmation_success() { use sea_orm::EntityTrait; let test_database = TestDatabase::new().await; - let email_confirmation = test_database .with_email_verification_request(UpdatableModel::default()) .await; @@ -241,3 +240,123 @@ async fn reset_password_flow() { let response = test::call_service(&app, request).await; assert_eq!(response.status(), 200); } + +#[actix_web::test] +async fn email_confirmation_with_callback() { + use chrono::Utc; + use hack4krak_backend::entities::email_verification_request; + use sea_orm::{EntityTrait, Set}; + use uuid::Uuid; + + let test_database = TestDatabase::new().await; + + let confirmation_code = Uuid::new_v4(); + let email_confirmation = email_verification_request::ActiveModel { + id: Set(confirmation_code), + email: Set("".to_string()), + action_type: Set("confirm_email_address".to_string()), + additional_data: Set(Some(json!({ + "user_information": { + "name": "test_user", + "email": "example@gmail.com", + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$nTzWdmrtGEOnwCocrg76xg$yv16FfDT5+meKwPmSiV+MF9kP8Man6bXZs+BloFTKIk" + }, + "callback": "/tasks" + }))), + expiration_time: Set(Some(Utc::now().naive_utc() + chrono::Duration::minutes(30))), + created_at: Set(Utc::now().naive_utc()), + }; + email_verification_request::Entity::insert(email_confirmation) + .exec(&test_database.database) + .await + .unwrap(); + + let app = TestApp::default() + .with_database(test_database) + .build_app() + .await; + + let path = format!("/auth/confirm/{confirmation_code}"); + let request = test::TestRequest::get().uri(&path).to_request(); + let response = test::call_service(&app, request).await; + assert!(response.status().is_success()); + + let refresh_header = response + .headers() + .get("Refresh") + .unwrap() + .to_str() + .unwrap() + .to_string(); + assert!( + refresh_header.contains("callback=%2Ftasks"), + "Redirect should contain callback parameter, got: {refresh_header}" + ); +} + +#[cfg(feature = "full-test-suite")] +#[actix_web::test] +async fn register_with_callback_persists_to_confirmation() { + use crate::test_utils::mail::SmtpTestClient; + + let test_database = TestDatabase::new().await; + let smtp_client = SmtpTestClient::new().await; + let app = TestApp::default() + .with_database(test_database) + .with_smtp_client(smtp_client.smtp_client.clone()) + .build_app() + .await; + + let request = test::TestRequest::post() + .uri("/auth/register") + .set_json(json!({ + "email": "test@example.com", + "name": "test_user", + "first_name": "Test", + "password": "password123", + "callback": "/panel/tasks" + })) + .to_request(); + let response = test::call_service(&app, request).await; + assert!(response.status().is_success()); + + let confirmation_code = smtp_client.find_uuid_in_first_email().await; + let request = test::TestRequest::get() + .uri(&format!("/auth/confirm/{confirmation_code}")) + .to_request(); + let response = test::call_service(&app, request).await; + assert!(response.status().is_success()); + + let refresh_header = response + .headers() + .get("Refresh") + .unwrap() + .to_str() + .unwrap() + .to_string(); + assert!( + refresh_header.contains("callback=%2Fpanel%2Ftasks"), + "Callback should persist from registration to confirmation redirect, got: {refresh_header}" + ); +} + +#[actix_web::test] +async fn register_with_invalid_callback_rejected() { + let app = TestApp::default().build_app().await; + + let request = test::TestRequest::post() + .uri("/auth/register") + .set_json(json!({ + "email": "test@example.com", + "name": "test_user", + "first_name": "Test", + "password": "password123", + "callback": "https://evil.com" + })) + .to_request(); + let response = test::call_service(&app, request).await; + assert!( + response.status().is_client_error(), + "Callback with absolute URL should be rejected" + ); +} diff --git a/frontend/app/components/AuthForm.vue b/frontend/app/components/AuthForm.vue index d6a0868ef..d70367b41 100644 --- a/frontend/app/components/AuthForm.vue +++ b/frontend/app/components/AuthForm.vue @@ -26,6 +26,10 @@ const { proxy } = useScriptUmamiAnalytics() const OAuthBaseUrl = `${useRuntimeConfig().public.openFetch.api.baseURL}/auth/oauth` const route = useRoute() +const callback = computed(() => { + const value = route.query.callback?.toString() + return value?.startsWith('/') ? value : undefined +}) if (route.query.redirect_from_confirmation === 'true' && import.meta.client) { toast.add({ @@ -50,10 +54,14 @@ async function onSubmit(event: Schema) { toast.add({ title: 'Oczekiwanie', description: 'Wysyłanie emaila…', color: 'info' }) } + const body = props.isLogin + ? event + : { ...event, callback: callback.value } + await useNuxtApp().$api(address, { method: 'POST', credentials: 'include', - body: event, + body, }) if (props.isLogin) { @@ -61,13 +69,13 @@ async function onSubmit(event: Schema) { method: 'email', }) toast.add({ title: 'Sukces', description: 'Pomyślnie zalogowano!', color: 'success' }) - await navigateTo('/panel/event') + await navigateTo(callback.value || '/panel/event') } else { proxy.track('account_registration_success', { method: 'email', }) toast.add({ title: 'Sukces', description: 'Pomyślnie zarejestrowano! Wysłaliśmy Ci na podany adres email link do aktywacji konta', color: 'success' }) - await navigateTo('/login') + await navigateTo({ path: '/login', query: callback.value ? { callback: callback.value } : undefined }) } } catch (error) { proxy.track(props.isLogin ? 'account_login_error' : 'account_registration_error', { @@ -108,7 +116,7 @@ function trackOAuth(provider: 'google' | 'github') {
{{ isLogin ? 'Nie masz konta?' : 'Masz już konto?' }} - + {{ isLogin ? 'Załóż je' : 'Zaloguj się' }} diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index 641f71c2d..f5774e7cc 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -5,20 +5,24 @@ export default defineNuxtRouteMiddleware(async (to) => { redirect: 'error', }) if (error.value || !data.value) { - return '/login' + return await navigateTo({ name: 'login', query: { callback: to.fullPath } }) } if (to.path.startsWith('/panel')) { if (data.value.has_completed_onboarding === false) { - return '/account/onboarding' + return await navigateTo({ path: '/account/onboarding', query: { callback: to.fullPath } }) } } if (to.path === '/account/onboarding') { if (data.value.has_completed_onboarding === true) { + const callback = to.query.callback?.toString() + if (callback?.startsWith('/')) { + return callback + } return '/panel' } } } catch { - return '/login' + return await navigateTo({ name: 'login', query: { callback: to.fullPath } }) } } }) diff --git a/frontend/app/middleware/guest.ts b/frontend/app/middleware/guest.ts index 75395881b..bae4913db 100644 --- a/frontend/app/middleware/guest.ts +++ b/frontend/app/middleware/guest.ts @@ -1,10 +1,11 @@ -export default defineNuxtRouteMiddleware(async () => { +export default defineNuxtRouteMiddleware(async (to) => { try { const user = await useAuth('/account/', { redirect: 'error', }) if (!user.error.value && user.data.value) { - return '/account' + const callback = to.query.callback?.toString() + return callback?.startsWith('/') ? callback : '/account' } } catch (error) { console.error(error) diff --git a/frontend/app/pages/account/onboarding.vue b/frontend/app/pages/account/onboarding.vue index 2b4d78434..a97d459c0 100644 --- a/frontend/app/pages/account/onboarding.vue +++ b/frontend/app/pages/account/onboarding.vue @@ -14,6 +14,11 @@ const STEP_TITLES = ['Powitanie', 'Pytania profilujące', 'Jak nas znalazłeś?' const stepIndex = ref(0) const direction = ref<'forward' | 'backward'>('forward') const submitting = ref(false) +const route = useRoute() +const callback = computed(() => { + const value = route.query.callback?.toString() + return value?.startsWith('/') ? value : undefined +}) const form = reactive({ organization: '', @@ -73,7 +78,7 @@ async function onSubmit() { }) await refreshNuxtData() - await navigateTo('/account/events') + await navigateTo(callback.value || '/account/events') } catch (error) { if (!(error instanceof FetchError)) { throw error diff --git a/frontend/openapi/api/openapi.json b/frontend/openapi/api/openapi.json index 7ca6f320e..34b42d633 100644 --- a/frontend/openapi/api/openapi.json +++ b/frontend/openapi/api/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Hack4Krak API Documentation","description":"","license":{"name":"GPL-3.0","url":"https://www.gnu.org/licenses/gpl-3.0.en.html"},"version":"2.2.0"},"servers":[{"url":"http://localhost:8080/"}],"paths":{"/":{"get":{"operationId":"index","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseData"}}}}}}},"/account/":{"get":{"tags":["account"],"operationId":"account_index","responses":{"200":{"description":"Use information received.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInformationResponse"}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/delete":{"delete":{"tags":["account"],"operationId":"delete","responses":{"200":{"description":"Account successfully deleted"},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/identification":{"get":{"tags":["account"],"operationId":"account_identification","responses":{"200":{"description":"Identification information received.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserIdentificationInfo"}}}},"401":{"description":"Unauthorized."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/onboarding":{"get":{"tags":["account"],"operationId":"get_onboarding","responses":{"200":{"description":"User onboarding answers received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"post":{"tags":["account"],"operationId":"submit_onboarding","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOnboardingSubmissionRequest"}}},"required":true},"responses":{"200":{"description":"User onboarding answers submitted."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/update":{"patch":{"tags":["account"],"operationId":"update","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserModel"}}},"required":true},"responses":{"200":{"description":"Account updated successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":[]}]}},"/account/update/password":{"patch":{"tags":["account"],"operationId":"change_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordModel"}}},"required":true},"responses":{"200":{"description":"Account updated successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":[]}]}},"/admin/":{"get":{"tags":["admin"],"operationId":"admin-index","responses":{"200":{"description":"Use information received."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/announcements/":{"get":{"tags":["admin/announcements"],"operationId":"admin-announcements-list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"action_type","in":"query","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"List of announcements.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}}},"security":[{"access_token":[]}]},"post":{"tags":["admin/announcements"],"operationId":"admin-announcements-create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementAction"}}},"required":true},"responses":{"201":{"description":"Announcement created successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}},"security":[{"access_token":[]}]}},"/admin/email/send_external_registration_form":{"post":{"tags":["admin/email"],"operationId":"send_external_registration_form","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalRegistrationFormModel"}}},"required":true},"responses":{"200":{"description":"Confirmation code successfully generated."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/email/send_informational":{"post":{"tags":["admin/email"],"operationId":"send_informational","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSendingModel"}}},"required":true},"responses":{"200":{"description":"Email successfully sent"},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/identification/apply-tag/{id}":{"post":{"tags":["admin/identification"],"operationId":"admin_identification_apply_tag","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyTagRequest"}}},"required":true},"responses":{"200":{"description":"Tag successfully applied."},"404":{"description":"User or tag not found."},"409":{"description":"User already has this tag applied."},"500":{"description":"Internal server error."}}}},"/admin/identification/identify/{id}":{"get":{"tags":["admin/identification"],"operationId":"admin_identification_identify","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User successfully identified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifiedUserInfo"}}}},"404":{"description":"No user found with the given identification UUID."},"500":{"description":"Internal server error."}}}},"/admin/identification/reset/{user_id}":{"post":{"tags":["admin/identification"],"operationId":"admin_identification_reset_identification_code","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Identification code successfully reset and email sent."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}}}},"/admin/tasks/refresh":{"post":{"tags":["admin/tasks"],"operationId":"admin_tasks_refresh","responses":{"200":{"description":"Tasks successfully refreshed."},"500":{"description":"Internal server error."}}}},"/admin/teams/delete/{id}":{"delete":{"tags":["admin/teams"],"operationId":"admin_teams_delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Team successfully deleted."},"500":{"description":"Internal server error."}}}},"/admin/teams/list":{"get":{"tags":["admin/teams"],"operationId":"admin_teams_list","responses":{"200":{"description":"Teams successfully fetched.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamWithMembers"}}}}},"500":{"description":"Internal server error."}}}},"/admin/teams/update/{id}":{"patch":{"tags":["admin/teams"],"operationId":"admin_teams_update","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatableModel"}}},"required":true},"responses":{"200":{"description":"Team successfully updated."},"500":{"description":"Internal server error."}}}},"/admin/users/delete/{id}":{"delete":{"tags":["admin/users"],"operationId":"admin_users_delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User successfully deleted"},"403":{"description":"User must have higher role than updated user."},"500":{"description":"Internal server error."}}}},"/admin/users/email_confirmations":{"get":{"tags":["admin/users"],"operationId":"email_confirmations_list","responses":{"200":{"description":"Email confirmations successfully fetched.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}},"500":{"description":"Internal server error."}}}},"/admin/users/list":{"get":{"tags":["admin/users"],"operationId":"admin_users_list","responses":{"200":{"description":"User list successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}},"500":{"description":"Internal server error."}}}},"/admin/users/onboarding/{id}":{"get":{"tags":["admin/users"],"operationId":"admin_get_onboarding","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User onboarding answers received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/users/update/{id}":{"patch":{"tags":["admin/users"],"operationId":"admin_users_update","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatableModel"}}},"required":true},"responses":{"200":{"description":"User successfully updated."},"403":{"description":"User must have higher role than updated user."},"500":{"description":"Internal server error."}}}},"/announcements/":{"get":{"tags":["announcements"],"operationId":"list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"action_type","in":"query","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"List of announcements.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}}}}},"/announcements/latest":{"get":{"tags":["announcements"],"operationId":"latest","responses":{"200":{"description":"The latest announcement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementResponse"}}}},"404":{"description":"No announcements found."}}}},"/auth/confirm/{confirmation_code}":{"get":{"tags":["auth"],"operationId":"confirm_email","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Email successfully confirmed. Redirecting..."},"307":{"description":"Invalid confirmation code."},"500":{"description":"Internal server error."}}}},"/auth/login":{"post":{"tags":["auth"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginModel"}}},"required":true},"responses":{"200":{"description":"User successfully logged in."},"401":{"description":"Invalid credentials."},"500":{"description":"Internal server error."}}}},"/auth/logout":{"post":{"tags":["auth"],"operationId":"logout","responses":{"200":{"description":"User successfully logged out."}}}},"/auth/oauth/github":{"get":{"tags":["auth/oauth"],"operationId":"github","responses":{"200":{"description":"Redirects to GitHub for OAuth authorization"}}}},"/auth/oauth/github/callback":{"get":{"tags":["auth/oauth"],"operationId":"github_callback","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth2 flow completed successfully"},"307":{"description":"Invalid credentials"},"500":{"description":"Internal server errors."}}}},"/auth/oauth/google":{"get":{"tags":["auth/oauth"],"operationId":"google","responses":{"200":{"description":"Redirects to Google for OAuth authorization"}}}},"/auth/oauth/google/callback":{"get":{"tags":["auth/oauth"],"operationId":"google_callback","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth2 flow completed successfully"},"307":{"description":"Invalid credentials"},"500":{"description":"Internal server errors."}}}},"/auth/refresh":{"post":{"tags":["auth"],"operationId":"refresh","responses":{"200":{"description":"New tokens are set as cookies"},"401":{"description":"Invalid credentials."}},"security":[{"access_token":[]}]}},"/auth/register":{"post":{"tags":["auth"],"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterModel"}}},"required":true},"responses":{"200":{"description":"User successfully registered."},"400":{"description":"User already registered."},"500":{"description":"Internal server error."}}}},"/auth/request_reset_password":{"post":{"tags":["auth"],"operationId":"request_reset_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResetPasswordModel"}}},"required":true},"responses":{"200":{"description":"Password reset email sent."}}}},"/auth/reset_password":{"patch":{"tags":["auth"],"operationId":"reset_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordModel"}}},"required":true},"responses":{"200":{"description":"Password successfully reset."},"400":{"description":"Invalid reset code."},"500":{"description":"Internal server error."}}}},"/auth/status":{"get":{"tags":["auth"],"operationId":"auth-status","responses":{"200":{"description":"User is logged in"},"401":{"description":"User is not logged in."}},"security":[{"access_token":[]}]}},"/event/info":{"get":{"tags":["event"],"operationId":"info","responses":{"200":{"description":"Correctly returned event config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInfoResponse"}}}}}}},"/event/label/{label_id}":{"get":{"operationId":"label","parameters":[{"name":"label_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned icon image.","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid label id."},"404":{"description":"Label not found."},"500":{"description":"Internal server error."}}}},"/event/participant-tags":{"get":{"tags":["event"],"operationId":"event_participant_tags","responses":{"200":{"description":"Successfully returned participant tags list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}}}}},"500":{"description":"Internal server error."}}}},"/event/participate":{"get":{"tags":["event"],"operationId":"get_participation","responses":{"200":{"description":"Registration data returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParticipateResponse"}}}},"404":{"description":"User has not registered yet."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"post":{"tags":["event"],"operationId":"submit_participation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParticipateRequest"}}},"required":true},"responses":{"200":{"description":"Registration submitted successfully."},"400":{"description":"Registration period is not open."},"409":{"description":"User is already registered."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"delete":{"tags":["event"],"operationId":"delete_participation","responses":{"200":{"description":"Registration cancelled successfully."},"404":{"description":"User has not registered yet."},"409":{"description":"User must leave or delete their team first."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/event/registration":{"get":{"tags":["event"],"operationId":"registration","responses":{"200":{"description":"Correctly returned registration config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationResponse"}}}}}}},"/event/status":{"get":{"tags":["event"],"operationId":"status","responses":{"200":{"description":"Event status returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventStatusResponse"}}}},"500":{"description":"Internal server error."}}}},"/flag/submit":{"post":{"tags":["flag"],"operationId":"submit","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitModel"}}},"required":true},"responses":{"200":{"description":"Correctly submitted flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitResponse"}}}},"400":{"description":"Invalid flag"},"403":{"description":"Team is not verified for flag submission"},"409":{"description":"Flag already submitted by the team"}}}},"/leaderboard/chart":{"get":{"tags":["leaderboard"],"operationId":"chart","responses":{"200":{"description":"Successfully retrieved chart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardChart"}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/ctftime/capture-log":{"get":{"tags":["leaderboard"],"operationId":"capture_log","parameters":[{"name":"lastId","in":"query","description":"ID of the last event received","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Successfully retrieved capture log.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CaptureLogEvent"}}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/ctftime/team-standings":{"get":{"tags":["leaderboard"],"operationId":"team_standings","responses":{"200":{"description":"Successfully retrieved information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamStandings"}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/teams":{"get":{"tags":["leaderboard"],"operationId":"teams","responses":{"200":{"description":"Successfully retrieved leaderboard","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamRanking"}}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/teams_with_tasks":{"get":{"tags":["leaderboard"],"operationId":"teams_with_tasks","responses":{"200":{"description":"Successfully retrieved leaderboard","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamRankingWithTasks"}}}}},"500":{"description":"Internal server error"}}}},"/metrics":{"get":{"operationId":"metrics","responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/tasks/assets/get/{task_id}/{asset_path}":{"get":{"tags":["task/assets"],"operationId":"get","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"asset_path","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of task assets.","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Asset does not exist."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/tasks/assets/list/{task_id}":{"get":{"tags":["task/assets"],"operationId":"task_assets_list","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of task assets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TaskAsset"}}}}},"404":{"description":"Task does not exist."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/tasks/count":{"get":{"tags":["tasks"],"operationId":"count","responses":{"200":{"description":"Correctly returned the number of tasks.","content":{"text/plain":{"schema":{"type":"integer","format":"int32","minimum":0}}}}}}},"/tasks/description/{task_id}":{"get":{"tags":["tasks"],"operationId":"description","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task description","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/icon/{task_id}":{"get":{"tags":["tasks"],"operationId":"icon","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned icon image.","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/labels":{"get":{"tags":["tasks"],"operationId":"task_labels","responses":{"200":{"description":"Successfully returned task labels list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Label"}}}}},"500":{"description":"Internal server error."}}}},"/tasks/list":{"get":{"tags":["tasks"],"operationId":"task_list","responses":{"200":{"description":"Tasks successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TaskWithStatus"}}}}},"500":{"description":"Internal server error."}}}},"/tasks/name/{task_id}":{"get":{"tags":["tasks"],"operationId":"name","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task name","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."}}}},"/tasks/solution/{task_id}":{"get":{"tags":["tasks"],"operationId":"solution","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task solution","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/task-status/updates":{"get":{"tags":["announcements"],"operationId":"updates","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}}],"responses":{"200":{"description":"List of task status updates.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}}}}},"/teams/create":{"post":{"tags":["teams"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamModel"}}},"required":true},"responses":{"200":{"description":"Team successfully created."},"403":{"description":"User already belongs to team."},"409":{"description":"Team already exists."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/external_invitations/create/{confirmation_code}":{"post":{"tags":["teams/external_invitations"],"description":"Create external team invitations using secret token","operationId":"create_external","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExternalTeamModel"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"array","items":{"type":"string"}}}}}},"400":{"description":""},"500":{"description":""}},"security":[{"access_token":[]}]}},"/teams/external_invitations/info/{code}":{"get":{"tags":["teams/external_invitations"],"description":"Create external team invitations using secret token","operationId":"external_invitations_info","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalInvitationsInfo"}}}},"400":{"description":""},"500":{"description":""}},"security":[{"access_token":[]}]}},"/teams/external_invitations/join":{"post":{"tags":["teams/external_invitations"],"operationId":"join_external","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinExternalModel"}}},"required":true},"responses":{"200":{"description":"Successfully joined team."},"403":{"description":"User already belongs to team."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/invitations/":{"get":{"tags":["teams/invitations"],"operationId":"get_invitations","responses":{"200":{"description":"Successfully retrieved invitations","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"User doesn't have any invitations."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/invitations/accept_invitation/{team_name}":{"post":{"tags":["teams/invitations"],"operationId":"accept_invitation","parameters":[{"name":"team_name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successfully joined team."},"403":{"description":"User already belongs to team."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/":{"get":{"tags":["teams/management"],"operationId":"teams_management_index","responses":{"200":{"description":"User is team leader."},"500":{"description":"Internal server error."}},"security":[{"access_token":["leader"]}]}},"/teams/management/change_leader":{"patch":{"tags":["teams/management"],"operationId":"change_leader","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeLeaderModel"}}},"required":true},"responses":{"200":{"description":"Team name successfully changed."},"403":{"description":"User is not the leader of any team."},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/delete":{"delete":{"tags":["teams/management"],"operationId":"delete_team","responses":{"200":{"description":"Team deleted successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":["leader"]}]}},"/teams/management/invite_user":{"post":{"tags":["teams/management"],"operationId":"invite_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUserModel"}}},"required":true},"responses":{"200":{"description":"User successfully invited to team."},"403":{"description":"User already belongs to team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":["leader"]}]}},"/teams/management/kick_user":{"delete":{"tags":["teams/management"],"operationId":"kick_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveUserModel"}}},"required":true},"responses":{"200":{"description":"User successfully removed from team."},"403":{"description":"User is not a member of the team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/rename":{"patch":{"tags":["teams/management"],"operationId":"rename","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeNameModel"}}},"required":true},"responses":{"200":{"description":"Team name successfully changed."},"403":{"description":"User is not the leader of any team."},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/revoke_invitation/{username}":{"delete":{"tags":["teams/management"],"operationId":"revoke_invitation","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invitation successfully revoked."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/completed_tasks":{"get":{"tags":["teams/membership"],"operationId":"completed_tasks","responses":{"200":{"description":"Successfully retrieved completed tasks.","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"403":{"description":"User doesn't belong to any team."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/leave_team":{"delete":{"tags":["teams/membership"],"operationId":"leave_team","responses":{"200":{"description":"User successfully left team."},"403":{"description":"User is not a member of the team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/my_team":{"get":{"tags":["teams/membership"],"operationId":"my_team","responses":{"200":{"description":"Team successfully retrieved.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MyTeamWithMembers"}]}}}},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/stats":{"get":{"tags":["teams/membership"],"operationId":"stats","responses":{"200":{"description":"Team successfully retrieved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamStats"}}}},"403":{"description":"User doesn't belong to any team."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}}},"components":{"schemas":{"AddUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"AnnouncementAction":{"oneOf":[{"type":"object","required":["text","type"],"properties":{"text":{"type":"string"},"type":{"type":"string","enum":["normal"]}}},{"type":"object","required":["task_id","status","type"],"properties":{"comment":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/TaskStatus"},"task_id":{"type":"string"},"type":{"type":"string","enum":["task_status_update"]}}}]},"AnnouncementResponse":{"type":"object","required":["id","action_type","created_at","action"],"properties":{"action":{"$ref":"#/components/schemas/AnnouncementAction"},"action_type":{"type":"string"},"additional_data":{},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"}}},"ApplyTagRequest":{"type":"object","required":["tag_id"],"properties":{"tag_id":{"type":"string"}}},"CaptureLogEvent":{"type":"object","description":"Capture log event for https://ctftime.org/json-scoreboard-feed","required":["id","team"],"properties":{"id":{"type":"integer","format":"int32"},"pointsDelta":{"type":["integer","null"],"minimum":0},"task":{"type":["string","null"]},"team":{"type":"string"},"time":{"type":["integer","null"],"format":"int64"},"type":{"type":["string","null"]},"victim":{"type":["string","null"]}}},"ChangeLeaderModel":{"type":"object","required":["new_leader_username"],"properties":{"new_leader_username":{"type":"string"}}},"ChangeNameModel":{"type":"object","required":["new_name"],"properties":{"new_name":{"type":"string"}}},"ChangePasswordModel":{"type":"object","required":["old_password","new_password"],"properties":{"new_password":{"$ref":"#/components/schemas/Password"},"old_password":{"$ref":"#/components/schemas/Password"}}},"Coordinates":{"type":"object","required":["lng","lat"],"properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}},"CreateExternalTeamModel":{"type":"object","required":["teams"],"properties":{"teams":{"type":"array","items":{"type":"array","items":false,"prefixItems":[{"type":"string"},{"type":"integer","format":"int32","minimum":0}]}}}},"CreateTeamModel":{"type":"object","required":["team_name"],"properties":{"organization":{"type":["string","null"]},"team_name":{"type":"string"}}},"CtfExperience":{"type":"string","enum":["Never","Beginner","Intermediate","Advanced","Expert"]},"EmailMeta":{"type":"object","required":["subject"],"properties":{"sender_name":{"type":["string","null"]},"subject":{"type":"string"}}},"EmailSendTarget":{"oneOf":[{"type":"string","enum":["AllUsers"]},{"type":"string","enum":["UsersInTeams"]},{"type":"string","enum":["UsersRegisteredOnEvent"]},{"type":"object","required":["SpecificUsernames"],"properties":{"SpecificUsernames":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["SpecificEmails"],"properties":{"SpecificEmails":{"type":"array","items":{"type":"string"}}}}]},"EmailSendingModel":{"allOf":[{"$ref":"#/components/schemas/EmailMeta"},{"type":"object","required":["address","send_target","content"],"properties":{"address":{"type":"string"},"content":{"type":"string"},"send_target":{"$ref":"#/components/schemas/EmailSendTarget"}}}]},"EventConfig":{"type":"object","required":["id","name","stages","task_release_phases"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/EventStage"}},"task_release_phases":{"type":"object","additionalProperties":{"type":"string","format":"date-time"},"propertyNames":{"type":"string"}}}},"EventInfoResponse":{"allOf":[{"$ref":"#/components/schemas/EventConfig"},{"type":"object","required":["is_started"],"properties":{"end_date":{"type":["string","null"],"format":"date-time"},"is_started":{"type":"boolean"},"start_date":{"type":["string","null"],"format":"date-time"}}}]},"EventStage":{"type":"object","required":["name","stage_type","start_date"],"properties":{"end_date":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"stage_type":{"$ref":"#/components/schemas/EventStageType"},"start_date":{"type":"string","format":"date-time"}}},"EventStageType":{"type":"string","enum":["event-start","event-end","informative"]},"EventStatusResponse":{"type":"object","required":["stage","is_live","is_before_event","is_after_event"],"properties":{"is_after_event":{"type":"boolean"},"is_before_event":{"type":"boolean"},"is_live":{"type":"boolean"},"stage":{"$ref":"#/components/schemas/EventStatusStage"}}},"EventStatusStage":{"type":"string","enum":["before","during","after"]},"ExternalInvitationsInfo":{"type":"object","required":["organization","is_registered","teams"],"properties":{"is_registered":{"type":"boolean"},"organization":{"type":"string"},"teams":{"type":"array","items":{"$ref":"#/components/schemas/TeamCodes"}}}},"ExternalRegistrationFormModel":{"type":"object","required":["organization","email_address"],"properties":{"email_address":{"type":"string"},"organization":{"type":"string"}}},"FoodPreference":{"type":"string","enum":["Standard","Vegetarian"]},"FoodPreferenceInput":{"type":"string","enum":["standard","vegetarian"]},"IdentifiedUserInfo":{"type":"object","required":["user_id","username","full_name","school","birth_year","is_underage","email","food_preference","tags"],"properties":{"birth_year":{"type":"string"},"email":{"type":"string"},"food_allergies":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreference"},"full_name":{"type":"string"},"is_underage":{"type":"boolean"},"school":{"type":"string"},"tags":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}},"team_name":{"type":["string","null"]},"user_id":{"type":"string","format":"uuid"},"username":{"type":"string"}}},"JoinExternalModel":{"type":"object","required":["code"],"properties":{"code":{"type":"string"}}},"Label":{"type":"object","required":["id","name","description"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}},"LeaderboardChart":{"type":"object","required":["event_timestamps","team_points_over_time"],"properties":{"event_timestamps":{"type":"array","items":{"type":"string","format":"date-time"}},"team_points_over_time":{"type":"array","items":{"$ref":"#/components/schemas/TeamPointsTimeSeries"}}}},"LoginModel":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"$ref":"#/components/schemas/Password"}}},"Model":{"type":"object","required":["id","action_type","created_at"],"properties":{"action_type":{"type":"string"},"additional_data":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value"}]},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"}}},"MyTeamWithMembers":{"type":"object","required":["team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"invited_users":{"type":["array","null"],"items":{"type":"string"}},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"ParticipantTag":{"type":"object","required":["id","name","description","type"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}}},"ParticipateRequest":{"type":"object","required":["full_name","school","birth_year","phone","is_underage","food_preference"],"properties":{"birth_year":{"type":"string"},"emergency_contact_email":{"type":["string","null"]},"emergency_contact_name":{"type":["string","null"]},"emergency_contact_phone":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreferenceInput"},"full_name":{"type":"string"},"is_underage":{"type":"boolean"},"phone":{"type":"string"},"school":{"type":"string"}}},"ParticipateResponse":{"type":"object","required":["id","user_id","full_name","school","birth_year","phone","is_underage","food_preference","registered_at"],"properties":{"birth_year":{"type":"string"},"emergency_contact_email":{"type":["string","null"]},"emergency_contact_name":{"type":["string","null"]},"emergency_contact_phone":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreferenceInput"},"full_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_underage":{"type":"boolean"},"phone":{"type":"string"},"registered_at":{"type":"string","format":"date-time"},"school":{"type":"string"},"user_id":{"type":"string","format":"uuid"}}},"Password":{"type":"string"},"RegisterModel":{"type":"object","required":["name","first_name","email","password"],"properties":{"email":{"type":"string"},"first_name":{"type":"string"},"name":{"type":"string"},"password":{"$ref":"#/components/schemas/Password"}}},"RegistrationConfig":{"type":"object","required":["start_date","end_date","max_teams","max_team_size","registration_mode","max_teams_per_organization"],"properties":{"end_date":{"type":"string","format":"date-time"},"max_team_size":{"type":"integer","format":"int32","minimum":0},"max_teams":{"type":"integer","format":"int32","minimum":0},"max_teams_per_organization":{"type":"integer","format":"int32","minimum":0},"registration_mode":{"$ref":"#/components/schemas/RegistrationMode"},"start_date":{"type":"string","format":"date-time"}}},"RegistrationMode":{"type":"string","enum":["internal","external"]},"RegistrationResponse":{"allOf":[{"$ref":"#/components/schemas/RegistrationConfig"},{"type":"object","required":["is_open"],"properties":{"is_open":{"type":"boolean"}}}]},"RemoveUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"RequestResetPasswordModel":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}},"ResetPasswordModel":{"type":"object","required":["code","new_password"],"properties":{"code":{"type":"string","format":"uuid"},"new_password":{"$ref":"#/components/schemas/Password"}}},"ResponseData":{"type":"object","required":["version","name","about"],"properties":{"about":{"type":"string"},"name":{"type":"string"},"version":{"type":"string"}}},"SchoolGrade":{"type":"string","enum":["NotStudying","PrimarySchool","Class1","Class2","Class3","Class4","Class5","University"]},"SimpleTask":{"allOf":[{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TaskMeta"}]},{"type":"object","required":["display"],"properties":{"available_since":{"type":["string","null"],"format":"date-time"},"display":{"$ref":"#/components/schemas/TaskDisplay"}}}]},"SingleTeamStanding":{"type":"object","required":["team","score","taskStats","lastAccept"],"properties":{"lastAccept":{"type":"integer","format":"int64"},"score":{"type":"integer","minimum":0},"taskStats":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TaskTeamStat"},"propertyNames":{"type":"string"}},"team":{"type":"string"}}},"SubmitModel":{"type":"object","required":["flag"],"properties":{"flag":{"type":"string"}}},"SubmitResponse":{"type":"object","required":["task_id","task_title","points"],"properties":{"points":{"type":"integer","minimum":0},"task_id":{"type":"string"},"task_title":{"type":"string"}}},"TaskAsset":{"type":"object","required":["description","path"],"properties":{"description":{"type":"string"},"path":{"type":"string"}}},"TaskAuthor":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"url":{"type":["string","null"]}}},"TaskDisplay":{"type":"object","required":["icon_coordinates"],"properties":{"icon_coordinates":{"$ref":"#/components/schemas/Coordinates"}}},"TaskMeta":{"type":"object","required":["id","name","labels","difficulty_estimate"],"properties":{"authors":{"type":"array","items":{"$ref":"#/components/schemas/TaskAuthor"}},"difficulty_estimate":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"name":{"type":"string"}}},"TaskStatus":{"type":"string","enum":["up","broken","down"]},"TaskTeamStat":{"type":"object","required":["points","time"],"properties":{"points":{"type":"integer","minimum":0},"time":{"type":"integer","format":"int64"}}},"TaskWithStatus":{"allOf":[{"$ref":"#/components/schemas/SimpleTask"},{"type":"object","required":["status"],"properties":{"status":{"$ref":"#/components/schemas/TaskStatus"}}}]},"TeamCodes":{"type":"object","required":["team_name","codes"],"properties":{"codes":{"type":"array","items":{"type":"string"}},"team_name":{"type":"string"}}},"TeamMember":{"type":"object","required":["name","is_leader"],"properties":{"is_leader":{"type":"boolean"},"name":{"type":"string"}}},"TeamPointsTimeSeries":{"type":"object","required":["name","color","points"],"properties":{"color":{"type":"string"},"name":{"type":"string"},"points":{"type":"array","items":{"type":"integer","minimum":0}}}},"TeamRanking":{"type":"object","required":["team_id","team_name","current_points","captured_flags","color"],"properties":{"captured_flags":{"type":"integer","minimum":0},"color":{"type":"string"},"current_points":{"type":"integer","minimum":0},"team_id":{"type":"string","format":"uuid"},"team_name":{"type":"string"}}},"TeamRankingWithTasks":{"type":"object","required":["team_id","team_name","current_points","captured_flags","color","tasks"],"properties":{"captured_flags":{"type":"integer","minimum":0},"color":{"type":"string"},"current_points":{"type":"integer","minimum":0},"tasks":{"type":"object","additionalProperties":{"type":"string","format":"date-time"},"propertyNames":{"type":"string"}},"team_id":{"type":"string","format":"uuid"},"team_name":{"type":"string"}}},"TeamStandings":{"type":"object","description":"Team Standings for https://ctftime.org/json-scoreboard-feed","required":["tasks","standings"],"properties":{"standings":{"type":"array","items":{"$ref":"#/components/schemas/SingleTeamStanding"}},"tasks":{"type":"array","items":{"type":"string"}}}},"TeamStats":{"type":"object","required":["captured_flags","remaining_flags"],"properties":{"captured_flags":{"type":"integer","minimum":0},"rank":{"type":["array","null"],"items":false,"prefixItems":[{"type":"integer","minimum":0},{"type":"integer","minimum":0}]},"remaining_flags":{"type":"integer","minimum":0}}},"TeamStatus":{"type":"string","enum":["Confirmed","Absent"]},"TeamWithMembers":{"type":"object","required":["id","team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"members":{"type":"array","items":{"type":"array","items":false,"prefixItems":[{"type":"string","format":"uuid"},{"type":"string"}]}},"organization":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"UpdatableModel":{"type":"object","properties":{"color":{"type":["string","null"]},"created_at":{"type":["string","null"],"format":"date-time"},"name":{"type":["string","null"]},"organization":{"type":["string","null"]},"status":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TeamStatus"}]}}},"UpdateUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"UserIdentificationInfo":{"type":"object","required":["identification_code","applied_tags"],"properties":{"applied_tags":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}},"identification_code":{"type":"string","format":"uuid"}}},"UserInformationResponse":{"type":"object","required":["username","email","has_completed_onboarding"],"properties":{"email":{"type":"string"},"first_name":{"type":["string","null"]},"has_completed_onboarding":{"type":"boolean"},"username":{"type":"string"}}},"UserOnboardingSubmissionRequest":{"type":"object","required":["organization","location","ctf_experience","school_grade","referral_sources","marketing_consent","collab_interest"],"properties":{"collab_interest":{"type":"boolean"},"ctf_experience":{"$ref":"#/components/schemas/CtfExperience"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"organization":{"type":"string"},"referral_sources":{"type":"array","items":{"type":"string"}},"school_grade":{"$ref":"#/components/schemas/SchoolGrade"}}},"UserRoles":{"type":"string","enum":["Default","Admin","Owner"]},"Value":{}},"securitySchemes":{"access_token":{"type":"apiKey","in":"cookie","name":"access_token"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Hack4Krak API Documentation","description":"","license":{"name":"GPL-3.0","url":"https://www.gnu.org/licenses/gpl-3.0.en.html"},"version":"2.2.0"},"servers":[{"url":"http://localhost:8080/"}],"paths":{"/":{"get":{"operationId":"index","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseData"}}}}}}},"/account/":{"get":{"tags":["account"],"operationId":"account_index","responses":{"200":{"description":"Use information received.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInformationResponse"}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/delete":{"delete":{"tags":["account"],"operationId":"delete","responses":{"200":{"description":"Account successfully deleted"},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/identification":{"get":{"tags":["account"],"operationId":"account_identification","responses":{"200":{"description":"Identification information received.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserIdentificationInfo"}}}},"401":{"description":"Unauthorized."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/onboarding":{"get":{"tags":["account"],"operationId":"get_onboarding","responses":{"200":{"description":"User onboarding answers received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"post":{"tags":["account"],"operationId":"submit_onboarding","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOnboardingSubmissionRequest"}}},"required":true},"responses":{"200":{"description":"User onboarding answers submitted."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/update":{"patch":{"tags":["account"],"operationId":"update","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserModel"}}},"required":true},"responses":{"200":{"description":"Account updated successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":[]}]}},"/account/update/password":{"patch":{"tags":["account"],"operationId":"change_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordModel"}}},"required":true},"responses":{"200":{"description":"Account updated successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":[]}]}},"/admin/":{"get":{"tags":["admin"],"operationId":"admin-index","responses":{"200":{"description":"Use information received."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/announcements/":{"get":{"tags":["admin/announcements"],"operationId":"admin-announcements-list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"action_type","in":"query","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"List of announcements.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}}},"security":[{"access_token":[]}]},"post":{"tags":["admin/announcements"],"operationId":"admin-announcements-create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementAction"}}},"required":true},"responses":{"201":{"description":"Announcement created successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}},"security":[{"access_token":[]}]}},"/admin/email/send_external_registration_form":{"post":{"tags":["admin/email"],"operationId":"send_external_registration_form","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalRegistrationFormModel"}}},"required":true},"responses":{"200":{"description":"Confirmation code successfully generated."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/email/send_informational":{"post":{"tags":["admin/email"],"operationId":"send_informational","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSendingModel"}}},"required":true},"responses":{"200":{"description":"Email successfully sent"},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/identification/apply-tag/{id}":{"post":{"tags":["admin/identification"],"operationId":"admin_identification_apply_tag","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyTagRequest"}}},"required":true},"responses":{"200":{"description":"Tag successfully applied."},"404":{"description":"User or tag not found."},"409":{"description":"User already has this tag applied."},"500":{"description":"Internal server error."}}}},"/admin/identification/identify/{id}":{"get":{"tags":["admin/identification"],"operationId":"admin_identification_identify","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User successfully identified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifiedUserInfo"}}}},"404":{"description":"No user found with the given identification UUID."},"500":{"description":"Internal server error."}}}},"/admin/identification/reset/{user_id}":{"post":{"tags":["admin/identification"],"operationId":"admin_identification_reset_identification_code","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Identification code successfully reset and email sent."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}}}},"/admin/tasks/refresh":{"post":{"tags":["admin/tasks"],"operationId":"admin_tasks_refresh","responses":{"200":{"description":"Tasks successfully refreshed."},"500":{"description":"Internal server error."}}}},"/admin/teams/delete/{id}":{"delete":{"tags":["admin/teams"],"operationId":"admin_teams_delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Team successfully deleted."},"500":{"description":"Internal server error."}}}},"/admin/teams/list":{"get":{"tags":["admin/teams"],"operationId":"admin_teams_list","responses":{"200":{"description":"Teams successfully fetched.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamWithMembers"}}}}},"500":{"description":"Internal server error."}}}},"/admin/teams/update/{id}":{"patch":{"tags":["admin/teams"],"operationId":"admin_teams_update","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatableModel"}}},"required":true},"responses":{"200":{"description":"Team successfully updated."},"500":{"description":"Internal server error."}}}},"/admin/users/delete/{id}":{"delete":{"tags":["admin/users"],"operationId":"admin_users_delete","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User successfully deleted"},"403":{"description":"User must have higher role than updated user."},"500":{"description":"Internal server error."}}}},"/admin/users/email_confirmations":{"get":{"tags":["admin/users"],"operationId":"email_confirmations_list","responses":{"200":{"description":"Email confirmations successfully fetched.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}},"500":{"description":"Internal server error."}}}},"/admin/users/list":{"get":{"tags":["admin/users"],"operationId":"admin_users_list","responses":{"200":{"description":"User list successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}},"500":{"description":"Internal server error."}}}},"/admin/users/onboarding/{id}":{"get":{"tags":["admin/users"],"operationId":"admin_get_onboarding","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User onboarding answers received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/admin/users/update/{id}":{"patch":{"tags":["admin/users"],"operationId":"admin_users_update","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatableModel"}}},"required":true},"responses":{"200":{"description":"User successfully updated."},"403":{"description":"User must have higher role than updated user."},"500":{"description":"Internal server error."}}}},"/announcements/":{"get":{"tags":["announcements"],"operationId":"list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"action_type","in":"query","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"List of announcements.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}}}}},"/announcements/latest":{"get":{"tags":["announcements"],"operationId":"latest","responses":{"200":{"description":"The latest announcement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnouncementResponse"}}}},"404":{"description":"No announcements found."}}}},"/auth/confirm/{confirmation_code}":{"get":{"tags":["auth"],"operationId":"confirm_email","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Email successfully confirmed. Redirecting..."},"307":{"description":"Invalid confirmation code."},"500":{"description":"Internal server error."}}}},"/auth/login":{"post":{"tags":["auth"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginModel"}}},"required":true},"responses":{"200":{"description":"User successfully logged in."},"401":{"description":"Invalid credentials."},"500":{"description":"Internal server error."}}}},"/auth/logout":{"post":{"tags":["auth"],"operationId":"logout","responses":{"200":{"description":"User successfully logged out."}}}},"/auth/oauth/github":{"get":{"tags":["auth/oauth"],"operationId":"github","responses":{"200":{"description":"Redirects to GitHub for OAuth authorization"}}}},"/auth/oauth/github/callback":{"get":{"tags":["auth/oauth"],"operationId":"github_callback","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth2 flow completed successfully"},"307":{"description":"Invalid credentials"},"500":{"description":"Internal server errors."}}}},"/auth/oauth/google":{"get":{"tags":["auth/oauth"],"operationId":"google","responses":{"200":{"description":"Redirects to Google for OAuth authorization"}}}},"/auth/oauth/google/callback":{"get":{"tags":["auth/oauth"],"operationId":"google_callback","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth2 flow completed successfully"},"307":{"description":"Invalid credentials"},"500":{"description":"Internal server errors."}}}},"/auth/refresh":{"post":{"tags":["auth"],"operationId":"refresh","responses":{"200":{"description":"New tokens are set as cookies"},"401":{"description":"Invalid credentials."}},"security":[{"access_token":[]}]}},"/auth/register":{"post":{"tags":["auth"],"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterModel"}}},"required":true},"responses":{"200":{"description":"User successfully registered."},"400":{"description":"User already registered."},"500":{"description":"Internal server error."}}}},"/auth/request_reset_password":{"post":{"tags":["auth"],"operationId":"request_reset_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResetPasswordModel"}}},"required":true},"responses":{"200":{"description":"Password reset email sent."}}}},"/auth/reset_password":{"patch":{"tags":["auth"],"operationId":"reset_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordModel"}}},"required":true},"responses":{"200":{"description":"Password successfully reset."},"400":{"description":"Invalid reset code."},"500":{"description":"Internal server error."}}}},"/auth/status":{"get":{"tags":["auth"],"operationId":"auth-status","responses":{"200":{"description":"User is logged in"},"401":{"description":"User is not logged in."}},"security":[{"access_token":[]}]}},"/event/info":{"get":{"tags":["event"],"operationId":"info","responses":{"200":{"description":"Correctly returned event config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInfoResponse"}}}}}}},"/event/label/{label_id}":{"get":{"operationId":"label","parameters":[{"name":"label_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned icon image.","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid label id."},"404":{"description":"Label not found."},"500":{"description":"Internal server error."}}}},"/event/participant-tags":{"get":{"tags":["event"],"operationId":"event_participant_tags","responses":{"200":{"description":"Successfully returned participant tags list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}}}}},"500":{"description":"Internal server error."}}}},"/event/participate":{"get":{"tags":["event"],"operationId":"get_participation","responses":{"200":{"description":"Registration data returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParticipateResponse"}}}},"404":{"description":"User has not registered yet."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"post":{"tags":["event"],"operationId":"submit_participation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParticipateRequest"}}},"required":true},"responses":{"200":{"description":"Registration submitted successfully."},"400":{"description":"Registration period is not open."},"409":{"description":"User is already registered."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]},"delete":{"tags":["event"],"operationId":"delete_participation","responses":{"200":{"description":"Registration cancelled successfully."},"404":{"description":"User has not registered yet."},"409":{"description":"User must leave or delete their team first."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/event/registration":{"get":{"tags":["event"],"operationId":"registration","responses":{"200":{"description":"Correctly returned registration config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationResponse"}}}}}}},"/event/status":{"get":{"tags":["event"],"operationId":"status","responses":{"200":{"description":"Event status returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventStatusResponse"}}}},"500":{"description":"Internal server error."}}}},"/flag/submit":{"post":{"tags":["flag"],"operationId":"submit","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitModel"}}},"required":true},"responses":{"200":{"description":"Correctly submitted flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitResponse"}}}},"400":{"description":"Invalid flag"},"403":{"description":"Team is not verified for flag submission"},"409":{"description":"Flag already submitted by the team"}}}},"/leaderboard/chart":{"get":{"tags":["leaderboard"],"operationId":"chart","responses":{"200":{"description":"Successfully retrieved chart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardChart"}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/ctftime/capture-log":{"get":{"tags":["leaderboard"],"operationId":"capture_log","parameters":[{"name":"lastId","in":"query","description":"ID of the last event received","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"Successfully retrieved capture log.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CaptureLogEvent"}}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/ctftime/team-standings":{"get":{"tags":["leaderboard"],"operationId":"team_standings","responses":{"200":{"description":"Successfully retrieved information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamStandings"}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/teams":{"get":{"tags":["leaderboard"],"operationId":"teams","responses":{"200":{"description":"Successfully retrieved leaderboard","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamRanking"}}}}},"500":{"description":"Internal server error"}}}},"/leaderboard/teams_with_tasks":{"get":{"tags":["leaderboard"],"operationId":"teams_with_tasks","responses":{"200":{"description":"Successfully retrieved leaderboard","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TeamRankingWithTasks"}}}}},"500":{"description":"Internal server error"}}}},"/metrics":{"get":{"operationId":"metrics","responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/tasks/assets/get/{task_id}/{asset_path}":{"get":{"tags":["task/assets"],"operationId":"get","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"asset_path","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of task assets.","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Asset does not exist."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/tasks/assets/list/{task_id}":{"get":{"tags":["task/assets"],"operationId":"task_assets_list","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of task assets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TaskAsset"}}}}},"404":{"description":"Task does not exist."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/tasks/count":{"get":{"tags":["tasks"],"operationId":"count","responses":{"200":{"description":"Correctly returned the number of tasks.","content":{"text/plain":{"schema":{"type":"integer","format":"int32","minimum":0}}}}}}},"/tasks/description/{task_id}":{"get":{"tags":["tasks"],"operationId":"description","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task description","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/icon/{task_id}":{"get":{"tags":["tasks"],"operationId":"icon","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned icon image.","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/labels":{"get":{"tags":["tasks"],"operationId":"task_labels","responses":{"200":{"description":"Successfully returned task labels list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Label"}}}}},"500":{"description":"Internal server error."}}}},"/tasks/list":{"get":{"tags":["tasks"],"operationId":"task_list","responses":{"200":{"description":"Tasks successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TaskWithStatus"}}}}},"500":{"description":"Internal server error."}}}},"/tasks/name/{task_id}":{"get":{"tags":["tasks"],"operationId":"name","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task name","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."}}}},"/tasks/solution/{task_id}":{"get":{"tags":["tasks"],"operationId":"solution","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Correctly returned task solution","content":{"text/plain":{"schema":{"type":"string"}}}},"400":{"description":"Invalid task id."},"404":{"description":"Task not found."},"500":{"description":"Internal server error."}}}},"/tasks/task-status/updates":{"get":{"tags":["announcements"],"operationId":"updates","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}},{"name":"offset","in":"query","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0}}],"responses":{"200":{"description":"List of task status updates.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnnouncementResponse"}}}}}}}},"/teams/create":{"post":{"tags":["teams"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamModel"}}},"required":true},"responses":{"200":{"description":"Team successfully created."},"403":{"description":"User already belongs to team."},"409":{"description":"Team already exists."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/external_invitations/create/{confirmation_code}":{"post":{"tags":["teams/external_invitations"],"description":"Create external team invitations using secret token","operationId":"create_external","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExternalTeamModel"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"array","items":{"type":"string"}}}}}},"400":{"description":""},"500":{"description":""}},"security":[{"access_token":[]}]}},"/teams/external_invitations/info/{code}":{"get":{"tags":["teams/external_invitations"],"description":"Create external team invitations using secret token","operationId":"external_invitations_info","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalInvitationsInfo"}}}},"400":{"description":""},"500":{"description":""}},"security":[{"access_token":[]}]}},"/teams/external_invitations/join":{"post":{"tags":["teams/external_invitations"],"operationId":"join_external","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinExternalModel"}}},"required":true},"responses":{"200":{"description":"Successfully joined team."},"403":{"description":"User already belongs to team."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/invitations/":{"get":{"tags":["teams/invitations"],"operationId":"get_invitations","responses":{"200":{"description":"Successfully retrieved invitations","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"User doesn't have any invitations."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/invitations/accept_invitation/{team_name}":{"post":{"tags":["teams/invitations"],"operationId":"accept_invitation","parameters":[{"name":"team_name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successfully joined team."},"403":{"description":"User already belongs to team."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/":{"get":{"tags":["teams/management"],"operationId":"teams_management_index","responses":{"200":{"description":"User is team leader."},"500":{"description":"Internal server error."}},"security":[{"access_token":["leader"]}]}},"/teams/management/change_leader":{"patch":{"tags":["teams/management"],"operationId":"change_leader","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeLeaderModel"}}},"required":true},"responses":{"200":{"description":"Team name successfully changed."},"403":{"description":"User is not the leader of any team."},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/delete":{"delete":{"tags":["teams/management"],"operationId":"delete_team","responses":{"200":{"description":"Team deleted successfully"},"500":{"description":"Internal server error"}},"security":[{"access_token":["leader"]}]}},"/teams/management/invite_user":{"post":{"tags":["teams/management"],"operationId":"invite_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUserModel"}}},"required":true},"responses":{"200":{"description":"User successfully invited to team."},"403":{"description":"User already belongs to team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":["leader"]}]}},"/teams/management/kick_user":{"delete":{"tags":["teams/management"],"operationId":"kick_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveUserModel"}}},"required":true},"responses":{"200":{"description":"User successfully removed from team."},"403":{"description":"User is not a member of the team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/rename":{"patch":{"tags":["teams/management"],"operationId":"rename","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeNameModel"}}},"required":true},"responses":{"200":{"description":"Team name successfully changed."},"403":{"description":"User is not the leader of any team."},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/management/revoke_invitation/{username}":{"delete":{"tags":["teams/management"],"operationId":"revoke_invitation","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invitation successfully revoked."},"404":{"description":"Invitation not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/completed_tasks":{"get":{"tags":["teams/membership"],"operationId":"completed_tasks","responses":{"200":{"description":"Successfully retrieved completed tasks.","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"403":{"description":"User doesn't belong to any team."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/leave_team":{"delete":{"tags":["teams/membership"],"operationId":"leave_team","responses":{"200":{"description":"User successfully left team."},"403":{"description":"User is not a member of the team."},"404":{"description":"User not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/my_team":{"get":{"tags":["teams/membership"],"operationId":"my_team","responses":{"200":{"description":"Team successfully retrieved.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MyTeamWithMembers"}]}}}},"404":{"description":"Team not found."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/teams/membership/stats":{"get":{"tags":["teams/membership"],"operationId":"stats","responses":{"200":{"description":"Team successfully retrieved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamStats"}}}},"403":{"description":"User doesn't belong to any team."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}}},"components":{"schemas":{"AddUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"AnnouncementAction":{"oneOf":[{"type":"object","required":["text","type"],"properties":{"text":{"type":"string"},"type":{"type":"string","enum":["normal"]}}},{"type":"object","required":["task_id","status","type"],"properties":{"comment":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/TaskStatus"},"task_id":{"type":"string"},"type":{"type":"string","enum":["task_status_update"]}}}]},"AnnouncementResponse":{"type":"object","required":["id","action_type","created_at","action"],"properties":{"action":{"$ref":"#/components/schemas/AnnouncementAction"},"action_type":{"type":"string"},"additional_data":{},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"}}},"ApplyTagRequest":{"type":"object","required":["tag_id"],"properties":{"tag_id":{"type":"string"}}},"CaptureLogEvent":{"type":"object","description":"Capture log event for https://ctftime.org/json-scoreboard-feed","required":["id","team"],"properties":{"id":{"type":"integer","format":"int32"},"pointsDelta":{"type":["integer","null"],"minimum":0},"task":{"type":["string","null"]},"team":{"type":"string"},"time":{"type":["integer","null"],"format":"int64"},"type":{"type":["string","null"]},"victim":{"type":["string","null"]}}},"ChangeLeaderModel":{"type":"object","required":["new_leader_username"],"properties":{"new_leader_username":{"type":"string"}}},"ChangeNameModel":{"type":"object","required":["new_name"],"properties":{"new_name":{"type":"string"}}},"ChangePasswordModel":{"type":"object","required":["old_password","new_password"],"properties":{"new_password":{"$ref":"#/components/schemas/Password"},"old_password":{"$ref":"#/components/schemas/Password"}}},"Coordinates":{"type":"object","required":["lng","lat"],"properties":{"lat":{"type":"number","format":"float"},"lng":{"type":"number","format":"float"}}},"CreateExternalTeamModel":{"type":"object","required":["teams"],"properties":{"teams":{"type":"array","items":{"type":"array","items":false,"prefixItems":[{"type":"string"},{"type":"integer","format":"int32","minimum":0}]}}}},"CreateTeamModel":{"type":"object","required":["team_name"],"properties":{"organization":{"type":["string","null"]},"team_name":{"type":"string"}}},"CtfExperience":{"type":"string","enum":["Never","Beginner","Intermediate","Advanced","Expert"]},"EmailMeta":{"type":"object","required":["subject"],"properties":{"sender_name":{"type":["string","null"]},"subject":{"type":"string"}}},"EmailSendTarget":{"oneOf":[{"type":"string","enum":["AllUsers"]},{"type":"string","enum":["UsersInTeams"]},{"type":"string","enum":["UsersRegisteredOnEvent"]},{"type":"object","required":["SpecificUsernames"],"properties":{"SpecificUsernames":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["SpecificEmails"],"properties":{"SpecificEmails":{"type":"array","items":{"type":"string"}}}}]},"EmailSendingModel":{"allOf":[{"$ref":"#/components/schemas/EmailMeta"},{"type":"object","required":["address","send_target","content"],"properties":{"address":{"type":"string"},"content":{"type":"string"},"send_target":{"$ref":"#/components/schemas/EmailSendTarget"}}}]},"EventConfig":{"type":"object","required":["id","name","stages","task_release_phases"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/EventStage"}},"task_release_phases":{"type":"object","additionalProperties":{"type":"string","format":"date-time"},"propertyNames":{"type":"string"}}}},"EventInfoResponse":{"allOf":[{"$ref":"#/components/schemas/EventConfig"},{"type":"object","required":["is_started"],"properties":{"end_date":{"type":["string","null"],"format":"date-time"},"is_started":{"type":"boolean"},"start_date":{"type":["string","null"],"format":"date-time"}}}]},"EventStage":{"type":"object","required":["name","stage_type","start_date"],"properties":{"end_date":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"stage_type":{"$ref":"#/components/schemas/EventStageType"},"start_date":{"type":"string","format":"date-time"}}},"EventStageType":{"type":"string","enum":["event-start","event-end","informative"]},"EventStatusResponse":{"type":"object","required":["stage","is_live","is_before_event","is_after_event"],"properties":{"is_after_event":{"type":"boolean"},"is_before_event":{"type":"boolean"},"is_live":{"type":"boolean"},"stage":{"$ref":"#/components/schemas/EventStatusStage"}}},"EventStatusStage":{"type":"string","enum":["before","during","after"]},"ExternalInvitationsInfo":{"type":"object","required":["organization","is_registered","teams"],"properties":{"is_registered":{"type":"boolean"},"organization":{"type":"string"},"teams":{"type":"array","items":{"$ref":"#/components/schemas/TeamCodes"}}}},"ExternalRegistrationFormModel":{"type":"object","required":["organization","email_address"],"properties":{"email_address":{"type":"string"},"organization":{"type":"string"}}},"FoodPreference":{"type":"string","enum":["Standard","Vegetarian"]},"FoodPreferenceInput":{"type":"string","enum":["standard","vegetarian"]},"IdentifiedUserInfo":{"type":"object","required":["user_id","username","full_name","school","birth_year","is_underage","email","food_preference","tags"],"properties":{"birth_year":{"type":"string"},"email":{"type":"string"},"food_allergies":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreference"},"full_name":{"type":"string"},"is_underage":{"type":"boolean"},"school":{"type":"string"},"tags":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}},"team_name":{"type":["string","null"]},"user_id":{"type":"string","format":"uuid"},"username":{"type":"string"}}},"JoinExternalModel":{"type":"object","required":["code"],"properties":{"code":{"type":"string"}}},"Label":{"type":"object","required":["id","name","description"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}},"LeaderboardChart":{"type":"object","required":["event_timestamps","team_points_over_time"],"properties":{"event_timestamps":{"type":"array","items":{"type":"string","format":"date-time"}},"team_points_over_time":{"type":"array","items":{"$ref":"#/components/schemas/TeamPointsTimeSeries"}}}},"LoginModel":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"$ref":"#/components/schemas/Password"}}},"Model":{"type":"object","required":["id","action_type","created_at"],"properties":{"action_type":{"type":"string"},"additional_data":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value"}]},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"}}},"MyTeamWithMembers":{"type":"object","required":["team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"invited_users":{"type":["array","null"],"items":{"type":"string"}},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"ParticipantTag":{"type":"object","required":["id","name","description","type"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}}},"ParticipateRequest":{"type":"object","required":["full_name","school","birth_year","phone","is_underage","food_preference"],"properties":{"birth_year":{"type":"string"},"emergency_contact_email":{"type":["string","null"]},"emergency_contact_name":{"type":["string","null"]},"emergency_contact_phone":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreferenceInput"},"full_name":{"type":"string"},"is_underage":{"type":"boolean"},"phone":{"type":"string"},"school":{"type":"string"}}},"ParticipateResponse":{"type":"object","required":["id","user_id","full_name","school","birth_year","phone","is_underage","food_preference","registered_at"],"properties":{"birth_year":{"type":"string"},"emergency_contact_email":{"type":["string","null"]},"emergency_contact_name":{"type":["string","null"]},"emergency_contact_phone":{"type":["string","null"]},"food_preference":{"$ref":"#/components/schemas/FoodPreferenceInput"},"full_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_underage":{"type":"boolean"},"phone":{"type":"string"},"registered_at":{"type":"string","format":"date-time"},"school":{"type":"string"},"user_id":{"type":"string","format":"uuid"}}},"Password":{"type":"string"},"RegisterModel":{"type":"object","required":["name","first_name","email","password"],"properties":{"callback":{"type":["string","null"]},"email":{"type":"string"},"first_name":{"type":"string"},"name":{"type":"string"},"password":{"$ref":"#/components/schemas/Password"}}},"RegistrationConfig":{"type":"object","required":["start_date","end_date","max_teams","max_team_size","registration_mode","max_teams_per_organization"],"properties":{"end_date":{"type":"string","format":"date-time"},"max_team_size":{"type":"integer","format":"int32","minimum":0},"max_teams":{"type":"integer","format":"int32","minimum":0},"max_teams_per_organization":{"type":"integer","format":"int32","minimum":0},"registration_mode":{"$ref":"#/components/schemas/RegistrationMode"},"start_date":{"type":"string","format":"date-time"}}},"RegistrationMode":{"type":"string","enum":["internal","external"]},"RegistrationResponse":{"allOf":[{"$ref":"#/components/schemas/RegistrationConfig"},{"type":"object","required":["is_open"],"properties":{"is_open":{"type":"boolean"}}}]},"RemoveUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"RequestResetPasswordModel":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}},"ResetPasswordModel":{"type":"object","required":["code","new_password"],"properties":{"code":{"type":"string","format":"uuid"},"new_password":{"$ref":"#/components/schemas/Password"}}},"ResponseData":{"type":"object","required":["version","name","about"],"properties":{"about":{"type":"string"},"name":{"type":"string"},"version":{"type":"string"}}},"SchoolGrade":{"type":"string","enum":["NotStudying","PrimarySchool","Class1","Class2","Class3","Class4","Class5","University"]},"SimpleTask":{"allOf":[{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TaskMeta"}]},{"type":"object","required":["display"],"properties":{"available_since":{"type":["string","null"],"format":"date-time"},"display":{"$ref":"#/components/schemas/TaskDisplay"}}}]},"SingleTeamStanding":{"type":"object","required":["team","score","taskStats","lastAccept"],"properties":{"lastAccept":{"type":"integer","format":"int64"},"score":{"type":"integer","minimum":0},"taskStats":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TaskTeamStat"},"propertyNames":{"type":"string"}},"team":{"type":"string"}}},"SubmitModel":{"type":"object","required":["flag"],"properties":{"flag":{"type":"string"}}},"SubmitResponse":{"type":"object","required":["task_id","task_title","points"],"properties":{"points":{"type":"integer","minimum":0},"task_id":{"type":"string"},"task_title":{"type":"string"}}},"TaskAsset":{"type":"object","required":["description","path"],"properties":{"description":{"type":"string"},"path":{"type":"string"}}},"TaskAuthor":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"url":{"type":["string","null"]}}},"TaskDisplay":{"type":"object","required":["icon_coordinates"],"properties":{"icon_coordinates":{"$ref":"#/components/schemas/Coordinates"}}},"TaskMeta":{"type":"object","required":["id","name","labels","difficulty_estimate"],"properties":{"authors":{"type":"array","items":{"$ref":"#/components/schemas/TaskAuthor"}},"difficulty_estimate":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"name":{"type":"string"}}},"TaskStatus":{"type":"string","enum":["up","broken","down"]},"TaskTeamStat":{"type":"object","required":["points","time"],"properties":{"points":{"type":"integer","minimum":0},"time":{"type":"integer","format":"int64"}}},"TaskWithStatus":{"allOf":[{"$ref":"#/components/schemas/SimpleTask"},{"type":"object","required":["status"],"properties":{"status":{"$ref":"#/components/schemas/TaskStatus"}}}]},"TeamCodes":{"type":"object","required":["team_name","codes"],"properties":{"codes":{"type":"array","items":{"type":"string"}},"team_name":{"type":"string"}}},"TeamMember":{"type":"object","required":["name","is_leader"],"properties":{"is_leader":{"type":"boolean"},"name":{"type":"string"}}},"TeamPointsTimeSeries":{"type":"object","required":["name","color","points"],"properties":{"color":{"type":"string"},"name":{"type":"string"},"points":{"type":"array","items":{"type":"integer","minimum":0}}}},"TeamRanking":{"type":"object","required":["team_id","team_name","current_points","captured_flags","color"],"properties":{"captured_flags":{"type":"integer","minimum":0},"color":{"type":"string"},"current_points":{"type":"integer","minimum":0},"team_id":{"type":"string","format":"uuid"},"team_name":{"type":"string"}}},"TeamRankingWithTasks":{"type":"object","required":["team_id","team_name","current_points","captured_flags","color","tasks"],"properties":{"captured_flags":{"type":"integer","minimum":0},"color":{"type":"string"},"current_points":{"type":"integer","minimum":0},"tasks":{"type":"object","additionalProperties":{"type":"string","format":"date-time"},"propertyNames":{"type":"string"}},"team_id":{"type":"string","format":"uuid"},"team_name":{"type":"string"}}},"TeamStandings":{"type":"object","description":"Team Standings for https://ctftime.org/json-scoreboard-feed","required":["tasks","standings"],"properties":{"standings":{"type":"array","items":{"$ref":"#/components/schemas/SingleTeamStanding"}},"tasks":{"type":"array","items":{"type":"string"}}}},"TeamStats":{"type":"object","required":["captured_flags","remaining_flags"],"properties":{"captured_flags":{"type":"integer","minimum":0},"rank":{"type":["array","null"],"items":false,"prefixItems":[{"type":"integer","minimum":0},{"type":"integer","minimum":0}]},"remaining_flags":{"type":"integer","minimum":0}}},"TeamStatus":{"type":"string","enum":["Confirmed","Absent"]},"TeamWithMembers":{"type":"object","required":["id","team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"members":{"type":"array","items":{"type":"array","items":false,"prefixItems":[{"type":"string","format":"uuid"},{"type":"string"}]}},"organization":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"UpdatableModel":{"type":"object","properties":{"color":{"type":["string","null"]},"created_at":{"type":["string","null"],"format":"date-time"},"name":{"type":["string","null"]},"organization":{"type":["string","null"]},"status":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TeamStatus"}]}}},"UpdateUserModel":{"type":"object","required":["username"],"properties":{"username":{"type":"string"}}},"UserIdentificationInfo":{"type":"object","required":["identification_code","applied_tags"],"properties":{"applied_tags":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantTag"}},"identification_code":{"type":"string","format":"uuid"}}},"UserInformationResponse":{"type":"object","required":["username","email","has_completed_onboarding"],"properties":{"email":{"type":"string"},"first_name":{"type":["string","null"]},"has_completed_onboarding":{"type":"boolean"},"username":{"type":"string"}}},"UserOnboardingSubmissionRequest":{"type":"object","required":["organization","location","ctf_experience","school_grade","referral_sources","marketing_consent","collab_interest"],"properties":{"collab_interest":{"type":"boolean"},"ctf_experience":{"$ref":"#/components/schemas/CtfExperience"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"organization":{"type":"string"},"referral_sources":{"type":"array","items":{"type":"string"}},"school_grade":{"$ref":"#/components/schemas/SchoolGrade"}}},"UserRoles":{"type":"string","enum":["Default","Admin","Owner"]},"Value":{}},"securitySchemes":{"access_token":{"type":"apiKey","in":"cookie","name":"access_token"}}}} \ No newline at end of file