Merged develop into feature/VBV-768-doc

This commit is contained in:
Christian Cueni 2024-10-23 13:44:07 +00:00
commit 82f9d66903
18 changed files with 71 additions and 48 deletions

View File

@ -219,6 +219,7 @@
"a.Personen, die du begleitest": "Personen, die du begleitest ", "a.Personen, die du begleitest": "Personen, die du begleitest ",
"a.Persönliche Informationen": "Persönliche Informationen", "a.Persönliche Informationen": "Persönliche Informationen",
"a.PLZ": "PLZ", "a.PLZ": "PLZ",
"a.Postleizahl hat das falsche Format": "Postleizahl hat das falsche Format",
"a.Praxisauftrag": "Praxisauftrag", "a.Praxisauftrag": "Praxisauftrag",
"a.Praxisaufträge anschauen": "Praxisaufträge anschauen", "a.Praxisaufträge anschauen": "Praxisaufträge anschauen",
"a.Praxisbildner": "Praxisbildner", "a.Praxisbildner": "Praxisbildner",

View File

@ -219,6 +219,7 @@
"a.Personen, die du begleitest": "Personnes que tu accompagnes", "a.Personen, die du begleitest": "Personnes que tu accompagnes",
"a.Persönliche Informationen": "Informations personnelles", "a.Persönliche Informationen": "Informations personnelles",
"a.PLZ": "Code postal", "a.PLZ": "Code postal",
"a.Postleizahl hat das falsche Format": "Le code postal n'a pas le bon format",
"a.Praxisauftrag": "Exercice pratique", "a.Praxisauftrag": "Exercice pratique",
"a.Praxisaufträge anschauen": "Voir les missions pratiques", "a.Praxisaufträge anschauen": "Voir les missions pratiques",
"a.Praxisbildner": "Formateur pratique", "a.Praxisbildner": "Formateur pratique",

View File

@ -219,6 +219,7 @@
"a.Personen, die du begleitest": "Persone che accompagni", "a.Personen, die du begleitest": "Persone che accompagni",
"a.Persönliche Informationen": "Informazioni personali", "a.Persönliche Informationen": "Informazioni personali",
"a.PLZ": "CAP", "a.PLZ": "CAP",
"a.Postleizahl hat das falsche Format": "Il codice postale ha un formato sbagliato",
"a.Praxisauftrag": "Lavoro pratico", "a.Praxisauftrag": "Lavoro pratico",
"a.Praxisaufträge anschauen": "Visualizzare gli incarichi pratici", "a.Praxisaufträge anschauen": "Visualizzare gli incarichi pratici",
"a.Praxisbildner": "Formatore pratico", "a.Praxisbildner": "Formatore pratico",

View File

