Merged develop into feature/VBV-768-doc
This commit is contained in:
commit
82f9d66903
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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/");
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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="")
|
||||||
|
|
|
||||||
|
|
@ -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')),
|
||||||
|
|
@ -241,7 +239,7 @@ urlpatterns = [
|
||||||
# testing and debug
|
# testing and debug
|
||||||
path('server/raise_error/',
|
path('server/raise_error/',
|
||||||
user_passes_test(lambda u: u.is_superuser, login_url='/login/')(
|
user_passes_test(lambda u: u.is_superuser, login_url='/login/')(
|
||||||
raise_example_error) ),
|
raise_example_error)),
|
||||||
path("server/checkratelimit/", check_rate_limit),
|
path("server/checkratelimit/", check_rate_limit),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue