Merge branch 'develop' of bitbucket.org:iterativ/vbv_lernwelt into develop

This commit is contained in:
Lorenz Padberg 2022-08-18 15:27:30 +02:00
commit 5af1041a12
13 changed files with 115 additions and 61 deletions

View File

@ -13,10 +13,9 @@ set -ev
npm run build npm run build
# create and push new docker container # create and push new docker container
docker buildx build --platform=linux/amd64 -f compose/django/Dockerfile -t "$TAG" -t "$LATEST" --build-arg VERSION="$VERSION" --build-arg BUILD_TIMESTAMP="$BUILD_TIMESTAMP" . # docker buildx build --platform=linux/amd64 -f compose/django/Dockerfile -t "$TAG" -t "$LATEST" --build-arg VERSION="$VERSION" --build-arg BUILD_TIMESTAMP="$BUILD_TIMESTAMP" .
docker buildx build --platform=linux/amd64 -f compose/django/Dockerfile .
docker push iterativ/vbv-lernwelt-django docker push iterativ/vbv-lernwelt-django
docker push "$TAG"
docker push "$LATEST"
# deploy to caprover # deploy to caprover
caprover deploy -h https://captain.control.iterativ.ch -a vbv-lernwelt -i docker.io/iterativ/vbv-lernwelt-django caprover deploy -h https://captain.control.iterativ.ch -a vbv-lernwelt -i docker.io/iterativ/vbv-lernwelt-django

View File