@ -15,6 +15,7 @@ import { useEntities } from "@/services/entities";
import { getLocalSessionKey } from "@/statistics"; import { getLocalSessionKey } from "@/statistics";
import { type User, useUserStore } from "@/stores/user"; import { type User, useUserStore } from "@/stores/user";
import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone"; import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone";
import { validatePostalCode } from "@/utils/postalcode";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import log from "loglevel"; import log from "loglevel";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
@ -129,6 +130,8 @@ function validateAddress() {
if (!address.value.postal_code) { if (!address.value.postal_code) {
formErrors.value.personal.push(t("a.PLZ")); 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) { if (!address.value.city) {
@ -172,6 +175,8 @@ function validateAddress() {
if (!address.value.organisation_postal_code) { if (!address.value.organisation_postal_code) {
formErrors.value.company.push(t("a.PLZ")); 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) { if (!address.value.organisation_city) {

View File

@ -5,14 +5,6 @@ import { directUpload } from "@/services/files";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { defineStore } from "pinia"; 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"]; const AVAILABLE_LANGUAGES = ["de", "fr", "it"];
export type AvailableLanguages = "de" | "fr" | "it"; export type AvailableLanguages = "de" | "fr" | "it";
@ -150,16 +142,7 @@ export const useUserStore = defineStore({
handleLogout() { handleLogout() {
Object.assign(this, initialUserState); Object.assign(this, initialUserState);
itPost("/api/core/logout/", {}).then(() => { window.location.href = `${window.location.origin}/sso/logout`;
let redirectUrl;
if (logoutRedirectUrl !== "") {
redirectUrl = logoutRedirectUrl;
} else {
redirectUrl = "/";
}
window.location.href = redirectUrl;
});
}, },
async fetchUser() { async fetchUser() {
const data: any = await itGetCached("/api/core/me/"); const data: any = await itGetCached("/api/core/me/");

View File

@ -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);
}

Binary file not shown.

Binary file not shown.

View File

@ -639,6 +639,8 @@ OAUTH_SIGNIN_REDIRECT_URI = env(
"OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback" "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_URL = env("OAUTH_SIGNIN_URL", default="")
OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv") OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv")
OAUTH_SIGNIN_ADMIN_CLIENT_ID = env("OAUTH_SIGNIN_ADMIN_CLIENT_ID", default="") OAUTH_SIGNIN_ADMIN_CLIENT_ID = env("OAUTH_SIGNIN_ADMIN_CLIENT_ID", default="")

View File

@ -29,7 +29,6 @@ from vbv_lernwelt.core.views import (
rate_limit_exceeded_view, rate_limit_exceeded_view,
vue_home, vue_home,
vue_login, vue_login,
vue_logout,
) )
from vbv_lernwelt.course.views import ( from vbv_lernwelt.course.views import (
course_page_api_view, course_page_api_view,
@ -124,7 +123,6 @@ urlpatterns = [
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login), re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
name='vue_login'), name='vue_login'),
re_path(r'api/core/logout/$', vue_logout, name='vue_logout'),
# notifications # notifications
re_path(r'^notifications/', include(notifications.urls, namespace='notifications')), re_path(r'^notifications/', include(notifications.urls, namespace='notifications')),

View File

@ -5,7 +5,7 @@ from pathlib import Path
import requests import requests
import structlog import structlog
from django.conf import settings 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.core.management import call_command
from django.http import ( from django.http import (
HttpResponse, 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): def permission_denied_view(request, exception):
return render(request, "403.html", status=403) return render(request, "403.html", status=403)

View File

@ -4,9 +4,7 @@ from rest_framework.response import Response
from vbv_lernwelt.course.models import CircleDocument from vbv_lernwelt.course.models import CircleDocument
from vbv_lernwelt.course.serializers import CircleDocumentSerializer from vbv_lernwelt.course.serializers import CircleDocumentSerializer
from vbv_lernwelt.iam.permissions import ( from vbv_lernwelt.iam.permissions import has_course_session_document_access
has_course_session_document_access,
)
@api_view(["GET"]) @api_view(["GET"])

View File

@ -30,9 +30,7 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import (
from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState
from vbv_lernwelt.shop.views import ( from vbv_lernwelt.shop.views import COURSE_SESSION_ID_TO_PRODUCT_SKU
COURSE_SESSION_ID_TO_PRODUCT_SKU,
)
class StatisticsCourseSessionDataType(graphene.ObjectType): class StatisticsCourseSessionDataType(graphene.ObjectType):

View File

@ -24,10 +24,7 @@ from vbv_lernwelt.competence.services import (
query_competence_course_session_edoniq_tests, query_competence_course_session_edoniq_tests,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import CourseConfiguration, CourseSessionUser
CourseConfiguration,
CourseSessionUser,
)
from vbv_lernwelt.course.views import logger from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session.services.export_attendance import ( from vbv_lernwelt.course_session.services.export_attendance import (
ATTENDANCE_EXPORT_FILENAME, ATTENDANCE_EXPORT_FILENAME,

View File

@ -7,9 +7,7 @@ from vbv_lernwelt.course.creators.test_utils import (
) )
from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.iam.permissions import ( from vbv_lernwelt.iam.permissions import has_course_session_document_access
has_course_session_document_access,
)
from vbv_lernwelt.learning_mentor.models import ( from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation, AgentParticipantRelation,
AgentParticipantRoleType, AgentParticipantRoleType,

View File

@ -496,6 +496,7 @@ def create_or_update_user(
contract_number: str = "", contract_number: str = "",
date_of_birth: str = "", date_of_birth: str = "",
intermediate_sso_id: str = "", # from keycloak intermediate_sso_id: str = "", # from keycloak
id_token: str = "", # used for sso logout
) -> User: ) -> User:
logger.debug( logger.debug(
"create_or_update_user", "create_or_update_user",
@ -544,6 +545,9 @@ def create_or_update_user(
user.update_additional_json_data({"intermediate_sso_id": intermediate_sso_id}) user.update_additional_json_data({"intermediate_sso_id": intermediate_sso_id})
init_notification_settings(user) init_notification_settings(user)
if id_token:
user.update_additional_json_data({"id_token": id_token})
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()

View File

@ -12,4 +12,9 @@ urlpatterns = [
django_view_authentication_exempt(views.authorize_signin), django_view_authentication_exempt(views.authorize_signin),
name="authorize", name="authorize",
), ),
path(
r"logout/",
django_view_authentication_exempt(views.logout),
name="logout",
),
] ]

View File

@ -5,6 +5,7 @@ import structlog as structlog
from authlib.integrations.base_client import OAuthError from authlib.integrations.base_client import OAuthError
from django.conf import settings from django.conf import settings
from django.contrib.auth import login as dj_login from django.contrib.auth import login as dj_login
from django.contrib.auth import logout as dj_logout
from django.shortcuts import redirect from django.shortcuts import redirect
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
@ -97,7 +98,7 @@ def authorize_signin(request):
capture_exception(e) capture_exception(e)
return redirect("/") return redirect("/")
id_token = decode_jwt(jwt_token["id_token"]) id_token_decoded = decode_jwt(jwt_token["id_token"])
state = json.loads( state = json.loads(
base64.urlsafe_b64decode(request.GET.get("state").encode()).decode() base64.urlsafe_b64decode(request.GET.get("state").encode()).decode()
@ -108,17 +109,44 @@ def authorize_signin(request):
logger.debug( logger.debug(
f"SSO Authorize (course={course}, next={next_url}", 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( user = create_or_update_user(
email=id_token.get("email", ""), email=id_token_decoded.get("email", ""),
sso_id=id_token.get("oid"), sso_id=id_token_decoded.get("oid"),
first_name=id_token.get("given_name", ""), first_name=id_token_decoded.get("given_name", ""),
last_name=id_token.get("family_name", ""), last_name=id_token_decoded.get("family_name", ""),
intermediate_sso_id=id_token.get("sub"), intermediate_sso_id=id_token_decoded.get("sub"),
id_token=jwt_token["id_token"],
) )
dj_login(request, user) dj_login(request, user)
return get_redirect_uri(user=user, course=course, next_url=next_url) 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}")