From 02ddb4e6838cf450bfe3e544c8d47d8365d4b749 Mon Sep 17 00:00:00 2001 From: lajczi Date: Sun, 1 Mar 2026 18:56:49 +0100 Subject: [PATCH 01/17] feat(frontend): redirect to callback url after login Signed-off-by: lajczi --- frontend/app/components/AuthForm.vue | 2 +- frontend/app/middleware/auth.global.ts | 4 ++-- frontend/app/middleware/guest.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/AuthForm.vue b/frontend/app/components/AuthForm.vue index 42c7510a7..fab84d0a4 100644 --- a/frontend/app/components/AuthForm.vue +++ b/frontend/app/components/AuthForm.vue @@ -47,7 +47,7 @@ async function onSubmit(event: Schema) { if (props.isLogin) { toast.add({ title: 'Sukces', description: 'Pomyślnie zalogowano!', color: 'success' }) - await navigateTo('/panel/') + await navigateTo((route.query.callback as string) || '/panel/') } else { 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') diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index eec1e57fd..0f0e23572 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -5,7 +5,7 @@ export default defineNuxtRouteMiddleware(async (to) => { redirect: 'error', }) if (error.value || !data.value) { - return '/login' + return `/login?callback=${encodeURIComponent(to.fullPath)}` } if (to.path.startsWith('/panel')) { if (data.value.has_personal_information === false) { @@ -13,7 +13,7 @@ export default defineNuxtRouteMiddleware(async (to) => { } } } catch { - return '/login' + return `/login?callback=${encodeURIComponent(to.fullPath)}` } } }) diff --git a/frontend/app/middleware/guest.ts b/frontend/app/middleware/guest.ts index cba23a086..5f2994915 100644 --- a/frontend/app/middleware/guest.ts +++ b/frontend/app/middleware/guest.ts @@ -1,10 +1,10 @@ -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 '/panel' + return (to.query.callback as string) || '/panel' } } catch (error) { console.error(error) From bd8769da556f97fb91d73e757c8d2479d63f9e12 Mon Sep 17 00:00:00 2001 From: lajczi Date: Mon, 2 Mar 2026 16:42:53 +0100 Subject: [PATCH 02/17] chore: apply suggestion from code review Co-authored-by: Norbiros --- frontend/app/middleware/auth.global.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index 0f0e23572..6c918b0a9 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -5,7 +5,7 @@ export default defineNuxtRouteMiddleware(async (to) => { redirect: 'error', }) if (error.value || !data.value) { - return `/login?callback=${encodeURIComponent(to.fullPath)}` + return await navigateTo({ name: 'product', params: { callback: to.fullPath } }) } if (to.path.startsWith('/panel')) { if (data.value.has_personal_information === false) { From 81a8122aa4c755eb6ea2d03ea4cc38751a9101ce Mon Sep 17 00:00:00 2001 From: lajczi Date: Mon, 2 Mar 2026 16:49:16 +0100 Subject: [PATCH 03/17] chore: fix Norbiros skill issue Signed-off-by: lajczi --- frontend/app/middleware/auth.global.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index 6c918b0a9..a1064a1ed 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -5,7 +5,7 @@ export default defineNuxtRouteMiddleware(async (to) => { redirect: 'error', }) if (error.value || !data.value) { - return await navigateTo({ name: 'product', params: { callback: to.fullPath } }) + return await navigateTo({ name: 'login', query: { callback: to.fullPath } }) } if (to.path.startsWith('/panel')) { if (data.value.has_personal_information === false) { @@ -13,7 +13,7 @@ export default defineNuxtRouteMiddleware(async (to) => { } } } catch { - return `/login?callback=${encodeURIComponent(to.fullPath)}` + return await navigateTo({ name: 'login', query: { callback: to.fullPath } }) } } }) From 601beb331fbe667b03d55f3bd5008e4806b65aa3 Mon Sep 17 00:00:00 2001 From: lajczi Date: Mon, 2 Mar 2026 17:07:59 +0100 Subject: [PATCH 04/17] feat(backend): add callback for registration & email confirmation Signed-off-by: lajczi --- backend/src/models/email_verification_request.rs | 11 +++++++++-- backend/src/routes/auth/confirm.rs | 8 ++++++-- backend/src/routes/auth/register.rs | 1 + backend/src/services/auth.rs | 15 ++++++++++----- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/src/models/email_verification_request.rs b/backend/src/models/email_verification_request.rs index f91a2a182..bf67459e6 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 }, } @@ -117,6 +121,7 @@ mod tests { name: NAME.to_string(), ..Default::default() }, + callback: None, }; let (name, data) = action.get(); assert_eq!(name, "confirm_email_address"); @@ -130,6 +135,7 @@ mod tests { name: NAME.to_string(), ..Default::default() }, + callback: Some("/panel".to_string()), }; let (action_type, additional_data) = action.get(); @@ -142,8 +148,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 1c943dbb4..57eaa0db7 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 AuthService::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 6bd8ac8cb..46b1717da 100644 --- a/backend/src/routes/auth/register.rs +++ b/backend/src/routes/auth/register.rs @@ -18,6 +18,7 @@ pub struct RegisterModel { pub email: String, #[validate(length(min = 8, max = 32))] pub password: Password, + pub callback: Option, } #[utoipa::path( diff --git a/backend/src/services/auth.rs b/backend/src/services/auth.rs index af7be6ac5..cb763a85a 100644 --- a/backend/src/services/auth.rs +++ b/backend/src/services/auth.rs @@ -44,7 +44,10 @@ impl AuthService { 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 AuthService { 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 AuthService { email_confirmation.delete(&app_state.database).await?; - Ok(()) + Ok(callback) } fn create_email_confirmation_link(confirmation_code: &str) -> Result { From cb2a82f28f03f2cd3144d021f2a5e413de765b04 Mon Sep 17 00:00:00 2001 From: lajczi Date: Mon, 2 Mar 2026 17:14:13 +0100 Subject: [PATCH 05/17] chore: regenerate `openapi.json` & update `AuthForm` Signed-off-by: lajczi --- frontend/app/components/AuthForm.vue | 13 +++++++++---- frontend/openapi/api/openapi.json | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/AuthForm.vue b/frontend/app/components/AuthForm.vue index fab84d0a4..2b7f173ef 100644 --- a/frontend/app/components/AuthForm.vue +++ b/frontend/app/components/AuthForm.vue @@ -18,6 +18,7 @@ const toast = useToast() const OAuthBaseUrl = `${useRuntimeConfig().public.openFetch.api.baseURL}/auth/oauth` const route = useRoute() +const callback = route.query.callback as string | undefined if (route.query.redirect_from_confirmation === 'true' && import.meta.client) { toast.add({ @@ -39,18 +40,22 @@ async function onSubmit(event: Schema) { toast.add({ title: 'Oczekiwanie', description: 'Wysyłanie emaila…', color: 'info' }) } + const body = props.isLogin + ? event + : { ...event, callback } + await useNuxtApp().$api(address, { method: 'POST', credentials: 'include', - body: event, + body, }) if (props.isLogin) { toast.add({ title: 'Sukces', description: 'Pomyślnie zalogowano!', color: 'success' }) - await navigateTo((route.query.callback as string) || '/panel/') + await navigateTo(callback || '/panel/') } else { 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 } }) } } catch (error) { console.error(error) @@ -80,7 +85,7 @@ async function onSubmit(event: Schema) {
{{ isLogin ? 'Nie masz konta?' : 'Masz już konto?' }} - + {{ isLogin ? 'Załóż je' : 'Zaloguj się' }} diff --git a/frontend/openapi/api/openapi.json b/frontend/openapi/api/openapi.json index 319812f59..45c051571 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.1.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/get_personal_information":{"get":{"tags":["account"],"operationId":"user_get_personal_information","responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/submit_personal_information":{"post":{"tags":["account"],"operationId":"submit_personal_information","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPersonalInformationSubmissionRequest"}}},"required":true},"responses":{"200":{"description":"User personal information 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/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/tasks/refresh":{"post":{"tags":["admin/tasks"],"operationId":"admin_tasks_refresh","responses":{"200":{"description":"Tasks successfully refreshed."},"500":{"description":"Internal server error."}}}},"/admin/teams/clear_confirmation_code/{id}":{"delete":{"tags":["admin/teams"],"operationId":"clear_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully cleared."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/generate_confirmation_code/{id}":{"patch":{"tags":["admin/teams"],"operationId":"generate_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully generated."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/get_personal_information/{id}":{"get":{"tags":["admin/users"],"operationId":"admin_get_personal_information","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/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."}}}},"/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."},"500":{"description":"Internal server error."}}}},"/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/EventConfig"}}}}}}},"/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/registration":{"get":{"tags":["event"],"operationId":"registration","responses":{"200":{"description":"Correctly returned registration config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationConfig"}}}}}}},"/event/status":{"get":{"tags":["event"],"operationId":"status","responses":{"200":{"description":"Event is in progress"},"403":{"description":"You cannot access the event"}}}},"/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"}}}},"/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/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/list":{"get":{"tags":["tasks"],"operationId":"task_list","responses":{"200":{"description":"Tasks successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SimpleTask"}}}}},"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."}}}},"/teams/confirm/{confirmation_code}":{"post":{"tags":["teams"],"operationId":"confirm","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successfully confirmed team."},"404":{"description":"Invalid confirmation code"},"500":{"description":"Internal server error."}}}},"/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/invited_users":{"get":{"tags":["teams/management"],"operationId":"invited_users","responses":{"200":{"description":"Successfully retrieved invitations to team","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"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"}}}}},"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":{"$ref":"#/components/schemas/MyTeamWithMembers"}}}},"403":{"description":"User doesn't belong to any team."},"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"}}},"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":["x","y"],"properties":{"x":{"type":"integer","format":"int32"},"y":{"type":"integer","format":"int32"}}},"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"}}},"EmailMeta":{"type":"object","required":["subject"],"properties":{"sender_name":{"type":["string","null"]},"subject":{"type":"string"}}},"EmailSendTarget":{"oneOf":[{"type":"string","enum":["AllUsers"]},{"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"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/EventStage"}}}},"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"]},"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"}}},"JoinExternalModel":{"type":"object","required":["code"],"properties":{"code":{"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","first_name","birth_year","location","organization","is_vegetarian","marketing_consent","marketing_consent_accepted_at","marketing_consent_updated_at"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"marketing_consent_accepted_at":{"type":"string","format":"date-time"},"marketing_consent_updated_at":{"type":"string","format":"date-time"},"organization":{"type":"string"},"referral_source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value"}]}}},"MyTeamWithMembers":{"type":"object","required":["team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"Password":{"type":"string"},"RegisterModel":{"type":"object","required":["name","email","password"],"properties":{"email":{"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"]},"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"}}},"SimpleTask":{"allOf":[{"$ref":"#/components/schemas/TaskMeta"},{"type":"object","required":["display"],"properties":{"display":{"$ref":"#/components/schemas/TaskDisplay"}}}]},"SubmitModel":{"type":"object","required":["flag"],"properties":{"flag":{"type":"string"}}},"SubmitResponse":{"type":"object","required":["task_id"],"properties":{"task_id":{"type":"string"}}},"TaskAsset":{"type":"object","required":["description","path"],"properties":{"description":{"type":"string"},"path":{"type":"string"}}},"TaskDisplay":{"type":"object","required":["icon_coordinates"],"properties":{"icon_coordinates":{"$ref":"#/components/schemas/Coordinates"}}},"TaskMeta":{"type":"object","required":["id","name","labels","difficulty_estimate"],"properties":{"difficulty_estimate":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"name":{"type":"string"}}},"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"}}},"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":{"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"]},"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"}}},"UserInformationResponse":{"type":"object","required":["username","email","has_personal_information"],"properties":{"email":{"type":"string"},"has_personal_information":{"type":"boolean"},"username":{"type":"string"}}},"UserPersonalInformationSubmissionRequest":{"type":"object","required":["first_name","birth_year","location","organization","is_vegetarian","marketing_consent"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"organization":{"type":"string"},"referral_source":{"type":["array","null"],"items":{"type":"string"}}}},"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.1.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/get_personal_information": {"get": {"tags": ["account"], "operationId": "user_get_personal_information", "responses": {"200": {"description": "User personal information received.", "content": {"application/json": {"schema": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/Model"}]}}}}, "500": {"description": "Internal server error."}}, "security": [{"access_token": []}]}}, "/account/submit_personal_information": {"post": {"tags": ["account"], "operationId": "submit_personal_information", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserPersonalInformationSubmissionRequest"}}}, "required": true}, "responses": {"200": {"description": "User personal information 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/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/tasks/refresh": {"post": {"tags": ["admin/tasks"], "operationId": "admin_tasks_refresh", "responses": {"200": {"description": "Tasks successfully refreshed."}, "500": {"description": "Internal server error."}}}}, "/admin/teams/clear_confirmation_code/{id}": {"delete": {"tags": ["admin/teams"], "operationId": "clear_confirmation_code", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "Confirmation code successfully cleared."}, "500": {"description": "Internal server error."}}, "security": [{"access_token": []}]}}, "/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/generate_confirmation_code/{id}": {"patch": {"tags": ["admin/teams"], "operationId": "generate_confirmation_code", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "Confirmation code successfully generated."}, "500": {"description": "Internal server error."}}, "security": [{"access_token": []}]}}, "/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/get_personal_information/{id}": {"get": {"tags": ["admin/users"], "operationId": "admin_get_personal_information", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "User personal information received.", "content": {"application/json": {"schema": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/Model"}]}}}}, "500": {"description": "Internal server error."}}, "security": [{"access_token": []}]}}, "/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/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."}}}}, "/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."}, "500": {"description": "Internal server error."}}}}, "/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/EventConfig"}}}}}}}, "/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/registration": {"get": {"tags": ["event"], "operationId": "registration", "responses": {"200": {"description": "Correctly returned registration config", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RegistrationConfig"}}}}}}}, "/event/status": {"get": {"tags": ["event"], "operationId": "status", "responses": {"200": {"description": "Event is in progress"}, "403": {"description": "You cannot access the event"}}}}, "/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"}}}}, "/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/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/list": {"get": {"tags": ["tasks"], "operationId": "task_list", "responses": {"200": {"description": "Tasks successfully retrieved.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/SimpleTask"}}}}}, "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."}}}}, "/teams/confirm/{confirmation_code}": {"post": {"tags": ["teams"], "operationId": "confirm", "parameters": [{"name": "confirmation_code", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "Successfully confirmed team."}, "404": {"description": "Invalid confirmation code"}, "500": {"description": "Internal server error."}}}}, "/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/invited_users": {"get": {"tags": ["teams/management"], "operationId": "invited_users", "responses": {"200": {"description": "Successfully retrieved invitations to team", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "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"}}}}}, "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": {"$ref": "#/components/schemas/MyTeamWithMembers"}}}}, "403": {"description": "User doesn't belong to any team."}, "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"}}}, "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": ["x", "y"], "properties": {"x": {"type": "integer", "format": "int32"}, "y": {"type": "integer", "format": "int32"}}}, "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"}}}, "EmailMeta": {"type": "object", "required": ["subject"], "properties": {"sender_name": {"type": ["string", "null"]}, "subject": {"type": "string"}}}, "EmailSendTarget": {"oneOf": [{"type": "string", "enum": ["AllUsers"]}, {"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"], "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "stages": {"type": "array", "items": {"$ref": "#/components/schemas/EventStage"}}}}, "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"]}, "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"}}}, "JoinExternalModel": {"type": "object", "required": ["code"], "properties": {"code": {"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", "first_name", "birth_year", "location", "organization", "is_vegetarian", "marketing_consent", "marketing_consent_accepted_at", "marketing_consent_updated_at"], "properties": {"birth_year": {"type": "integer", "format": "int32"}, "first_name": {"type": "string"}, "id": {"type": "string", "format": "uuid"}, "is_vegetarian": {"type": "boolean"}, "location": {"type": "string"}, "marketing_consent": {"type": "boolean"}, "marketing_consent_accepted_at": {"type": "string", "format": "date-time"}, "marketing_consent_updated_at": {"type": "string", "format": "date-time"}, "organization": {"type": "string"}, "referral_source": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/Value"}]}}}, "MyTeamWithMembers": {"type": "object", "required": ["team_name", "created_at", "members", "status"], "properties": {"created_at": {"type": "string", "format": "date-time"}, "members": {"type": "array", "items": {"$ref": "#/components/schemas/TeamMember"}}, "status": {"$ref": "#/components/schemas/TeamStatus"}, "team_name": {"type": "string"}}}, "Password": {"type": "string"}, "RegisterModel": {"type": "object", "required": ["name", "email", "password"], "properties": {"email": {"type": "string"}, "name": {"type": "string"}, "password": {"$ref": "#/components/schemas/Password"}, "callback": {"type": ["string", "null"]}}}, "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"]}, "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"}}}, "SimpleTask": {"allOf": [{"$ref": "#/components/schemas/TaskMeta"}, {"type": "object", "required": ["display"], "properties": {"display": {"$ref": "#/components/schemas/TaskDisplay"}}}]}, "SubmitModel": {"type": "object", "required": ["flag"], "properties": {"flag": {"type": "string"}}}, "SubmitResponse": {"type": "object", "required": ["task_id"], "properties": {"task_id": {"type": "string"}}}, "TaskAsset": {"type": "object", "required": ["description", "path"], "properties": {"description": {"type": "string"}, "path": {"type": "string"}}}, "TaskDisplay": {"type": "object", "required": ["icon_coordinates"], "properties": {"icon_coordinates": {"$ref": "#/components/schemas/Coordinates"}}}, "TaskMeta": {"type": "object", "required": ["id", "name", "labels", "difficulty_estimate"], "properties": {"difficulty_estimate": {"type": "string"}, "id": {"type": "string"}, "labels": {"type": "array", "items": {"type": "string"}}, "name": {"type": "string"}}}, "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"}}}, "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": {"confirmation_code": {"type": ["string", "null"], "format": "uuid"}, "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"]}, "confirmation_code": {"type": ["string", "null"], "format": "uuid"}, "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"}}}, "UserInformationResponse": {"type": "object", "required": ["username", "email", "has_personal_information"], "properties": {"email": {"type": "string"}, "has_personal_information": {"type": "boolean"}, "username": {"type": "string"}}}, "UserPersonalInformationSubmissionRequest": {"type": "object", "required": ["first_name", "birth_year", "location", "organization", "is_vegetarian", "marketing_consent"], "properties": {"birth_year": {"type": "integer", "format": "int32"}, "first_name": {"type": "string"}, "is_vegetarian": {"type": "boolean"}, "location": {"type": "string"}, "marketing_consent": {"type": "boolean"}, "organization": {"type": "string"}, "referral_source": {"type": ["array", "null"], "items": {"type": "string"}}}}, "UserRoles": {"type": "string", "enum": ["Default", "Admin", "Owner"]}, "Value": {}}, "securitySchemes": {"access_token": {"type": "apiKey", "in": "cookie", "name": "access_token"}}}} \ No newline at end of file From dd68d407e5cbdb834e24990b870f6463ab585427 Mon Sep 17 00:00:00 2001 From: lajczi Date: Mon, 2 Mar 2026 17:26:05 +0100 Subject: [PATCH 06/17] chore(backend): add max length validation to `callback` Signed-off-by: lajczi --- backend/src/routes/auth/register.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/routes/auth/register.rs b/backend/src/routes/auth/register.rs index 46b1717da..78db4f3cf 100644 --- a/backend/src/routes/auth/register.rs +++ b/backend/src/routes/auth/register.rs @@ -18,6 +18,7 @@ pub struct RegisterModel { pub email: String, #[validate(length(min = 8, max = 32))] pub password: Password, + #[validate(length(max = 256))] pub callback: Option, } From a140e36940373501d7c1f4a89cf66a5bf95086a4 Mon Sep 17 00:00:00 2001 From: lajczi Date: Tue, 3 Mar 2026 17:16:29 +0100 Subject: [PATCH 07/17] feat: add callback url to `submit_personal_info` page Signed-off-by: lajczi --- backend/src/routes/auth/register.rs | 3 ++- backend/src/utils/mod.rs | 1 + backend/src/utils/validation.rs | 8 ++++++++ frontend/app/middleware/auth.global.ts | 2 +- frontend/app/pages/account/submit_personal_info.vue | 4 +++- frontend/app/pages/panel/profile.vue | 2 +- 6 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 backend/src/utils/validation.rs diff --git a/backend/src/routes/auth/register.rs b/backend/src/routes/auth/register.rs index 78db4f3cf..c211a1ee3 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::auth::AuthService; 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; @@ -18,7 +19,7 @@ pub struct RegisterModel { pub email: String, #[validate(length(min = 8, max = 32))] pub password: Password, - #[validate(length(max = 256))] + #[validate(length(max = 256), custom(function = "validate_callback"))] pub callback: Option, } diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs index 61cc81261..a11d0fa62 100644 --- a/backend/src/utils/mod.rs +++ b/backend/src/utils/mod.rs @@ -12,3 +12,4 @@ pub mod openapi; pub mod points_counter; 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..839d17142 --- /dev/null +++ b/backend/src/utils/validation.rs @@ -0,0 +1,8 @@ +use validator::ValidationError; + +pub fn validate_callback(callback: &str) -> Result<(), ValidationError> { + if callback.starts_with('/') { + return Ok(()); + } + Err(ValidationError::new("invalid_callback")) +} diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index a1064a1ed..44eb46172 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -9,7 +9,7 @@ export default defineNuxtRouteMiddleware(async (to) => { } if (to.path.startsWith('/panel')) { if (data.value.has_personal_information === false) { - return '/account/submit_personal_info' + return await navigateTo({ path: '/account/submit_personal_info', query: { callback: to.fullPath } }) } } } catch { diff --git a/frontend/app/pages/account/submit_personal_info.vue b/frontend/app/pages/account/submit_personal_info.vue index 8a87bcfbd..307a2b3ba 100644 --- a/frontend/app/pages/account/submit_personal_info.vue +++ b/frontend/app/pages/account/submit_personal_info.vue @@ -1,4 +1,6 @@ diff --git a/frontend/app/pages/panel/profile.vue b/frontend/app/pages/panel/profile.vue index e2502a76c..86cb1de1c 100644 --- a/frontend/app/pages/panel/profile.vue +++ b/frontend/app/pages/panel/profile.vue @@ -47,7 +47,7 @@ async function logout() { Ustawienia konta
- + Zmień lub zobacz informacje o koncie From ff3c932ebd4d6edca2b6dac42c1b4d780606c22f Mon Sep 17 00:00:00 2001 From: lajczi Date: Sun, 19 Apr 2026 15:29:49 +0200 Subject: [PATCH 08/17] test(backend): add additional test coverage for callback url Signed-off-by: lajczi --- backend/tests/routes/auth.rs | 160 ++++++++++++++++++++++++++++++ frontend/openapi/api/openapi.json | 2 +- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/backend/tests/routes/auth.rs b/backend/tests/routes/auth.rs index ec40d4d12..602766b48 100644 --- a/backend/tests/routes/auth.rs +++ b/backend/tests/routes/auth.rs @@ -245,3 +245,163 @@ 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() { + 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}" + ); +} + +#[actix_web::test] +async fn email_confirmation_without_callback() { + 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" + } + }))), + 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"), + "Redirect should NOT 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", + "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", + "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/openapi/api/openapi.json b/frontend/openapi/api/openapi.json index b727d7111..f2986698f 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.1.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/get_personal_information":{"get":{"tags":["account"],"operationId":"user_get_personal_information","responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/submit_personal_information":{"post":{"tags":["account"],"operationId":"submit_personal_information","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPersonalInformationSubmissionRequest"}}},"required":true},"responses":{"200":{"description":"User personal information 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/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/tasks/refresh":{"post":{"tags":["admin/tasks"],"operationId":"admin_tasks_refresh","responses":{"200":{"description":"Tasks successfully refreshed."},"500":{"description":"Internal server error."}}}},"/admin/teams/clear_confirmation_code/{id}":{"delete":{"tags":["admin/teams"],"operationId":"clear_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully cleared."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/generate_confirmation_code/{id}":{"patch":{"tags":["admin/teams"],"operationId":"generate_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully generated."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/get_personal_information/{id}":{"get":{"tags":["admin/users"],"operationId":"admin_get_personal_information","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/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."}}}},"/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."},"500":{"description":"Internal server error."}}}},"/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/EventConfig"}}}}}}},"/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/registration":{"get":{"tags":["event"],"operationId":"registration","responses":{"200":{"description":"Correctly returned registration config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationConfig"}}}}}}},"/event/status":{"get":{"tags":["event"],"operationId":"status","responses":{"200":{"description":"Event is in progress"},"403":{"description":"You cannot access the event"}}}},"/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"}}}},"/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/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/list":{"get":{"tags":["tasks"],"operationId":"task_list","responses":{"200":{"description":"Tasks successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SimpleTask"}}}}},"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."}}}},"/teams/confirm/{confirmation_code}":{"post":{"tags":["teams"],"operationId":"confirm","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successfully confirmed team."},"404":{"description":"Invalid confirmation code"},"500":{"description":"Internal server error."}}}},"/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/invited_users":{"get":{"tags":["teams/management"],"operationId":"invited_users","responses":{"200":{"description":"Successfully retrieved invitations to team","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"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"}}}}},"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":{"$ref":"#/components/schemas/MyTeamWithMembers"}}}},"403":{"description":"User doesn't belong to any team."},"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"}}},"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":["x","y"],"properties":{"x":{"type":"integer","format":"int32"},"y":{"type":"integer","format":"int32"}}},"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"}}},"EmailMeta":{"type":"object","required":["subject"],"properties":{"sender_name":{"type":["string","null"]},"subject":{"type":"string"}}},"EmailSendTarget":{"oneOf":[{"type":"string","enum":["AllUsers"]},{"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"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/EventStage"}}}},"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"]},"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"}}},"JoinExternalModel":{"type":"object","required":["code"],"properties":{"code":{"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","first_name","birth_year","location","organization","is_vegetarian","marketing_consent","marketing_consent_accepted_at","marketing_consent_updated_at"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"marketing_consent_accepted_at":{"type":"string","format":"date-time"},"marketing_consent_updated_at":{"type":"string","format":"date-time"},"organization":{"type":"string"},"referral_source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value"}]}}},"MyTeamWithMembers":{"type":"object","required":["team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"Password":{"type":"string"},"RegisterModel":{"type":"object","required":["name","email","password"],"properties":{"callback":{"type":["string","null"]},"email":{"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"]},"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"}}},"SimpleTask":{"allOf":[{"$ref":"#/components/schemas/TaskMeta"},{"type":"object","required":["display"],"properties":{"display":{"$ref":"#/components/schemas/TaskDisplay"}}}]},"SubmitModel":{"type":"object","required":["flag"],"properties":{"flag":{"type":"string"}}},"SubmitResponse":{"type":"object","required":["task_id"],"properties":{"task_id":{"type":"string"}}},"TaskAsset":{"type":"object","required":["description","path"],"properties":{"description":{"type":"string"},"path":{"type":"string"}}},"TaskDisplay":{"type":"object","required":["icon_coordinates"],"properties":{"icon_coordinates":{"$ref":"#/components/schemas/Coordinates"}}},"TaskMeta":{"type":"object","required":["id","name","labels","difficulty_estimate"],"properties":{"difficulty_estimate":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"name":{"type":"string"}}},"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"}}},"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":{"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"]},"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"}}},"UserInformationResponse":{"type":"object","required":["username","email","has_personal_information"],"properties":{"email":{"type":"string"},"has_personal_information":{"type":"boolean"},"username":{"type":"string"}}},"UserPersonalInformationSubmissionRequest":{"type":"object","required":["first_name","birth_year","location","organization","is_vegetarian","marketing_consent"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"organization":{"type":"string"},"referral_source":{"type":["array","null"],"items":{"type":"string"}}}},"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.1.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/get_personal_information":{"get":{"tags":["account"],"operationId":"user_get_personal_information","responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/account/submit_personal_information":{"post":{"tags":["account"],"operationId":"submit_personal_information","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPersonalInformationSubmissionRequest"}}},"required":true},"responses":{"200":{"description":"User personal information 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/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/tasks/refresh":{"post":{"tags":["admin/tasks"],"operationId":"admin_tasks_refresh","responses":{"200":{"description":"Tasks successfully refreshed."},"500":{"description":"Internal server error."}}}},"/admin/teams/clear_confirmation_code/{id}":{"delete":{"tags":["admin/teams"],"operationId":"clear_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully cleared."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/generate_confirmation_code/{id}":{"patch":{"tags":["admin/teams"],"operationId":"generate_confirmation_code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Confirmation code successfully generated."},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/get_personal_information/{id}":{"get":{"tags":["admin/users"],"operationId":"admin_get_personal_information","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User personal information received.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Model"}]}}}},"500":{"description":"Internal server error."}},"security":[{"access_token":[]}]}},"/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/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."}}}},"/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/EventConfig"}}}}}}},"/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/registration":{"get":{"tags":["event"],"operationId":"registration","responses":{"200":{"description":"Correctly returned registration config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationConfig"}}}}}}},"/event/status":{"get":{"tags":["event"],"operationId":"status","responses":{"200":{"description":"Event is in progress"},"403":{"description":"You cannot access the event"}}}},"/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"}}}},"/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/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/list":{"get":{"tags":["tasks"],"operationId":"task_list","responses":{"200":{"description":"Tasks successfully retrieved.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SimpleTask"}}}}},"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."}}}},"/teams/confirm/{confirmation_code}":{"post":{"tags":["teams"],"operationId":"confirm","parameters":[{"name":"confirmation_code","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successfully confirmed team."},"404":{"description":"Invalid confirmation code"},"500":{"description":"Internal server error."}}}},"/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/invited_users":{"get":{"tags":["teams/management"],"operationId":"invited_users","responses":{"200":{"description":"Successfully retrieved invitations to team","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}},"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"}}}}},"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":{"$ref":"#/components/schemas/MyTeamWithMembers"}}}},"403":{"description":"User doesn't belong to any team."},"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"}}},"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":["x","y"],"properties":{"x":{"type":"integer","format":"int32"},"y":{"type":"integer","format":"int32"}}},"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"}}},"EmailMeta":{"type":"object","required":["subject"],"properties":{"sender_name":{"type":["string","null"]},"subject":{"type":"string"}}},"EmailSendTarget":{"oneOf":[{"type":"string","enum":["AllUsers"]},{"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"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/EventStage"}}}},"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"]},"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"}}},"JoinExternalModel":{"type":"object","required":["code"],"properties":{"code":{"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","first_name","birth_year","location","organization","is_vegetarian","marketing_consent","marketing_consent_accepted_at","marketing_consent_updated_at"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"id":{"type":"string","format":"uuid"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"marketing_consent_accepted_at":{"type":"string","format":"date-time"},"marketing_consent_updated_at":{"type":"string","format":"date-time"},"organization":{"type":"string"},"referral_source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Value"}]}}},"MyTeamWithMembers":{"type":"object","required":["team_name","created_at","members","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"status":{"$ref":"#/components/schemas/TeamStatus"},"team_name":{"type":"string"}}},"Password":{"type":"string"},"RegisterModel":{"type":"object","required":["name","email","password"],"properties":{"callback":{"type":["string","null"]},"email":{"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"]},"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"}}},"SimpleTask":{"allOf":[{"$ref":"#/components/schemas/TaskMeta"},{"type":"object","required":["display"],"properties":{"display":{"$ref":"#/components/schemas/TaskDisplay"}}}]},"SubmitModel":{"type":"object","required":["flag"],"properties":{"flag":{"type":"string"}}},"SubmitResponse":{"type":"object","required":["task_id"],"properties":{"task_id":{"type":"string"}}},"TaskAsset":{"type":"object","required":["description","path"],"properties":{"description":{"type":"string"},"path":{"type":"string"}}},"TaskDisplay":{"type":"object","required":["icon_coordinates"],"properties":{"icon_coordinates":{"$ref":"#/components/schemas/Coordinates"}}},"TaskMeta":{"type":"object","required":["id","name","labels","difficulty_estimate"],"properties":{"difficulty_estimate":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"type":"string"}},"name":{"type":"string"}}},"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"}}},"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":{"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"]},"confirmation_code":{"type":["string","null"],"format":"uuid"},"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"}}},"UserInformationResponse":{"type":"object","required":["username","email","has_personal_information"],"properties":{"email":{"type":"string"},"has_personal_information":{"type":"boolean"},"username":{"type":"string"}}},"UserPersonalInformationSubmissionRequest":{"type":"object","required":["first_name","birth_year","location","organization","is_vegetarian","marketing_consent"],"properties":{"birth_year":{"type":"integer","format":"int32"},"first_name":{"type":"string"},"is_vegetarian":{"type":"boolean"},"location":{"type":"string"},"marketing_consent":{"type":"boolean"},"organization":{"type":"string"},"referral_source":{"type":["array","null"],"items":{"type":"string"}}}},"UserRoles":{"type":"string","enum":["Default","Admin","Owner"]},"Value":{}},"securitySchemes":{"access_token":{"type":"apiKey","in":"cookie","name":"access_token"}}}} \ No newline at end of file From eb1e5f8e90d83cfa9e882842fca508212f028ef4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 08:51:47 +0000 Subject: [PATCH 09/17] refactor(backend): merge email_confirmation_without_callback into email_confirmation_success Consolidate duplicate test setup into the existing success test, adding the no-callback Refresh header assertion there instead of a separate test. https://claude.ai/code/session_013q6iJ5EWBJR6ZXAf3itbrd --- backend/tests/routes/auth.rs | 65 +++++++++--------------------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/backend/tests/routes/auth.rs b/backend/tests/routes/auth.rs index 602766b48..0c8800db1 100644 --- a/backend/tests/routes/auth.rs +++ b/backend/tests/routes/auth.rs @@ -144,13 +144,13 @@ async fn email_confirmation_success() { id: Set(confirmation_code), email: Set("".to_string()), action_type: Set("confirm_email_address".to_string()), - additional_data: Set(Some(json![{ + 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".to_string(), + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$nTzWdmrtGEOnwCocrg76xg$yv16FfDT5+meKwPmSiV+MF9kP8Man6bXZs+BloFTKIk" } - }])), + }))), expiration_time: Set(Some(Utc::now().naive_utc() + chrono::Duration::minutes(30))), created_at: Set(Utc::now().naive_utc()), }; @@ -168,6 +168,18 @@ async fn email_confirmation_success() { 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"), + "Redirect should NOT contain callback parameter, got: {refresh_header}" + ); } #[actix_web::test] @@ -294,53 +306,6 @@ async fn email_confirmation_with_callback() { ); } -#[actix_web::test] -async fn email_confirmation_without_callback() { - 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" - } - }))), - 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"), - "Redirect should NOT contain callback parameter, got: {refresh_header}" - ); -} - #[cfg(feature = "full-test-suite")] #[actix_web::test] async fn register_with_callback_persists_to_confirmation() { From 050407185b9262a6e2cd470c3ee396dc8a8fd1bb Mon Sep 17 00:00:00 2001 From: lajczi Date: Tue, 21 Apr 2026 17:09:13 +0200 Subject: [PATCH 10/17] fix: add validation message and safe callback query cast Signed-off-by: lajczi --- backend/src/utils/validation.rs | 3 ++- frontend/app/components/AuthForm.vue | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/utils/validation.rs b/backend/src/utils/validation.rs index 839d17142..5447718f7 100644 --- a/backend/src/utils/validation.rs +++ b/backend/src/utils/validation.rs @@ -4,5 +4,6 @@ pub fn validate_callback(callback: &str) -> Result<(), ValidationError> { if callback.starts_with('/') { return Ok(()); } - Err(ValidationError::new("invalid_callback")) + Err(ValidationError::new("invalid_callback") + .with_message("Callback URL must start with '/'".into())) } diff --git a/frontend/app/components/AuthForm.vue b/frontend/app/components/AuthForm.vue index 2b7f173ef..c22ba7838 100644 --- a/frontend/app/components/AuthForm.vue +++ b/frontend/app/components/AuthForm.vue @@ -18,7 +18,7 @@ const toast = useToast() const OAuthBaseUrl = `${useRuntimeConfig().public.openFetch.api.baseURL}/auth/oauth` const route = useRoute() -const callback = route.query.callback as string | undefined +const callback = route.query.callback?.toString() if (route.query.redirect_from_confirmation === 'true' && import.meta.client) { toast.add({ From 55fb2459a0864e8293431f2be1147e5efb7f5fc1 Mon Sep 17 00:00:00 2001 From: lajczi Date: Tue, 21 Apr 2026 17:14:44 +0200 Subject: [PATCH 11/17] ... Signed-off-by: lajczi --- frontend/app/pages/account/submit_personal_info.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/pages/account/submit_personal_info.vue b/frontend/app/pages/account/submit_personal_info.vue index 307a2b3ba..5c6fbaa14 100644 --- a/frontend/app/pages/account/submit_personal_info.vue +++ b/frontend/app/pages/account/submit_personal_info.vue @@ -1,5 +1,5 @@