@ -24,12 +24,12 @@ function toggleNav() {
state.showMenu = !state.showMenu; state.showMenu = !state.showMenu;
} }
function menuActive(checkPath) { function isInRoutePath(checkPaths: string[]) {
return route.path.startsWith(checkPath); return checkPaths.some((path) => route.path.startsWith(path))
} }
function inLearningPath() { function inLearningPath() {
return route.path.startsWith('/learningpath/') || route.path.startsWith('/circle/'); return isInRoutePath(['/learningpath/', '/circle/']);
} }
function getLearningPathStringProp (prop: 'title' | 'slug'): string { function getLearningPathStringProp (prop: 'title' | 'slug'): string {
@ -58,13 +58,17 @@ function handleDropdownSelect(data) {
router.push('/profile') router.push('/profile')
break break
case 'logout': case 'logout':
router.push('/logout') userStore.handleLogout();
break break
default: default:
console.log('no action') console.log('no action')
} }
} }
function logout () {
userStore.handleLogout();
}
onMounted(() => { onMounted(() => {
log.debug('MainNavigationBar mounted'); log.debug('MainNavigationBar mounted');
}) })
@ -96,7 +100,7 @@ const profileDropdownData = [
<div> <div>
<Teleport to="body"> <Teleport to="body">
<MobileMenu <MobileMenu
:user="userStore" :user-store="userStore"
:show="state.showMenu" :show="state.showMenu"
:learning-path-slug="learninPathSlug()" :learning-path-slug="learninPathSlug()"
:learning-path-name="learningPathName()" :learning-path-name="learningPathName()"
@ -139,6 +143,7 @@ const profileDropdownData = [
<div class="flex items-center lg:hidden"> <div class="flex items-center lg:hidden">
<router-link <router-link
v-if="userStore.loggedIn"
to="/messages" to="/messages"
class="nav-item flex flex-row items-center" class="nav-item flex flex-row items-center"
> >
@ -164,7 +169,7 @@ const profileDropdownData = [
<!-- Mobile Menu open: "block", Menu closed: "hidden" --> <!-- Mobile Menu open: "block", Menu closed: "hidden" -->
<div <div
v-if="appStore.userLoaded && appStore.routingFinished && userStore.loggedIn " v-if="appStore.userLoaded && appStore.routingFinished && userStore.loggedIn"
:class="state.showMenu ? 'flex' : 'hidden'" :class="state.showMenu ? 'flex' : 'hidden'"
class=" class="
flex-auto flex-auto
@ -176,7 +181,7 @@ const profileDropdownData = [
v-if="inLearningPath()" v-if="inLearningPath()"
to="/learningpath/versicherungsvermittlerin" to="/learningpath/versicherungsvermittlerin"
class="nav-item" class="nav-item"
:class="{'nav-item--active': menuActive('/learningpath/')}" :class="{'nav-item--active': inLearningPath()}"
> >
Lernpfad Lernpfad
</router-link> </router-link>
@ -185,7 +190,7 @@ const profileDropdownData = [
v-if="inLearningPath()" v-if="inLearningPath()"
to="/competences/" to="/competences/"
class="nav-item" class="nav-item"
:class="{'nav-item--active': menuActive('/competences/')}" :class="{'nav-item--active': isInRoutePath(['/competences/'])}"
> >
Kompetenzprofil Kompetenzprofil
</router-link> </router-link>
@ -194,14 +199,14 @@ const profileDropdownData = [
<router-link <router-link
to="/shop" to="/shop"
class="nav-item" class="nav-item"
:class="{'nav-item--active': menuActive('/shop')}" :class="{'nav-item--active': isInRoutePath(['/shop'])}"
> >
Shop Shop
</router-link> </router-link>
<router-link <router-link
to="/mediacenter" to="/mediacenter"
class="nav-item" class="nav-item"
:class="{'nav-item--active': menuActive('/mediacenter')}" :class="{'nav-item--active': isInRoutePath(['/mediacenter'])}"
> >
Mediathek Mediathek
</router-link> </router-link>

View File

@ -8,7 +8,7 @@ const router = useRouter()
const props = defineProps<{ const props = defineProps<{
show: boolean, show: boolean,
user: object, userStore: object,
learningPathName: string, learningPathName: string,
learningPathSlug: string learningPathSlug: string
}>() }>()
@ -29,19 +29,25 @@ const clickLink = (to: string) => {
> >
<div> <div>
<div> <div>
<div class="flex border-b border-gray-500 -mx-8 px-8 pb-4"> <div
<div v-if="user.avatar_url"> v-if="userStore.loggedIn"
<img class="inline-block h-16 w-16 rounded-full" class="border-b border-gray-500 -mx-4 px-8 pb-4">
:src="user.avatar_url" <div class="-ml-4 flex">
alt=""/> <div
</div> v-if="userStore.avatar_url"
<div class="ml-6"> >
<h3>{{user.first_name}} {{user.last_name}}</h3> <img class="inline-block h-16 w-16 rounded-full"
<button :src="userStore.avatar_url"
@click="clickLink('/profile')" alt=""/>
class="mt-2 inline-block items-center"> </div>
<IconSettings class="inline-block" /><span class="ml-3">Kontoeinstellungen</span> <div class="ml-6">
</button> <h3>{{userStore.first_name}} {{userStore.last_name}}</h3>
<button
@click="clickLink('/profile')"
class="mt-2 inline-block flex items-center">
<IconSettings class="inline-block" /><span class="ml-3">Kontoeinstellungen</span>
</button>
</div>
</div> </div>
</div> </div>
<div> <div>
@ -50,7 +56,7 @@ const clickLink = (to: string) => {
v-if="learningPathName"> v-if="learningPathName">
<h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4> <h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4>
<ul class="mt-6"> <ul class="mt-6">
<li><button @click="clickLink('/learningpath')">Lernpfad</button></li> <li><button @click="clickLink(`/learningpath/${learningPathSlug}`)">Lernpfad</button></li>
<li class="mt-6">Kompetenzprofil</li> <li class="mt-6">Kompetenzprofil</li>
</ul> </ul>
</div> </div>
@ -60,9 +66,13 @@ const clickLink = (to: string) => {
<li class="mt-6">Mediathek</li> <li class="mt-6">Mediathek</li>
</ul> </ul>
</div> </div>
<div class="mt-6 items-center"> <button
v-if="userStore.loggedIn"
type="button"
@click="userStore.handleLogout()"
class="mt-6 items-center flex">
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span> <IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</div> </button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,23 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
// inspiration https://vuejs.org/examples/#modal // inspiration https://vuejs.org/examples/#modal
import {onMounted, watch} from "vue";
import {HTMLElement} from "happy-dom";
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
}>() }>()
const emits = defineEmits(['closemodal']) const emits = defineEmits(['closemodal'])
let appElement: HTMLElement | null = null;
watch(() => props.show,
(isShown) => isShown && appElement ? appElement.classList.add('no-scroll') : null
)
onMounted(() => {
appElement = document.getElementById('app');
})
const closeModal = () => {
if (appElement) {
appElement.classList.remove('no-scroll')
}
emits('closemodal')
}
</script> </script>
<template> <template>
<Transition mode="in-out"> <Transition mode="in-out">
<div <div
v-if="show" v-if="show"
class="circle-overview px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full"> class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full">
<button <button
type="button" type="button"
class="w-8 h-8 absolute right-4 top-4 cursor-pointer" class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
@click="$emit('closemodal')" @click="closeModal"
> >
<it-icon-close></it-icon-close> <it-icon-close></it-icon-close>
</button> </button>

View File

@ -14,16 +14,18 @@ export type UserState = {
loggedIn: boolean; loggedIn: boolean;
} }
export const useUserStore = defineStore({ const initialUserState: UserState = {
id: 'user',
state: () => ({
email: '', email: '',
first_name: '', first_name: '',
last_name: '', last_name: '',
username: '', username: '',
avatar_url: '', avatar_url: '',
loggedIn: false loggedIn: false
} as UserState), }
export const useUserStore = defineStore({
id: 'user',
state: () => (initialUserState as UserState),
getters: { getters: {
getFullName(): string { getFullName(): string {
return `${this.first_name} ${this.last_name}`.trim(); return `${this.first_name} ${this.last_name}`.trim();
@ -46,6 +48,13 @@ export const useUserStore = defineStore({
}); });
} }
}, },
handleLogout() {
itPost('/core/logout/', {})
.then(data => {
Object.assign(this, initialUserState);
window.location.href = '/';
})
},
fetchUser() { fetchUser() {
const appStore = useAppStore(); const appStore = useAppStore();
itGet('/api/core/me/').then((data) => { itGet('/api/core/me/').then((data) => {

View File

@ -43,7 +43,15 @@ onMounted(async () => {
<div v-else> <div v-else>
<div class="circle"> <div class="circle">
<div class="flex flex-col lg:flex-row"> <div class="flex flex-col lg:flex-row">
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:py-8"> <div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:pt-4">
<router-link
to="/learningpath/versicherungsvermittlerin"
class="btn-text inline-flex items-center px-3 py-4 font-normal"
>
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
<span class="inline">zurück zum Lernpfad</span>
</router-link>
<h1 class="text-blue-dark text-7xl"> <h1 class="text-blue-dark text-7xl">
{{ circleStore.circle?.title }} {{ circleStore.circle?.title }}
</h1> </h1>

View File

@ -54,7 +54,7 @@ const userStore = useUserStore();
/> />
</div> </div>
</form> </form>
<p class="pt-8"><a href="/sso/login/">Login mit SSO</a></p>
</main> </main>
</template> </template>

View File

@ -90,3 +90,10 @@ svg {
} }
} }
@layer utilities {
.no-scroll {
height: 100vh;
overflow: hidden;
}
}

View File

@ -526,15 +526,17 @@ OAUTH = {
"client_name": env("IT_OAUTH_CLIENT_NAME", default="lernetz"), "client_name": env("IT_OAUTH_CLIENT_NAME", default="lernetz"),
"client_id": env("IT_OAUTH_CLIENT_ID", default="iterativ"), "client_id": env("IT_OAUTH_CLIENT_ID", default="iterativ"),
"client_secret": env("IT_OAUTH_CLIENT_SECRET", default=""), "client_secret": env("IT_OAUTH_CLIENT_SECRET", default=""),
"access_token_url": env("IT_OAUTH_ACCESS_TOKEN_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/token"), # "access_token_url": env("IT_OAUTH_ACCESS_TOKEN_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/token"),
"authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"), # "authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"),
"authorize_params": IT_OAUTH_AUTHORIZE_PARAMS, "authorize_params": IT_OAUTH_AUTHORIZE_PARAMS,
"access_token_params": IT_OAUTH_AUTHORIZE_PARAMS,
"api_base_url": env("IT_OAUTH_API_BASE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"), "api_base_url": env("IT_OAUTH_API_BASE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"),
"local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"), "local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"),
"server_metadata_url": env("IT_OAUTH_SERVER_METADATA_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration"),
"client_kwargs": { "client_kwargs": {
'scope': env("IT_OAUTH_SCOPE", default=''), 'scope': env("IT_OAUTH_SCOPE", default=''),
'token_endpoint_auth_method': 'client_secret_post', 'token_endpoint_auth_method': 'client_secret_post',
'token_placement': 'header', 'token_placement': 'body',
} }
} }

View File

@ -16,7 +16,7 @@ from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import ( from vbv_lernwelt.core.views import (
rate_limit_exceeded_view, rate_limit_exceeded_view,
permission_denied_view, permission_denied_view,
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, ) check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, )
from .wagtail_api import wagtail_api_router from .wagtail_api import wagtail_api_router
@ -42,6 +42,7 @@ urlpatterns = [
path('learnpath/', include("vbv_lernwelt.learnpath.urls")), path('learnpath/', include("vbv_lernwelt.learnpath.urls")),
path('api/completion/', include("vbv_lernwelt.completion.urls")), path('api/completion/', include("vbv_lernwelt.completion.urls")),
re_path(r'api/core/me/$', me_user_view, name='me_user_view'), re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
re_path(r'core/logout/$', vue_logout, name='vue_logout'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development # Static file serving when using Gunicorn + Uvicorn for local web socket development

View File

@ -3,7 +3,7 @@
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 from django.contrib.auth import authenticate, login, logout
from django.core.management import call_command from django.core.management import call_command
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
@ -69,6 +69,12 @@ def me_user_view(request):
return Response(status=403) return Response(status=403)
@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

@ -2,17 +2,6 @@ from authlib.integrations.django_client import OAuth
from django.conf import settings from django.conf import settings
# # https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients # # https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
# def fetch_token(_name, request):
# try:
# token = OAuth2Token.objects.get(
# user=request.user
# )
# return token.to_token()
# except (OAuth2Token.DoesNotExist, TypeError):
# return None
# oauth = OAuth(fetch_token=fetch_token)
oauth = OAuth() oauth = OAuth()
oauth.register( oauth.register(
name=settings.OAUTH["client_name"], name=settings.OAUTH["client_name"],
@ -20,10 +9,7 @@ oauth.register(
client_secret=settings.OAUTH["client_secret"], client_secret=settings.OAUTH["client_secret"],
request_token_url=None, request_token_url=None,
request_token_params=None, request_token_params=None,
access_token_url=settings.OAUTH["access_token_url"],
access_token_params=None,
authorize_url=settings.OAUTH["authorize_url"],
authorize_params=settings.OAUTH["authorize_params"], authorize_params=settings.OAUTH["authorize_params"],
api_base_url=settings.OAUTH["api_base_url"], client_kwargs=settings.OAUTH["client_kwargs"],
client_kwargs=settings.OAUTH["client_kwargs"] server_metadata_url=settings.OAUTH["server_metadata_url"],
) )

View File

@ -2,6 +2,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.shortcuts import redirect from django.shortcuts import redirect
from django.http import HttpResponse
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
from django.contrib.auth import login as dj_login, get_user_model from django.contrib.auth import login as dj_login, get_user_model
@ -24,7 +25,7 @@ def authorize(request):
try: try:
logger.debug(request) logger.debug(request)
token = getattr(oauth, settings.OAUTH["client_name"]).authorize_access_token(request) token = getattr(oauth, settings.OAUTH["client_name"]).authorize_access_token(request)
deocded_token = decode_jwt(token["access_token"]) deocded_token = decode_jwt(token["id_token"])
except OAuthError as e: except OAuthError as e:
logger.error(f'OAuth error: {e}') logger.error(f'OAuth error: {e}')
if not settings.DEBUG: if not settings.DEBUG:
@ -45,5 +46,5 @@ def _user_data_from_token_data(token: dict) -> dict:
"first_name": token.get("given_name", ""), "first_name": token.get("given_name", ""),
"last_name": token.get("family_name", ""), "last_name": token.get("family_name", ""),
"username": token.get("preferred_username", ""), "username": token.get("preferred_username", ""),
"email": token.get("email", ""), "email": token.get("emails", [''])[0],
} }