diff --git a/client/src/router/guards.ts b/client/src/router/guards.ts index b96e1849..80596c46 100644 --- a/client/src/router/guards.ts +++ b/client/src/router/guards.ts @@ -1,24 +1,21 @@ import type {NavigationGuardWithThis, RouteLocationNormalized} from 'vue-router'; -import type {UserState} from '@/stores/user' -import {useUserStore} from '@/stores/user' -import type {Store} from 'pinia'; - -const cookieName = 'loginStatus' -let userStore: Store | null = null - +import {useUserStore} from '@/stores/user'; export const updateLoggedIn: NavigationGuardWithThis = (_to) => { - const loggedIn = getCookieValue(cookieName) === 'true' - const store = getUserStore() + const loggedIn = getCookieValue('loginStatus') === 'true' + const userStore = useUserStore() - store.$patch({ loggedIn }) + userStore.$patch({loggedIn}); + if (loggedIn && !userStore.email) { + userStore.fetchUser(); + } } export const redirectToLoginIfRequired: NavigationGuardWithThis = (to, _from) => { - const store = getUserStore() - if(loginRequired(to) && !store.loggedIn) { - return { name: 'home' } + const userStore = useUserStore() + if(loginRequired(to) && !userStore.loggedIn) { + return `/login?next=${to.fullPath}` } } @@ -31,20 +28,6 @@ export const getCookieValue = (cookieName: string): string => { return cookieValue.pop() || ''; } -// Pina is not ready when router is called the first time by app.use(), so we need to load it here -const getUserStore = (): UserState & Store => { - if (!userStore) { - userStore = useUserStore() - } - return userStore as unknown as UserState & Store -} - const loginRequired = (to: RouteLocationNormalized) => { return !to.meta?.public } - - - -// export const unauthorizedAccess: NavigationGuardWithThis = (to) => { -// r loginRequired(to) && getCookieValue('loginStatus') !== 'true'; -// } diff --git a/client/src/router/index.ts b/client/src/router/index.ts index ff276668..08fb9fcb 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -1,9 +1,8 @@ import {createRouter, createWebHistory} from 'vue-router' import HomeView from '../views/HomeView.vue'; +import LoginView from '../views/LoginView.vue'; import {redirectToLoginIfRequired, updateLoggedIn} from '@/router/guards'; -const loginUrl = '/sso/login/' - const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ @@ -18,15 +17,12 @@ const router = createRouter({ }, { path: '/login', - component: HomeView, - beforeEnter(_to, _from) { - window.location.href = loginUrl - }, + component: LoginView, meta: { public: true } }, - { + { path: '/learningpath/:learningPathSlug', component: () => import('../views/LearningPathView.vue'), props: true diff --git a/client/src/services/circle.ts b/client/src/services/circle.ts index 9c0178ce..3cb8c462 100644 --- a/client/src/services/circle.ts +++ b/client/src/services/circle.ts @@ -1,7 +1,7 @@ import type {CircleChild, LearningContent, LearningSequence, LearningUnit} from '@/types'; -function createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit { +function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit { return { id: 0, title: '', @@ -35,7 +35,7 @@ export function parseLearningSequences (children: CircleChild[]): LearningSequen learningSequence = Object.assign(child, {learningUnits: []}); // initialize empty learning unit if there will not come a learning unit next - learningUnit = createEmptyLearningUnit(learningSequence); + learningUnit = _createEmptyLearningUnit(learningSequence); } else if (child.type === 'learnpath.LearningUnit') { if (!learningSequence) { throw new Error('LearningUnit found before LearningSequence'); diff --git a/client/src/services/http.ts b/client/src/services/http.ts deleted file mode 100644 index ba95cecc..00000000 --- a/client/src/services/http.ts +++ /dev/null @@ -1,8 +0,0 @@ -import axios from 'axios'; - -export function getUserData () { - return axios({ - method: 'get', - url: 'http://localhost:3000/api/me', - }) -} diff --git a/client/src/stores/user.ts b/client/src/stores/user.ts index 15a52714..32d9aaf3 100644 --- a/client/src/stores/user.ts +++ b/client/src/stores/user.ts @@ -1,8 +1,15 @@ +import * as log from 'loglevel'; + import {defineStore} from 'pinia' +import {itGet, itPost} from '@/fetchHelpers'; // typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state export type UserState = { + first_name: string, + last_name: string, email: string; + username: string, + avatar_url: string, loggedIn: boolean; } @@ -10,16 +17,38 @@ export const useUserStore = defineStore({ id: 'user', state: () => ({ email: '', + first_name: '', + last_name: '', + username: '', + avatar_url: '', loggedIn: false } as UserState), getters: { }, actions: { - setEmail (email: string) { - this.email = email + handleLogin(username: string, password: string, next='/') { + if (username && password) { + itPost('/core/login/', { + username, + password, + }).then((data) => { + this.$state = data; + this.loggedIn = true; + log.debug(`redirect to ${next}`); + window.location.href = next; + }).catch(() => { + this.loggedIn = false; + alert('Login failed'); + }); + } }, - setLoggedIn (isLoggedIn: boolean) { - this.loggedIn = isLoggedIn + fetchUser() { + itGet('/api/core/me/').then((data) => { + this.$state = data; + this.loggedIn = true; + }).catch(() => { + this.loggedIn = false; + }) } } }) diff --git a/client/src/views/CircleView.vue b/client/src/views/CircleView.vue index 1b2ff255..318e4da9 100644 --- a/client/src/views/CircleView.vue +++ b/client/src/views/CircleView.vue @@ -11,6 +11,8 @@ import {useCircleStore} from '@/stores/circle'; import SelfEvaluation from '@/components/circle/SelfEvaluation.vue'; import CircleDiagram from '@/components/circle/CircleDiagram.vue'; +log.debug('CircleView.vue created'); + const props = defineProps<{ circleSlug: string }>() diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue index 35d4e0b9..7b72def6 100644 --- a/client/src/views/HomeView.vue +++ b/client/src/views/HomeView.vue @@ -9,8 +9,9 @@ import MainNavigationBar from '@/components/MainNavigationBar.vue';
Styelguide - Login - Lernpfad "Versicherungsvermittlerin" (Login benötigt) + Login + + Lernpfad "Versicherungsvermittlerin" (Login benötigt) Circle "Analyse" (Login benötigt)
diff --git a/client/src/views/LoginView.vue b/client/src/views/LoginView.vue new file mode 100644 index 00000000..6d6e78f0 --- /dev/null +++ b/client/src/views/LoginView.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 1f774569..43ce67e7 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -129,7 +129,7 @@ AUTH_USER_MODEL = "core.User" # FIXME make configurable!? # LOGIN_URL = "/sso/login/" -LOGIN_URL = "/login/" +LOGIN_URL = "/login" LOGIN_REDIRECT_URL = "/" ALLOW_LOCAL_LOGIN = env.bool("IT_ALLOW_LOCAL_LOGIN", default=False) diff --git a/server/config/urls.py b/server/config/urls.py index 008d58a3..f99b3bdb 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -1,7 +1,6 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.contrib.auth import views as auth_views from django.contrib.auth.decorators import user_passes_test from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path, re_path @@ -17,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, ) + check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, ) from .wagtail_api import wagtail_api_router @@ -42,14 +41,16 @@ urlpatterns = [ path('pages/', include(wagtail_urls)), 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'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development urlpatterns += staticfiles_urlpatterns() if settings.ALLOW_LOCAL_LOGIN: - urlpatterns += [path("login/", django_view_authentication_exempt( - auth_views.LoginView.as_view(template_name="core/login.html"))),] + urlpatterns += [ + re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'), + ] # API URLS diff --git a/server/vbv_lernwelt/completion/tests/test_api.py b/server/vbv_lernwelt/completion/tests/test_api.py index 6d21296e..22bcca73 100644 --- a/server/vbv_lernwelt/completion/tests/test_api.py +++ b/server/vbv_lernwelt/completion/tests/test_api.py @@ -19,7 +19,7 @@ class CompletionApiTestCase(APITestCase): def setUp(self) -> None: self.user = User.objects.get(username='student') - self.client.login(username='student', password='student') + self.client.login(username='student', password='test') def test_completeLearningContent_works(self): learning_content = LearningContent.objects.get(title='Einleitung Circle "Anlayse"') diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 886e736f..41f8be4f 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -5,25 +5,75 @@ from vbv_lernwelt.core.models import User def create_default_users(user_model=User, group_model=Group, default_password=None): + if default_password is None: + default_password = 'test' + admin_group, created = group_model.objects.get_or_create(name='admin_group') content_creator_grop, created = group_model.objects.get_or_create(name='content_creator_grop') student_group, created = group_model.objects.get_or_create(name='student_group') - admin_password = default_password - if not admin_password: - admin_password = 'admin' - admin_user, created = _get_or_create_user(user_model=user_model, username='admin', password=admin_password) - admin_user.is_superuser = True - admin_user.is_staff = True - admin_user.groups.add(admin_group) - admin_user.save() + def _create_student_user(email, first_name, last_name, avatar_url=''): + student_user, created = _get_or_create_user( + user_model=user_model, username=email, password=default_password, + ) + student_user.first_name = first_name + student_user.last_name = last_name + student_user.avatar_url = avatar_url + student_user.groups.add(student_group) + student_user.save() - student_user_password = default_password - if not student_user_password: - student_user_password = 'student' - student_user, created = _get_or_create_user(user_model=user_model, username='student', password=student_user_password) - student_user.groups.add(student_group) - student_user.save() + def _create_admin_user(email, first_name, last_name, avatar_url=''): + admin_user, created = _get_or_create_user( + user_model=user_model, username=email, password=default_password + ) + admin_user.is_superuser = True + admin_user.is_staff = True + admin_user.first_name = first_name + admin_user.last_name = last_name + admin_user.groups.add(admin_group) + admin_user.save() + + _create_admin_user( + email='info@iterativ.ch', + first_name='Info', + last_name='Iterativ', + avatar_url='/static/avatars/avatar_iterativ.png' + ) + + _create_admin_user( + email='admin', + first_name='Peter', + last_name='Adminson', + avatar_url='/static/avatars/avatar_iterativ.png' + ) + + _create_student_user( + email='student', + first_name='Student', + last_name='Meier', + avatar_url='/static/avatars/avatar_iterativ.png' + ) + + _create_student_user( + email='daniel.egger@iterativ.ch', + first_name='Daniel', + last_name='Egger', + avatar_url='/static/avatars/avatar_iterativ.png' + ) + + _create_student_user( + email='axel.manderbach@lernetz.ch', + first_name='Axel', + last_name='Manderbach', + avatar_url='/static/avatars/avatar_axel.png' + ) + + _create_student_user( + email='christoph.bosshard@vbv-afa.ch', + first_name='Christoph', + last_name='Bosshard', + avatar_url='/static/avatars/avatar_christoph.png' + ) def _get_or_create_user(user_model, *args, **kwargs): @@ -34,6 +84,10 @@ def _get_or_create_user(user_model, *args, **kwargs): user = user_model.objects.filter(username=username).first() if not user: - user = user_model.objects.create(username=username, password=make_password(password)) + user = user_model.objects.create( + username=username, + password=make_password(password), + email=username, + ) created = True return user, created diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py index 934dc1f2..757ef365 100644 --- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py +++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py @@ -10,4 +10,4 @@ def command(customer_language): print("cypress reset data") delete_default_learning_path() - create_default_learning_path() + create_default_learning_path(skip_locales=False) diff --git a/server/vbv_lernwelt/core/migrations/0002_user_model.py b/server/vbv_lernwelt/core/migrations/0002_user_model.py new file mode 100644 index 00000000..2733511e --- /dev/null +++ b/server/vbv_lernwelt/core/migrations/0002_user_model.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-06-28 12:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='avatar_url', + field=models.CharField(blank=True, default='', max_length=254), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='email address'), + ), + ] diff --git a/server/vbv_lernwelt/core/migrations/0002_create_users.py b/server/vbv_lernwelt/core/migrations/0003_create_users.py similarity index 94% rename from server/vbv_lernwelt/core/migrations/0002_create_users.py rename to server/vbv_lernwelt/core/migrations/0003_create_users.py index e5d92cfa..faf79e86 100644 --- a/server/vbv_lernwelt/core/migrations/0002_create_users.py +++ b/server/vbv_lernwelt/core/migrations/0003_create_users.py @@ -15,7 +15,7 @@ def create_users(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("core", "0001_initial"), + ("core", "0002_user_model"), ] operations = [ diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index b679d72d..55054f86 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -10,6 +10,8 @@ class User(AbstractUser): """ # FIXME: look into it... # objects = UserManager() + avatar_url = models.CharField(max_length=254, blank=True, default='') + email = models.EmailField('email address', unique=True) class SecurityRequestResponseLog(models.Model): diff --git a/server/vbv_lernwelt/core/serializers.py b/server/vbv_lernwelt/core/serializers.py new file mode 100644 index 00000000..ddc5ef24 --- /dev/null +++ b/server/vbv_lernwelt/core/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from vbv_lernwelt.core.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'id', 'first_name', 'last_name', 'email', 'username', 'avatar_url', + ] diff --git a/server/vbv_lernwelt/core/templates/core/login.html b/server/vbv_lernwelt/core/templates/core/login.html deleted file mode 100644 index 4a8794dd..00000000 --- a/server/vbv_lernwelt/core/templates/core/login.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- -
-
-

Login

-
- {% csrf_token %} -
- - -
-
- - -
- -
- -
-
-
-
- -
- -{% endblock %} diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py index d48f902c..8ad3171f 100644 --- a/server/vbv_lernwelt/core/utils.py +++ b/server/vbv_lernwelt/core/utils.py @@ -6,8 +6,6 @@ from rest_framework.throttling import UserRateThrottle from structlog.types import EventDict -#from .models import User - def structlog_add_app_info( logger: logging.Logger, method_name: str, event_dict: EventDict ) -> EventDict: diff --git a/server/vbv_lernwelt/core/views.py b/server/vbv_lernwelt/core/views.py index 29c9daa0..36c3a3b9 100644 --- a/server/vbv_lernwelt/core/views.py +++ b/server/vbv_lernwelt/core/views.py @@ -1,7 +1,9 @@ # Create your views here. import requests +import structlog from django.conf import settings +from django.contrib.auth import authenticate, login from django.core.management import call_command from django.http import JsonResponse, HttpResponse, HttpResponseRedirect from django.shortcuts import render @@ -11,8 +13,12 @@ from ratelimit.decorators import ratelimit from rest_framework import authentication from rest_framework.decorators import api_view, authentication_classes, permission_classes from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt +from vbv_lernwelt.core.serializers import UserSerializer + +logger = structlog.get_logger(__name__) @django_view_authentication_exempt @@ -37,6 +43,32 @@ def vue_home(request): return HttpResponse(content) +@api_view(['POST']) +@ensure_csrf_cookie +def vue_login(request): + try: + username = request.data.get('username') + password = request.data.get('password') + if username and password: + user = authenticate(request, username=username, password=password) + if user: + login(request, user) + logger.debug('login successful', username=username, email=user.email, label='login') + return Response(UserSerializer(user).data) + except Exception as e: + logger.exception(e) + + logger.debug('login failed', username=username, label='login') + return Response({'success': False}, status=401) + + +@api_view(['GET']) +def me_user_view(request): + if request.user.is_authenticated: + return Response(UserSerializer(request.user).data) + return Response(status=403) + + def permission_denied_view(request, exception): return render(request, "403.html", status=403) diff --git a/server/vbv_lernwelt/learnpath/management/commands/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/management/commands/create_default_learning_path.py index 7df9caaf..2218afd3 100644 --- a/server/vbv_lernwelt/learnpath/management/commands/create_default_learning_path.py +++ b/server/vbv_lernwelt/learnpath/management/commands/create_default_learning_path.py @@ -5,4 +5,4 @@ from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_def @click.command() def command(): - create_default_learning_path() + create_default_learning_path(skip_locales=False) diff --git a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py index e6a6f45e..560b7434 100644 --- a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py +++ b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py @@ -12,9 +12,9 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFac ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory -def create_default_learning_path(user=None): +def create_default_learning_path(user=None, skip_locales=True): if user is None: - user = User.objects.get(username='admin') + user = User.objects.get(username='info@iterativ.ch') site = Site.objects.filter(is_default_site=True).first() @@ -409,18 +409,19 @@ Fachspezialisten bei. # circle_7 = CircleFactory.create(title="Prüfungsvorbereitung", parent=lp, topic=tp) # locales - locale_de = Locale.objects.get(language_code='de-CH') - locale_fr, _ = Locale.objects.get_or_create(language_code='fr-CH') - LocaleSynchronization.objects.get_or_create( - locale_id=locale_fr.id, - sync_from_id=locale_de.id - ) - locale_it, _ = Locale.objects.get_or_create(language_code='it-CH') - LocaleSynchronization.objects.get_or_create( - locale_id=locale_it.id, - sync_from_id=locale_de.id - ) - call_command('sync_locale_trees') + if not skip_locales: + locale_de = Locale.objects.get(language_code='de-CH') + locale_fr, _ = Locale.objects.get_or_create(language_code='fr-CH') + LocaleSynchronization.objects.get_or_create( + locale_id=locale_fr.id, + sync_from_id=locale_de.id + ) + locale_it, _ = Locale.objects.get_or_create(language_code='it-CH') + LocaleSynchronization.objects.get_or_create( + locale_id=locale_it.id, + sync_from_id=locale_de.id + ) + call_command('sync_locale_trees') # all pages belong to 'admin' by default Page.objects.update(owner=user) diff --git a/server/vbv_lernwelt/static/avatars/avatar_axel.jpg b/server/vbv_lernwelt/static/avatars/avatar_axel.jpg new file mode 100644 index 00000000..d8884568 Binary files /dev/null and b/server/vbv_lernwelt/static/avatars/avatar_axel.jpg differ diff --git a/server/vbv_lernwelt/static/avatars/avatar_christoph.png b/server/vbv_lernwelt/static/avatars/avatar_christoph.png new file mode 100644 index 00000000..5e0bc0b7 Binary files /dev/null and b/server/vbv_lernwelt/static/avatars/avatar_christoph.png differ diff --git a/server/vbv_lernwelt/static/avatars/avatar_iterativ.png b/server/vbv_lernwelt/static/avatars/avatar_iterativ.png new file mode 100644 index 00000000..9167b36e Binary files /dev/null and b/server/vbv_lernwelt/static/avatars/avatar_iterativ.png differ