diff --git a/admin/src/admin/lib/legal.py b/admin/src/admin/lib/legal.py
index 65caab3..ff02087 100644
--- a/admin/src/admin/lib/legal.py
+++ b/admin/src/admin/lib/legal.py
@@ -1,82 +1,21 @@
import logging as log
import os
-from pprint import pprint
-
-from minio import Minio
-from minio.commonconfig import REPLACE, CopySource
-from minio.deleteobjects import DeleteObject
-from requests import get, post
+import traceback
from admin import app
+legal_path= os.path.join(app.root_path, "static/templates/pages/legal/")
-class Avatars:
- def __init__(self):
- self.mclient = Minio(
- "isard-sso-avatars:9000",
- access_key="AKIAIOSFODNN7EXAMPLE",
- secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
- secure=False,
- )
- self.bucket = "master-avatars"
- self._minio_set_realm()
- # self.update_missing_avatars()
+def get_legal(lang):
+ with open(legal_path+lang, "r") as languagefile:
+ return languagefile.read()
- def add_user_default_avatar(self, userid, role="unknown"):
- self.mclient.fput_object(
- self.bucket,
- userid,
- os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
- content_type="image/jpeg ",
- )
- log.warning(
- " AVATARS: Updated avatar for user " + userid + " with role " + role
- )
+def gen_legal_if_not_exists(lang):
+ if not os.path.isfile(legal_path+lang):
+ log.debug("Creating new language file")
+ with open(legal_path+lang, "w") as languagefile:
+ languagefile.write("Legal
This is the default legal page for language " + lang)
- def delete_user_avatar(self, userid):
- self.minio_delete_object(userid)
-
- def update_missing_avatars(self, users):
- sys_roles = ["admin", "manager", "teacher", "student"]
- for u in self.get_users_without_image(users):
- try:
- img = [r + ".jpg" for r in sys_roles if r in u["roles"]][0]
- except:
- img = "unknown.jpg"
-
- self.mclient.fput_object(
- self.bucket,
- u["id"],
- os.path.join(app.root_path, "../custom/avatars/" + img),
- content_type="image/jpeg ",
- )
- log.warning(
- " AVATARS: Updated avatar for user "
- + u["username"]
- + " with role "
- + img.split(".")[0]
- )
-
- def _minio_set_realm(self):
- if not self.mclient.bucket_exists(self.bucket):
- self.mclient.make_bucket(self.bucket)
-
- def minio_get_objects(self):
- return [o.object_name for o in self.mclient.list_objects(self.bucket)]
-
- def minio_delete_all_objects(self):
- delete_object_list = map(
- lambda x: DeleteObject(x.object_name),
- self.mclient.list_objects(self.bucket),
- )
- errors = self.mclient.remove_objects(self.bucket, delete_object_list)
- for error in errors:
- log.error(" AVATARS: Error occured when deleting avatar object: " + error)
-
- def minio_delete_object(self, oid):
- errors = self.mclient.remove_objects(self.bucket, [DeleteObject(oid)])
- for error in errors:
- log.error(" AVATARS: Error occured when deleting avatar object: " + error)
-
- def get_users_without_image(self, users):
- return [u for u in users if u["id"] and u["id"] not in self.minio_get_objects()]
+def new_legal(lang,html):
+ with open(legal_path+lang, "w") as languagefile:
+ languagefile.write(html)
\ No newline at end of file
diff --git a/admin/src/admin/static/js/legal.js b/admin/src/admin/static/js/legal.js
index f33c44b..821a50c 100644
--- a/admin/src/admin/static/js/legal.js
+++ b/admin/src/admin/static/js/legal.js
@@ -18,14 +18,25 @@ $(document).ready(function () {
// })
$("#save-legal").click(function () {
+ console.log($('#editor-legal').cleanHtml())
+ console.log($('#legal-lang').val())
$.ajax({
type: "POST",
url: "/api/legal/legal",
- data: {
+ data: JSON.stringify({
'html': $('#editor-legal').cleanHtml(),
'lang': $('#legal-lang').val()
- },
+ }),
success: function () {
+ new PNotify({
+ title: "Legal text",
+ text: "Updated for "+$('#legal-lang').val()+" language",
+ hide: true,
+ delay: 3000,
+ icon: 'fa fa-alert-sign',
+ opacity: 1,
+ type: 'info'
+ });
},
});
});
diff --git a/admin/src/admin/views/AppViews.py b/admin/src/admin/views/AppViews.py
new file mode 100644
index 0000000..3c1feeb
--- /dev/null
+++ b/admin/src/admin/views/AppViews.py
@@ -0,0 +1,563 @@
+#!flask/bin/python
+# coding=utf-8
+import concurrent.futures
+import json
+import logging as log
+import os
+import re
+import sys
+
+# import Queue
+import threading
+import time
+import traceback
+from uuid import uuid4
+
+from flask import Response, jsonify, redirect, render_template, request, url_for
+from flask_login import current_user, login_required
+
+from admin import app
+
+from ..lib.helpers import system_group
+from .decorators import login_or_token
+
+threads = {"external": None}
+# q = Queue.Queue()
+
+from keycloak.exceptions import KeycloakGetError
+
+from ..lib.dashboard import Dashboard
+from ..lib.exceptions import UserExists, UserNotFound
+
+dashboard = Dashboard()
+
+from ..lib.legal import get_legal, gen_legal_if_not_exists, new_legal
+
+@app.route("/sysadmin/api/resync")
+@app.route("/api/resync")
+@login_required
+def resync():
+ return (
+ json.dumps(app.admin.resync_data()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+
+
+@app.route("/api/users", methods=["GET", "PUT"])
+@app.route("/api/users/", methods=["POST", "PUT", "GET", "DELETE"])
+@login_or_token
+def users(provider=False):
+ if request.method == "DELETE":
+ if current_user.role != "admin":
+ return json.dumps({}), 301, {"Content-Type": "application/json"}
+ if provider == "keycloak":
+ return (
+ json.dumps(app.admin.delete_keycloak_users()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if provider == "nextcloud":
+ return (
+ json.dumps(app.admin.delete_nextcloud_users()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if provider == "moodle":
+ return (
+ json.dumps(app.admin.delete_moodle_users()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if request.method == "POST":
+ if current_user.role != "admin":
+ return json.dumps({}), 301, {"Content-Type": "application/json"}
+ if provider == "moodle":
+ return (
+ json.dumps(app.admin.sync_to_moodle()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if provider == "nextcloud":
+ return (
+ json.dumps(app.admin.sync_to_nextcloud()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if request.method == "PUT" and not provider:
+ if current_user.role != "admin":
+ return json.dumps({}), 301, {"Content-Type": "application/json"}
+
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return (
+ json.dumps(
+ {"msg": "Precondition failed: already working with users"}
+ ),
+ 412,
+ {"Content-Type": "application/json"},
+ )
+ else:
+ threads["external"] = None
+ try:
+ threads["external"] = threading.Thread(
+ target=app.admin.update_users_from_keycloak, args=()
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except:
+ log.error(traceback.format_exc())
+ return (
+ json.dumps({"msg": "Add user error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+
+ # return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'}
+
+ users = app.admin.get_mix_users()
+ if current_user.role != "admin":
+ for user in users:
+ user["keycloak_groups"] = [
+ g for g in user["keycloak_groups"] if not system_group(g)
+ ]
+ return json.dumps(users), 200, {"Content-Type": "application/json"}
+
+
+@app.route("/api/users_bulk/", methods=["PUT"])
+@login_required
+def users_bulk(action):
+ data = request.get_json(force=True)
+ if request.method == "PUT":
+ if action == "enable":
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return (
+ json.dumps(
+ {"msg": "Precondition failed: already operating users"}
+ ),
+ 412,
+ {"Content-Type": "application/json"},
+ )
+ else:
+ threads["external"] = None
+ try:
+ threads["external"] = threading.Thread(
+ target=app.admin.enable_users, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except:
+ log.error(traceback.format_exc())
+ return (
+ json.dumps({"msg": "Enable users error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+ if action == "disable":
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return (
+ json.dumps(
+ {"msg": "Precondition failed: already operating users"}
+ ),
+ 412,
+ {"Content-Type": "application/json"},
+ )
+ else:
+ threads["external"] = None
+ try:
+ threads["external"] = threading.Thread(
+ target=app.admin.disable_users, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except:
+ log.error(traceback.format_exc())
+ return (
+ json.dumps({"msg": "Disabling users error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+ if action == "delete":
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return (
+ json.dumps(
+ {"msg": "Precondition failed: already operating users"}
+ ),
+ 412,
+ {"Content-Type": "application/json"},
+ )
+ else:
+ threads["external"] = None
+ try:
+ threads["external"] = threading.Thread(
+ target=app.admin.delete_users, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except:
+ log.error(traceback.format_exc())
+ return (
+ json.dumps({"msg": "Deleting users error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+ return json.dumps({}), 405, {"Content-Type": "application/json"}
+
+
+# Update pwd
+@app.route("/api/user_password", methods=["GET"])
+@app.route("/api/user_password/", methods=["PUT"])
+@login_required
+def user_password(userid=False):
+ if request.method == "GET":
+ return (
+ json.dumps(app.admin.get_dice_pwd()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if request.method == "PUT":
+ data = request.get_json(force=True)
+ password = data["password"]
+ temporary = data.get("temporary", True)
+ try:
+ res = app.admin.user_update_password(userid, password, temporary)
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except KeycloakGetError as e:
+ log.error(e.error_message.decode("utf-8"))
+ return (
+ json.dumps({"msg": "Update password error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+
+ return json.dumps({}), 405, {"Content-Type": "application/json"}
+
+
+# User
+@app.route("/api/user", methods=["POST"])
+@app.route("/api/user/", methods=["PUT", "GET", "DELETE"])
+@login_required
+def user(userid=None):
+ if request.method == "DELETE":
+ app.admin.delete_user(userid)
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ if request.method == "POST":
+ data = request.get_json(force=True)
+ if app.admin.get_user_username(data["username"]):
+ return (
+ json.dumps({"msg": "Add user error: already exists."}),
+ 409,
+ {"Content-Type": "application/json"},
+ )
+ data["enabled"] = not data.get("enabled", False)
+ data["quota"] = data["quota"] if data["quota"] != "false" else False
+ data["groups"] = data["groups"] if data.get("groups", False) else []
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return (
+ json.dumps({"msg": "Precondition failed: already adding users"}),
+ 412,
+ {"Content-Type": "application/json"},
+ )
+ else:
+ threads["external"] = None
+ try:
+ threads["external"] = threading.Thread(
+ target=app.admin.add_user, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except:
+ log.error(traceback.format_exc())
+ return (
+ json.dumps({"msg": "Add user error."}),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+
+ if request.method == "PUT":
+ data = request.get_json(force=True)
+ data["enabled"] = True if data.get("enabled", False) else False
+ data["groups"] = data["groups"] if data.get("groups", False) else []
+ data["roles"] = [data.pop("role-keycloak")]
+ try:
+ app.admin.user_update(data)
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ except UserNotFound:
+ return (
+ json.dumps({"msg": "User not found."}),
+ 404,
+ {"Content-Type": "application/json"},
+ )
+ if request.method == "DELETE":
+ pass
+ if request.method == "GET":
+ user = app.admin.get_user(userid)
+ if not user:
+ return (
+ json.dumps({"msg": "User not found."}),
+ 404,
+ {"Content-Type": "application/json"},
+ )
+ return json.dumps(user), 200, {"Content-Type": "application/json"}
+
+
+@app.route("/api/roles")
+@login_required
+def roles():
+ sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k["name"])
+ if current_user.role != "admin":
+ sorted_roles = [sr for sr in sorted_roles if sr["name"] != "admin"]
+ return json.dumps(sorted_roles), 200, {"Content-Type": "application/json"}
+
+
+@app.route("/api/group", methods=["POST", "DELETE"])
+@app.route("/api/group/", methods=["PUT", "GET", "DELETE"])
+@login_required
+def group(group_id=False):
+ if request.method == "POST":
+ data = request.get_json(force=True)
+ log.error(data)
+ data["parent"] = data["parent"] if data["parent"] != "" else None
+ return (
+ json.dumps(app.admin.add_group(data)),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ if request.method == "DELETE":
+ try:
+ data = request.get_json(force=True)
+ except:
+ data = False
+
+ if data:
+ res = app.admin.delete_group_by_path(data["path"])
+ else:
+ if not group_id:
+ return (
+ json.dumps({"error": "bad_request","msg":"Bad request"}),
+ 400,
+ {"Content-Type": "application/json"},
+ )
+ res = app.admin.delete_group_by_id(group_id)
+ return json.dumps(res), 200, {"Content-Type": "application/json"}
+
+
+@app.route("/api/groups")
+@app.route("/api/groups/", methods=["POST", "PUT", "GET", "DELETE"])
+@login_required
+def groups(provider=False):
+ if request.method == "GET":
+ sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"]))
+ if current_user.role != "admin":
+ ## internal groups should be avoided as are assigned with the role
+ sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])]
+ else:
+ sorted_groups = [sg for sg in sorted_groups]
+ return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"}
+ if request.method == "DELETE":
+ if provider == "keycloak":
+ return (
+ json.dumps(app.admin.delete_keycloak_groups()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+
+
+### SYSADM USERS ONLY
+
+
+@app.route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
+@login_required
+def external():
+ if "external" in threads.keys():
+ if threads["external"] is not None and threads["external"].is_alive():
+ return json.dumps({}), 301, {"Content-Type": "application/json"}
+ else:
+ threads["external"] = None
+
+ if request.method == "POST":
+ data = request.get_json(force=True)
+ if data["format"] == "json-ga":
+ threads["external"] = threading.Thread(
+ target=app.admin.upload_json_ga, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ if data["format"] == "csv-ug":
+ valid = check_upload_errors(data)
+ if valid["pass"]:
+ threads["external"] = threading.Thread(
+ target=app.admin.upload_csv_ug, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ else:
+ return json.dumps(valid), 422, {"Content-Type": "application/json"}
+ if request.method == "PUT":
+ data = request.get_json(force=True)
+ threads["external"] = threading.Thread(
+ target=app.admin.sync_external, args=(data,)
+ )
+ threads["external"].start()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ if request.method == "DELETE":
+ print("RESET")
+ app.admin.reset_external()
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ return json.dumps({}), 500, {"Content-Type": "application/json"}
+
+
+@app.route("/api/external/users")
+@login_required
+def external_users_list():
+ while threads["external"] is not None and threads["external"].is_alive():
+ time.sleep(0.5)
+ return (
+ json.dumps(app.admin.get_external_users()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+
+
+@app.route("/api/external/groups")
+@login_required
+def external_groups_list():
+ while threads["external"] is not None and threads["external"].is_alive():
+ time.sleep(0.5)
+ return (
+ json.dumps(app.admin.get_external_groups()),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+
+
+@app.route("/api/external/roles", methods=["PUT"])
+@login_required
+def external_roles():
+ if request.method == "PUT":
+ return (
+ json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+
+
+def check_upload_errors(data):
+ email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
+ for u in data["data"]:
+ try:
+ user_groups = [g.strip() for g in u["groups"].split(",")]
+ except:
+ resp = {
+ "pass": False,
+ "msg": "User " + u["username"] + " has invalid groups: " + u["groups"],
+ }
+ log.error(resp)
+ return resp
+
+ if not re.fullmatch(email_regex, u["email"]):
+ resp = {
+ "pass": False,
+ "msg": "User " + u["username"] + " has invalid email: " + u["email"],
+ }
+ log.error(resp)
+ return resp
+
+ if u["role"] not in ["admin", "manager", "teacher", "student"]:
+ if u["role"] == "":
+ resp = {
+ "pass": False,
+ "msg": "User " + u["username"] + " has no role assigned!",
+ }
+ log.error(resp)
+ return resp
+ resp = {
+ "pass": False,
+ "msg": "User " + u["username"] + " has invalid role: " + u["role"],
+ }
+ log.error(resp)
+ return resp
+ return {"pass": True, "msg": ""}
+
+
+@app.route("/api/dashboard/- ", methods=["PUT"])
+@login_required
+def dashboard_put(item):
+ if item == "colours":
+ try:
+ data = request.get_json(force=True)
+ dashboard.update_colours(data)
+ except:
+ log.error(traceback.format_exc())
+ return json.dumps({"colours": data}), 200, {"Content-Type": "application/json"}
+ if item == "menu":
+ try:
+ data = request.get_json(force=True)
+ dashboard.update_menu(data)
+ except:
+ log.error(traceback.format_exc())
+ return json.dumps(data), 200, {"Content-Type": "application/json"}
+ if item == "logo":
+ dashboard.update_logo(request.files["croppedImage"])
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ if item == "background":
+ dashboard.update_background(request.files["croppedImage"])
+ return json.dumps({}), 200, {"Content-Type": "application/json"}
+ return (
+ json.dumps(
+ {
+ "error": "update_error",
+ "msg": "Error updating item " + item + "\n" + traceback.format_exc(),
+ }
+ ),
+ 500,
+ {"Content-Type": "application/json"},
+ )
+
+@app.route("/api/legal/
- ", methods=["GET"])
+# @login_required
+def legal_get(item):
+ if request.method == "GET":
+ if item == "legal":
+ lang = request.args.get("lang")
+ if not lang or lang not in ["ca","es","en","fr"]:
+ lang="ca"
+ gen_legal_if_not_exists(lang)
+ return (
+ json.dumps({"html": get_legal(lang)}),
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ # if item == "privacy":
+ # return json.dumps({ "html": "Privacy policy
This works!"}), 200, {'Content-Type': 'application/json'}
+
+
+@app.route("/api/legal/- ", methods=["POST"])
+@login_required
+def legal_put(item):
+ if request.method == "POST":
+ if item == "legal":
+ data = None
+ try:
+ data = data = request.get_json(force=True)
+ html = data["html"]
+ lang = data["lang"]
+ if not lang or lang not in ["ca","es","en","fr"]:
+ lang="ca"
+ new_legal(lang,html)
+ except:
+ log.error(traceback.format_exc())
+ return json.dumps(data), 200, {"Content-Type": "application/json"}
+ # if item == "privacy":
+ # data = None
+ # try:
+ # data = request.json
+ # html = data["html"]
+ # lang = data["lang"]
+ # except:
+ # log.error(traceback.format_exc())
+ # return json.dumps(data), 200, {'Content-Type': 'application/json'}
diff --git a/admin/src/admin/views/WebViews.py b/admin/src/admin/views/WebViews.py
index 8af0365..1fbc8a4 100644
--- a/admin/src/admin/views/WebViews.py
+++ b/admin/src/admin/views/WebViews.py
@@ -29,6 +29,8 @@ from .decorators import is_admin
avatars = Avatars()
+from ..lib.legal import gen_legal_if_not_exists
+
""" OIDC TESTS """
# from ..auth.authentication import oidc
@@ -99,6 +101,13 @@ def legal():
# data = json.loads(requests.get("http://isard-sso-api/json").text)
return render_template("pages/legal.html", title="Legal", nav="Legal", data={})
+@app.route("/legal_text")
+def legal_text():
+ lang = request.args.get("lang")
+ if not lang or lang not in ["ca","es","en","fr"]:
+ lang="ca"
+ gen_legal_if_not_exists(lang)
+ return render_template("pages/legal/"+lang)
### SYS ADMIN
diff --git a/docker-compose-parts/admin.yml b/docker-compose-parts/admin.yml
index ae8fe00..4538bc2 100644
--- a/docker-compose-parts/admin.yml
+++ b/docker-compose-parts/admin.yml
@@ -22,6 +22,7 @@ services:
- ${DATA_FOLDER}/avatars:/admin/avatars:ro
- ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw
- ${DATA_FOLDER}/saml_certs:/admin/saml_certs:rw
+ - ${DATA_FOLDER}/legal:/admin/admin/static/templates/pages/legal:rw
env_file:
- .env
environment: