diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index e02c702e..d9e2b6b3 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -219,6 +219,7 @@ "a.Personen, die du begleitest": "Personen, die du begleitest ", "a.Persönliche Informationen": "Persönliche Informationen", "a.PLZ": "PLZ", + "a.Postleizahl hat das falsche Format": "Postleizahl hat das falsche Format", "a.Praxisauftrag": "Praxisauftrag", "a.Praxisaufträge anschauen": "Praxisaufträge anschauen", "a.Praxisbildner": "Praxisbildner", diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index 3de4022c..ef3c306a 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -219,6 +219,7 @@ "a.Personen, die du begleitest": "Personnes que tu accompagnes", "a.Persönliche Informationen": "Informations personnelles", "a.PLZ": "Code postal", + "a.Postleizahl hat das falsche Format": "Le code postal n'a pas le bon format", "a.Praxisauftrag": "Exercice pratique", "a.Praxisaufträge anschauen": "Voir les missions pratiques", "a.Praxisbildner": "Formateur pratique", diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index 30f04667..da81b106 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -219,6 +219,7 @@ "a.Personen, die du begleitest": "Persone che accompagni", "a.Persönliche Informationen": "Informazioni personali", "a.PLZ": "CAP", + "a.Postleizahl hat das falsche Format": "Il codice postale ha un formato sbagliato", "a.Praxisauftrag": "Lavoro pratico", "a.Praxisaufträge anschauen": "Visualizzare gli incarichi pratici", "a.Praxisbildner": "Formatore pratico", diff --git a/client/src/pages/onboarding/vv/CheckoutAddress.vue b/client/src/pages/onboarding/vv/CheckoutAddress.vue index 89d71557..cb1b7b72 100644 --- a/client/src/pages/onboarding/vv/CheckoutAddress.vue +++ b/client/src/pages/onboarding/vv/CheckoutAddress.vue @@ -15,6 +15,7 @@ import { useEntities } from "@/services/entities"; import { getLocalSessionKey } from "@/statistics"; import { type User, useUserStore } from "@/stores/user"; import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone"; +import { validatePostalCode } from "@/utils/postalcode"; import { useTranslation } from "i18next-vue"; import log from "loglevel"; import { computed, ref, watch } from "vue"; @@ -129,6 +130,8 @@ function validateAddress() { if (!address.value.postal_code) { formErrors.value.personal.push(t("a.PLZ")); + } else if (!validatePostalCode(address.value.postal_code)) { + formErrors.value.personal.push(t("a.Postleizahl hat das falsche Format")); } if (!address.value.city) { @@ -172,6 +175,8 @@ function validateAddress() { if (!address.value.organisation_postal_code) { formErrors.value.company.push(t("a.PLZ")); + } else if (!validatePostalCode(address.value.organisation_postal_code)) { + formErrors.value.personal.push(t("a.Postleizahl hat das falsche Format")); } if (!address.value.organisation_city) { diff --git a/client/src/stores/user.ts b/client/src/stores/user.ts index 1f41d130..d8413709 100644 --- a/client/src/stores/user.ts +++ b/client/src/stores/user.ts @@ -5,14 +5,6 @@ import { directUpload } from "@/services/files"; import dayjs from "dayjs"; import { defineStore } from "pinia"; -let logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT || "/"; - -if (import.meta.env.VITE_OAUTH_API_BASE_URL) { - logoutRedirectUrl = `${ - import.meta.env.VITE_OAUTH_API_BASE_URL - }logout/?post_logout_redirect_uri=${window.location.origin}&client_id=iterativ`; -} - const AVAILABLE_LANGUAGES = ["de", "fr", "it"]; export type AvailableLanguages = "de" | "fr" | "it"; @@ -150,16 +142,7 @@ export const useUserStore = defineStore({ handleLogout() { Object.assign(this, initialUserState); - itPost("/api/core/logout/", {}).then(() => { - let redirectUrl; - - if (logoutRedirectUrl !== "") { - redirectUrl = logoutRedirectUrl; - } else { - redirectUrl = "/"; - } - window.location.href = redirectUrl; - }); + window.location.href = `${window.location.origin}/sso/logout`; }, async fetchUser() { const data: any = await itGetCached("/api/core/me/"); diff --git a/client/src/utils/postalcode.ts b/client/src/utils/postalcode.ts new file mode 100644 index 00000000..59fdac69 --- /dev/null +++ b/client/src/utils/postalcode.ts @@ -0,0 +1,10 @@ +export function validatePostalCode(input: string) { + // Remove non-ASCII characters + // eslint-disable-next-line no-control-regex + input = input.replace(/[^\x00-\x7F]/g, ""); + if (input.length < 4) { + return false; + } + const regex = /^[0-9]+$/; + return regex.test(input); +} diff --git a/env_secrets/caprover_myvbv-prod.env b/env_secrets/caprover_myvbv-prod.env index e57d1105..13eec0c7 100644 Binary files a/env_secrets/caprover_myvbv-prod.env and b/env_secrets/caprover_myvbv-prod.env differ diff --git a/env_secrets/local_chrigu.env b/env_secrets/local_chrigu.env index f8d1f1fe..6c686f35 100644 Binary files a/env_secrets/local_chrigu.env and b/env_secrets/local_chrigu.env differ diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 2f79e970..68710668 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -639,6 +639,8 @@ OAUTH_SIGNIN_REDIRECT_URI = env( "OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback" ) +OAUTH_LOGOUT_REDIRECT_URI = env("OAUTH_LOGOUT_REDIRECT_URI", default="") + OAUTH_SIGNIN_URL = env("OAUTH_SIGNIN_URL", default="") OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv") OAUTH_SIGNIN_ADMIN_CLIENT_ID = env("OAUTH_SIGNIN_ADMIN_CLIENT_ID", default="") diff --git a/server/config/urls.py b/server/config/urls.py index 0077db32..7789d41b 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -29,7 +29,6 @@ from vbv_lernwelt.core.views import ( rate_limit_exceeded_view, vue_home, vue_login, - vue_logout, ) from vbv_lernwelt.course.views import ( course_page_api_view, @@ -124,7 +123,6 @@ urlpatterns = [ re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'), - re_path(r'api/core/logout/$', vue_logout, name='vue_logout'), # notifications re_path(r'^notifications/', include(notifications.urls, namespace='notifications')), @@ -241,7 +239,7 @@ urlpatterns = [ # testing and debug path('server/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')( - raise_example_error) ), + raise_example_error)), path("server/checkratelimit/", check_rate_limit), ] diff --git a/server/vbv_lernwelt/core/views.py b/server/vbv_lernwelt/core/views.py index 191ccc40..896041af 100644 --- a/server/vbv_lernwelt/core/views.py +++ b/server/vbv_lernwelt/core/views.py @@ -5,7 +5,7 @@ from pathlib import Path import requests import structlog from django.conf import settings -from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import authenticate, login from django.core.management import call_command from django.http import ( HttpResponse, @@ -96,12 +96,6 @@ def vue_login(request): ) -@api_view(["POST"]) -def vue_logout(request): - logout(request) - return Response({"success": True}, 200) - - def permission_denied_view(request, exception): return render(request, "403.html", status=403) diff --git a/server/vbv_lernwelt/course_session/views.py b/server/vbv_lernwelt/course_session/views.py index 8839da3b..b3d45e29 100644 --- a/server/vbv_lernwelt/course_session/views.py +++ b/server/vbv_lernwelt/course_session/views.py @@ -4,9 +4,7 @@ from rest_framework.response import Response from vbv_lernwelt.course.models import CircleDocument from vbv_lernwelt.course.serializers import CircleDocumentSerializer -from vbv_lernwelt.iam.permissions import ( - has_course_session_document_access, -) +from vbv_lernwelt.iam.permissions import has_course_session_document_access @api_view(["GET"]) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 43d2b56d..95480e26 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -30,9 +30,7 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import ( from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState -from vbv_lernwelt.shop.views import ( - COURSE_SESSION_ID_TO_PRODUCT_SKU, -) +from vbv_lernwelt.shop.views import COURSE_SESSION_ID_TO_PRODUCT_SKU class StatisticsCourseSessionDataType(graphene.ObjectType): diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index d7e54ec5..16d2608a 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -24,10 +24,7 @@ from vbv_lernwelt.competence.services import ( query_competence_course_session_edoniq_tests, ) from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import ( - CourseConfiguration, - CourseSessionUser, -) +from vbv_lernwelt.course.models import CourseConfiguration, CourseSessionUser from vbv_lernwelt.course.views import logger from vbv_lernwelt.course_session.services.export_attendance import ( ATTENDANCE_EXPORT_FILENAME, diff --git a/server/vbv_lernwelt/iam/tests/test_permissions.py b/server/vbv_lernwelt/iam/tests/test_permissions.py index 2678502e..5194710b 100644 --- a/server/vbv_lernwelt/iam/tests/test_permissions.py +++ b/server/vbv_lernwelt/iam/tests/test_permissions.py @@ -7,9 +7,7 @@ from vbv_lernwelt.course.creators.test_utils import ( ) from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session_group.models import CourseSessionGroup -from vbv_lernwelt.iam.permissions import ( - has_course_session_document_access, -) +from vbv_lernwelt.iam.permissions import has_course_session_document_access from vbv_lernwelt.learning_mentor.models import ( AgentParticipantRelation, AgentParticipantRoleType, diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 2e5894d5..5f8beab4 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -496,6 +496,7 @@ def create_or_update_user( contract_number: str = "", date_of_birth: str = "", intermediate_sso_id: str = "", # from keycloak + id_token: str = "", # used for sso logout ) -> User: logger.debug( "create_or_update_user", @@ -544,6 +545,9 @@ def create_or_update_user( user.update_additional_json_data({"intermediate_sso_id": intermediate_sso_id}) init_notification_settings(user) + if id_token: + user.update_additional_json_data({"id_token": id_token}) + user.set_unusable_password() user.save() diff --git a/server/vbv_lernwelt/sso/urls.py b/server/vbv_lernwelt/sso/urls.py index ba607c3f..9713edfe 100644 --- a/server/vbv_lernwelt/sso/urls.py +++ b/server/vbv_lernwelt/sso/urls.py @@ -12,4 +12,9 @@ urlpatterns = [ django_view_authentication_exempt(views.authorize_signin), name="authorize", ), + path( + r"logout/", + django_view_authentication_exempt(views.logout), + name="logout", + ), ] diff --git a/server/vbv_lernwelt/sso/views.py b/server/vbv_lernwelt/sso/views.py index a665d7af..bae5040b 100644 --- a/server/vbv_lernwelt/sso/views.py +++ b/server/vbv_lernwelt/sso/views.py @@ -5,6 +5,7 @@ import structlog as structlog from authlib.integrations.base_client import OAuthError from django.conf import settings from django.contrib.auth import login as dj_login +from django.contrib.auth import logout as dj_logout from django.shortcuts import redirect from sentry_sdk import capture_exception @@ -97,7 +98,7 @@ def authorize_signin(request): capture_exception(e) return redirect("/") - id_token = decode_jwt(jwt_token["id_token"]) + id_token_decoded = decode_jwt(jwt_token["id_token"]) state = json.loads( base64.urlsafe_b64decode(request.GET.get("state").encode()).decode() @@ -108,17 +109,44 @@ def authorize_signin(request): logger.debug( f"SSO Authorize (course={course}, next={next_url}", - sso_authorize_id_token=id_token, + sso_authorize_id_token=id_token_decoded, ) user = create_or_update_user( - email=id_token.get("email", ""), - sso_id=id_token.get("oid"), - first_name=id_token.get("given_name", ""), - last_name=id_token.get("family_name", ""), - intermediate_sso_id=id_token.get("sub"), + email=id_token_decoded.get("email", ""), + sso_id=id_token_decoded.get("oid"), + first_name=id_token_decoded.get("given_name", ""), + last_name=id_token_decoded.get("family_name", ""), + intermediate_sso_id=id_token_decoded.get("sub"), + id_token=jwt_token["id_token"], ) dj_login(request, user) return get_redirect_uri(user=user, course=course, next_url=next_url) + + +def logout(request): + user_data = ( + request.user.additional_json_data if request.user.is_authenticated else {} + ) + dj_logout(request) + + redirect_uri = getattr(settings, "OAUTH_LOGOUT_REDIRECT_URI", "/") + + # Handle scenarios when SSO-related data is missing, assume local login + if not user_data.get("intermediate_sso_id"): + logger.debug("SSO Logout", extra={"mode": "intermediate_sso_id_not_set"}) + return redirect("/") + + # Temporary solution if id_token is not set in the user data, can be removed after rollout + 2 weeks + id_token = user_data.get("id_token", "") + if not id_token: + logger.debug("SSO Logout", extra={"mode": "id_token_not_set"}) + return redirect(f"{redirect_uri}&client_id=iterativ") + + # Handle scenarios when SSO-related data is present or redirect_uri is not set + if not redirect_uri: + logger.debug("SSO Logout", extra={"mode": "redirect_uri_not_set"}) + return redirect("/") + return redirect(f"{redirect_uri}&id_token_hint={id_token}")