diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index cef045d867bd838acc8443c17eb695a48e90b31a..3a92a82b810622b63f36a467b876bf61ccdaa873 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -132,3 +132,7 @@ class EditTermForm(forms.ModelForm): class Meta: model = SchoolTerm fields = ["caption", "date_start", "date_end"] + + +class NextcloudServerForm(forms.Form): + url = forms.URLField(required=True, label=_("URL of your Nextcloud instance")) diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py index 65d0a584858322665bbfad34af67aebd539f6b4f..3a06bdae36ae3df8cfd19dd81ec2055796614b7b 100644 --- a/aleksis/core/menus.py +++ b/aleksis/core/menus.py @@ -87,6 +87,15 @@ MENUS = { "menu_generator.validators.is_superuser", ], }, + { + "name": _("Third-party services"), + "url": "third_party_services", + "icon": "share", + "validators": [ + "menu_generator.validators.is_authenticated", + "menu_generator.validators.is_superuser", + ], + }, { "name": _("Backend Admin"), "url": "admin:index", diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 522695ea6ce9aa1dacc6904a701cc7b8d4230370..95d9287643c6382353822ff8c4923423f750bff6 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -371,6 +371,9 @@ CONSTANCE_CONFIG = { "IMPRINT_URL": ("", _("Link to imprint"), "url_field"), "ADRESSING_NAME_FORMAT": ("german", _("Name format of adresses"), "adressing-select"), "NOTIFICATION_CHANNELS": (["email"], _("Channels to allow for notifications"), "notifications-select"), + "NEXTCLOUD_TALK_SERVER": ("", _("Nextcloud Talk server configured by connection service"), "url_field"), + "NEXTCLOUD_TALK_LOGIN_NAME": ("", _("Nextcloud Talk server configured by connection service"), str), + "NEXTCLOUD_TALK_APP_PASSWORD": ("", _("Nextcloud Talk server configured by connection service"), str), } CONSTANCE_CONFIG_FIELDSETS = { "General settings": ("SITE_TITLE",), diff --git a/aleksis/core/static/js/nextcloud_talk.js b/aleksis/core/static/js/nextcloud_talk.js new file mode 100644 index 0000000000000000000000000000000000000000..14fca65317a1b5a95026557e8c1af52aa9e19b63 --- /dev/null +++ b/aleksis/core/static/js/nextcloud_talk.js @@ -0,0 +1,29 @@ +var POLL_INTERVAL = 500; +var w = null; + +function poll() { + $.ajax({ + url: Urls.pollNextcloudTalk(), + }).done(function (data) { + if (data.done) { + console.log("Polling done"); + + // Redirect to third-party services home view + window.location.href = Urls.thirdPartyServices(); + w.close(); + } else { + window.setTimeout(poll, POLL_INTERVAL); + } + }).fail(function () { + window.setTimeout(poll, POLL_INTERVAL); + }) +} + +$(document).ready(function () { + // Open login URL + var loginData = getJSONScript("login_data"); + w = window.open(loginData.login); + + console.log("Start polling"); + poll(); +}); diff --git a/aleksis/core/templates/core/connect_nextcloud_talk.html b/aleksis/core/templates/core/connect_nextcloud_talk.html new file mode 100644 index 0000000000000000000000000000000000000000..e47ff7ae2dd4249a5a9e4c7f83f69c636f7ee438 --- /dev/null +++ b/aleksis/core/templates/core/connect_nextcloud_talk.html @@ -0,0 +1,39 @@ +{# -*- engine:django -*- #} +{% extends "core/base.html" %} +{% load i18n material_form static %} + +{% block browser_title %}{% blocktrans %}Connect to Nextcloud Talk{% endblocktrans %}{% endblock %} + +{% block page_title %}{% blocktrans %}Connect to Nextcloud Talk{% endblocktrans %}{% endblock %} + +{% block content %} + {% if step == 1 %} + <form action="" method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + + <button class="btn green waves-effect waves-light" type="submit" name="step-1"> + <i class="material-icons left">open_in_new</i> + {% trans "Open login page" %} + </button> + </form> + {% elif step == 2 %} + {{ login_data|json_script:"login_data" }} + + <p class="flow-text"> + {% blocktrans %} + Waiting for successful login ... + {% endblocktrans %} + </p> + + <p> + {% blocktrans with url=login_data.login %} + Login window has not opened? + <a href="{{ url }}" target="_blank">Try again.</a> + {% endblocktrans %} + </p> + + <script src="{% static "js/helper.js" %}"></script> + <script src="{% static "js/nextcloud_talk.js" %}"></script> + {% endif %} +{% endblock %} diff --git a/aleksis/core/templates/core/third_party_services.html b/aleksis/core/templates/core/third_party_services.html new file mode 100644 index 0000000000000000000000000000000000000000..31753139307a8455a3b48bdc36dc013143955dd8 --- /dev/null +++ b/aleksis/core/templates/core/third_party_services.html @@ -0,0 +1,46 @@ +{# -*- engine:django -*- #} +{% extends "core/base.html" %} +{% load i18n %} + +{% block browser_title %}{% blocktrans %}Third-party services{% endblocktrans %}{% endblock %} + +{% block page_title %}{% blocktrans %}Third-party services{% endblocktrans %}{% endblock %} + +{% block content %} + <div class="card"> + <div class="card-content"> + {% if nextcloud.connected %} + <span class="badge new green right">{% trans "Connected" %}</span> + {% else %} + <span class="badge new red right">{% trans "Not connected" %}</span> + {% endif %} + + <span class="card-title">{% trans "Nextcloud Talk" %}</span> + + {% if nextcloud.connected %} + <p> + {% blocktrans with login_name=nextcloud.login_name %} + Connected Nextcloud user: {{ login_name }} + {% endblocktrans %} + </p> + <br/> + <a class="btn red waves-effect waves-light" href="{% url "disconnect_nextcloud_talk" %}"> + <i class="material-icons left">leak_remove</i> + {% trans "Disconnect" %} + </a> + {% else %} + <p> + {% blocktrans %} + In order to send notifications by Nextcloud Talk you must connect a Nextcloud user which should send the + notifications to the users. + {% endblocktrans %} + </p> + <br/> + <a class="btn green waves-effect waves-light" href="{% url "connect_nextcloud_talk" %}"> + <i class="material-icons left">leak_add</i> + {% trans "Connect" %} + </a> + {% endif %} + </div> + </div> +{% endblock %} diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 6f1d4275e0fd15f2b1cd315353beb56d219342fd..054e38247f476cc8d49b481efdd70218f8c83758 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -19,6 +19,10 @@ urlpatterns = [ path("admin/", admin.site.urls), path("data_management/", views.data_management, name="data_management"), path("status/", views.system_status, name="system_status"), + path("third_party/", views.third_party_services, name="third_party_services"), + path("third_party/nextcloud_talk/", views.connect_nextcloud_talk, name="connect_nextcloud_talk"), + path("third_party/nextcloud_talk/disconnect/", views.disconnect_nextcloud_talk, name="disconnect_nextcloud_talk"), + path("third_party/nextcloud_talk/poll/", views.poll_nextcloud_talk, name="poll_nextcloud_talk"), path("school_management", views.school_management, name="school_management"), path("school/information/edit", views.edit_school, name="edit_school_information"), path("school/term/edit", views.edit_schoolterm, name="edit_school_term"), diff --git a/aleksis/core/util/nextcloud.py b/aleksis/core/util/nextcloud.py new file mode 100644 index 0000000000000000000000000000000000000000..0595dbfd45c46f73709b8412b999435e54f07ec6 --- /dev/null +++ b/aleksis/core/util/nextcloud.py @@ -0,0 +1,34 @@ +from typing import Union + +import requests +from constance import config + +HEADERS = { + "User-Agent": "AlekSIS", +} +INITIATE_LOGIN_PROCESS_URL = "index.php/login/v2" + + +def initiate_login_process(nextcloud_url: str) -> dict: + url = nextcloud_url + INITIATE_LOGIN_PROCESS_URL + + r = requests.post(url, headers=HEADERS) + + return r.json() + + +def login_process_poll(endpoint: str, token: str) -> Union[dict, bool]: + r = requests.post(endpoint, headers=HEADERS, data={"token": token}) + + if r.status_code != 200: + return False + + return r.json() + + +def is_connected() -> bool: + return ( + config.NEXTCLOUD_TALK_SERVER != "" + and config.NEXTCLOUD_TALK_LOGIN_NAME != "" + and config.NEXTCLOUD_TALK_APP_PASSWORD + ) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index c610ec8abf640246355fd04e56130e15f103f483..3957e4ed0cc5cde996e82ab5172584a62c152dff 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -1,8 +1,9 @@ from typing import Optional +from constance import config from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpRequest, HttpResponse +from django.http import Http404, HttpRequest, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import ugettext_lazy as _ @@ -15,10 +16,11 @@ from .forms import ( EditSchoolForm, EditTermForm, PersonsAccountsFormSet, -) + NextcloudServerForm) from .models import Activity, Group, Notification, Person, School, DashboardWidget from .tables import GroupsTable, PersonsTable -from .util import messages +from .util import messages, nextcloud +from .util.nextcloud import initiate_login_process, login_process_poll @person_required @@ -264,3 +266,80 @@ def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse: raise PermissionDenied(_("You are not allowed to mark notifications from other users as read!")) return redirect("index") + + +@admin_required +def third_party_services(request: HttpRequest) -> HttpResponse: + context = {} + + context["nextcloud"] = { + "connected": nextcloud.is_connected(), + "login_name": config.NEXTCLOUD_TALK_LOGIN_NAME + } + + return render(request, "core/third_party_services.html", context) + + +@admin_required +def connect_nextcloud_talk(request: HttpRequest) -> HttpResponse: + context = {} + + if request.method == "GET": + form = NextcloudServerForm() + + context["step"] = 1 + context["form"] = form + + elif request.method == "POST": + if "step-1" in request.POST: + form = NextcloudServerForm(request.POST) + + if form.is_valid(): + url = form.cleaned_data["url"] + if url[-1] != "/": + url += "/" + + r = initiate_login_process(url) + + request.session["nextcloud_poll"] = True + request.session["nextcloud_login_data"] = r + + context["login_data"] = r + context["step"] = 2 + + return render(request, "core/connect_nextcloud_talk.html", context) + + +@admin_required +def disconnect_nextcloud_talk(request: HttpRequest) -> HttpResponse: + config.NEXTCLOUD_TALK_SERVER = "" + config.NEXTCLOUD_TALK_LOGIN_NAME = "" + config.NEXTCLOUD_TALK_APP_PASSWORD = "" + + messages.success(request, _("The Nextcloud user was successfully disconnected .")) + + return redirect("third_party_services") + + +@admin_required +def poll_nextcloud_talk(request: HttpRequest) -> HttpResponse: + done = False + + if request.session.get("nextcloud_poll", False): + login_data = request.session["nextcloud_login_data"] + + r = login_process_poll(login_data["poll"]["endpoint"], login_data["poll"]["token"]) + + done = r != False + + if done: + config.NEXTCLOUD_TALK_SERVER = r["server"] + config.NEXTCLOUD_TALK_LOGIN_NAME = r["loginName"] + config.NEXTCLOUD_TALK_APP_PASSWORD = r["appPassword"] + + messages.success(request, _("The Nextcloud user was successfully connected.")) + + del request.session["nextcloud_login_data"] + del request.session["nextcloud_poll"] + + return JsonResponse({"done": done})