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

View File

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

View File

@ -1,23 +1,43 @@
<script setup lang="ts">
// inspiration https://vuejs.org/examples/#modal
import {onMounted, watch} from "vue";
import {HTMLElement} from "happy-dom";
const props = defineProps<{
show: boolean
}>()
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>
<template>
<Transition mode="in-out">
<div
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
type="button"
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
@click="$emit('closemodal')"
@click="closeModal"
>
<it-icon-close></it-icon-close>
</button>

View File

@ -14,16 +14,18 @@ export type UserState = {
loggedIn: boolean;
}
export const useUserStore = defineStore({
id: 'user',
state: () => ({
const initialUserState: UserState = {
email: '',
first_name: '',
last_name: '',
username: '',
avatar_url: '',
loggedIn: false
} as UserState),
}
export const useUserStore = defineStore({
id: 'user',
state: () => (initialUserState as UserState),
getters: {
getFullName(): string {
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() {
const appStore = useAppStore();
itGet('/api/core/me/').then((data) => {

View File

@ -43,7 +43,15 @@ onMounted(async () => {
<div v-else>
<div class="circle">
<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">
{{ circleStore.circle?.title }}
</h1>

View File

@ -54,7 +54,7 @@ const userStore = useUserStore();
/>
</div>
</form>
<p class="pt-8"><a href="/sso/login/">Login mit SSO</a></p>
</main>
</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_id": env("IT_OAUTH_CLIENT_ID", default="iterativ"),
"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"),
"authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"),
# "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_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/"),
"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": {
'scope': env("IT_OAUTH_SCOPE", default=''),
'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 (
rate_limit_exceeded_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
@ -42,6 +42,7 @@ urlpatterns = [
path('learnpath/', include("vbv_lernwelt.learnpath.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'core/logout/$', vue_logout, name='vue_logout'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development

View File

@ -3,7 +3,7 @@
import requests
import structlog
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.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
@ -69,6 +69,12 @@ def me_user_view(request):
return Response(status=403)
@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)

View File

@ -2,17 +2,6 @@ from authlib.integrations.django_client import OAuth
from django.conf import settings
# # 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.register(
name=settings.OAUTH["client_name"],
@ -20,10 +9,7 @@ oauth.register(
client_secret=settings.OAUTH["client_secret"],
request_token_url=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"],
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 django.conf import settings
from django.shortcuts import redirect
from django.http import HttpResponse
from sentry_sdk import capture_exception
from django.contrib.auth import login as dj_login, get_user_model
@ -24,7 +25,7 @@ def authorize(request):
try:
logger.debug(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:
logger.error(f'OAuth error: {e}')
if not settings.DEBUG:
@ -45,5 +46,5 @@ def _user_data_from_token_data(token: dict) -> dict:
"first_name": token.get("given_name", ""),
"last_name": token.get("family_name", ""),
"username": token.get("preferred_username", ""),
"email": token.get("email", ""),
"email": token.get("emails", [''])[0],
}