Initial commit
This commit is contained in:
commit
1df066fe6c
7
.env.example
Normal file
7
.env.example
Normal file
@ -0,0 +1,7 @@
|
||||
LDAP_SERVER=ldaps://ldap.example.com:636
|
||||
USER_DN="ou=accounts,dc=example,dc=com"
|
||||
GROUP_DN="ou=groups,dc=example,dc=com"
|
||||
LDAP_SERVICE_USER="uid=self-service-portal,ou=service,ou=accounts,dc=example,dc=com"
|
||||
LDAP_SERVICE_PASSWORD="yoursupersecurepassword"
|
||||
FLASK_SECRET_KEY=your_flask_secret
|
||||
JWT_SECRET=your_jwt_secret
|
||||
304
.gitignore
vendored
Normal file
304
.gitignore
vendored
Normal file
@ -0,0 +1,304 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python,vim,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python,vim,visualstudiocode
|
||||
|
||||
### PyCharm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python,vim,visualstudiocode
|
||||
145
app.py
Normal file
145
app.py
Normal file
@ -0,0 +1,145 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
from flask import Flask, request, redirect, url_for, render_template, flash
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user
|
||||
from ldap3 import Server, Connection, MODIFY_REPLACE
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
|
||||
|
||||
# Flask setup
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretflaskkey")
|
||||
|
||||
# Flask-Login setup
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
|
||||
# LDAP setup
|
||||
LDAP_SERVER = os.getenv("LDAP_SERVER")
|
||||
USER_DN = os.getenv("USER_DN")
|
||||
GROUP_DN = os.getenv("GROUP_DN")
|
||||
LDAP_SERVICE_USER = os.getenv("LDAP_SERVICE_USER")
|
||||
LDAP_SERVICE_PASSWORD = os.getenv("LDAP_SERVICE_PASSWORD")
|
||||
|
||||
# JWT setup
|
||||
JWT_SECRET = os.getenv("JWT_SECRET")
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_EXPIRATION_SECONDS = 3600
|
||||
|
||||
# User model
|
||||
class User(UserMixin):
|
||||
def __init__(self, id, dn):
|
||||
self.id = id
|
||||
self.dn = dn
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User(user_id, f"uid={user_id},{USER_DN}")
|
||||
|
||||
# JWT utility functions
|
||||
def generate_jwt(payload):
|
||||
expiration = datetime.utcnow() + timedelta(seconds=JWT_EXPIRATION_SECONDS)
|
||||
payload.update({"exp": expiration})
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
def decode_jwt(token):
|
||||
try:
|
||||
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||
except jwt.ExpiredSignatureError:
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
# Decorator for JWT authentication
|
||||
def jwt_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = request.cookies.get("jwt")
|
||||
if not token:
|
||||
flash("You must log in to access this page.", "error")
|
||||
return redirect(url_for("login"))
|
||||
payload = decode_jwt(token)
|
||||
if not payload:
|
||||
flash("Session expired or invalid. Please log in again.", "error")
|
||||
return redirect(url_for("login"))
|
||||
request.user = payload.get("username")
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Routes
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form["username"]
|
||||
password = request.form["password"]
|
||||
user_dn = f"uid={username},{USER_DN}"
|
||||
server = Server(LDAP_SERVER)
|
||||
conn = Connection(server, user=user_dn, password=password)
|
||||
if conn.bind():
|
||||
login_user(User(username, user_dn))
|
||||
token = generate_jwt({"username": username})
|
||||
response = redirect(url_for("profile"))
|
||||
response.set_cookie("jwt", token, httponly=True, secure=True)
|
||||
return response
|
||||
flash("Invalid credentials", "error")
|
||||
return render_template("login.html")
|
||||
|
||||
@app.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
response = redirect(url_for("login"))
|
||||
response.delete_cookie("jwt")
|
||||
return response
|
||||
|
||||
@app.route("/profile", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@jwt_required
|
||||
def profile():
|
||||
username = request.user
|
||||
user_dn = f"uid={username},{USER_DN}"
|
||||
server = Server(LDAP_SERVER)
|
||||
|
||||
# Connect as the service account
|
||||
service_conn = Connection(server, user=LDAP_SERVICE_USER, password=LDAP_SERVICE_PASSWORD, auto_bind=True)
|
||||
|
||||
service_conn.search(user_dn, "(objectClass=klUser)", attributes=["*"])
|
||||
user_attrs = service_conn.entries[0]
|
||||
|
||||
if request.method == "POST":
|
||||
if "protectedAccount" in user_attrs and user_attrs.protectedAccount == True:
|
||||
flash("User has protectedAccount: TRUE. Cannot edit!", "error")
|
||||
else:
|
||||
updates = {
|
||||
"sn": request.form["sn"],
|
||||
"cn": request.form["cn"],
|
||||
"givenName": request.form["givenName"],
|
||||
"sipPassword": request.form["sipPassword"],
|
||||
}
|
||||
for key, value in updates.items():
|
||||
service_conn.modify(user_dn, {key: [(MODIFY_REPLACE, [value])]})
|
||||
ssh_keys = request.form.get('sshPublicKey', '').split('\n')
|
||||
ssh_keys = map(str.strip, ssh_keys)
|
||||
ssh_keys = list(set(filter(lambda x: not x.startswith('#') and len(x) > 0, ssh_keys)))
|
||||
service_conn.modify(user_dn, {"sshPublicKey": [(MODIFY_REPLACE, ssh_keys)]})
|
||||
if "password" in request.form and request.form["password"].strip() and "password_repeat" in request.form and request.form["password_repeat"].strip():
|
||||
if request.form["password"] == request.form["password_repeat"]:
|
||||
hashed_password = "{SHA}" + base64.b64encode(hashlib.sha1(request.form["password"].encode()).digest()).decode()
|
||||
service_conn.modify(user_dn, {"userPassword": [(MODIFY_REPLACE, [hashed_password])]})
|
||||
else:
|
||||
flash("Passwords do not match", "error")
|
||||
flash("Profile updated successfully", "success")
|
||||
|
||||
service_conn.search(user_dn, "(objectClass=klUser)", attributes=["*"])
|
||||
user_attrs = service_conn.entries[0]
|
||||
service_conn.search(GROUP_DN, f"(member={user_dn})", attributes=["cn"])
|
||||
groups = [entry.cn.value for entry in service_conn.entries]
|
||||
return render_template("profile.html", user_attrs=user_attrs, groups=groups)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="127.0.0.1", port=5000)
|
||||
|
||||
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Flask==3.0.0 # Flask framework
|
||||
Flask-Login==0.6.3 # User session management
|
||||
ldap3==2.9 # LDAP connection and query handling
|
||||
PyJWT==2.8.0 # JWT generation and verification
|
||||
python-dotenv
|
||||
100
static/main.css
Normal file
100
static/main.css
Normal file
@ -0,0 +1,100 @@
|
||||
html {
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: 'Roboto Medium', sans-serif;
|
||||
}
|
||||
|
||||
input, button, textarea {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: white;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.center-box {
|
||||
background: none;
|
||||
position: relative;
|
||||
width: 60vw;
|
||||
margin: auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-box-content {
|
||||
}
|
||||
|
||||
form.form-control {
|
||||
width: 60%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
form.form-control label, form.form-control input, .form-control textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.form-control input, form.form-control button, .form-control textarea {
|
||||
margin-bottom: 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.m-1 {
|
||||
margin: 1em;
|
||||
}
|
||||
.m-2 {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5em;
|
||||
background-color: cornflowerblue;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: darkgreen;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: orange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: darkred;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.table-items-top td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.table-items-middle td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
47
static/reset.css
Normal file
47
static/reset.css
Normal file
@ -0,0 +1,47 @@
|
||||
/* 1. Use a more-intuitive box-sizing model */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 2. Remove default margin */
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
/* 3. Add accessible line-height */
|
||||
line-height: 1.5;
|
||||
/* 4. Improve text rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* 5. Improve media defaults */
|
||||
img, picture, video, canvas, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 6. Inherit fonts for form controls */
|
||||
input, button, textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* 7. Avoid text overflows */
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 8. Improve line wrapping */
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/*
|
||||
9. Create a root stacking context
|
||||
*/
|
||||
#root, #__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
17
templates/layout.html
Normal file
17
templates/layout.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='reset.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}" />
|
||||
{% block head %}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">{% block content %}{% endblock %}</div>
|
||||
<div id="footer">
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
16
templates/login.html
Normal file
16
templates/login.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block title %}Login{% endblock %}
|
||||
{% block content %}
|
||||
<div class="center-box">
|
||||
<div class="center-box-content m-2 center">
|
||||
<h1>Login</h1>
|
||||
<form method="POST" class="form-control m-auto">
|
||||
<label>Username:</label>
|
||||
<input type="text" name="username" required>
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit" class="btn btn-success">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
146
templates/profile.html
Normal file
146
templates/profile.html
Normal file
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<title>{{ user_attrs.cn }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}" />
|
||||
</head>
|
||||
<body>
|
||||
{% for category, mesg in get_flashed_messages(with_categories=True) %}
|
||||
<div class="flash-message flash-message-{{ category }}">{{ mesg }}</div>
|
||||
{% endfor %}
|
||||
<h2>Kombilösen: {{ user_attrs.cn }}</h2>
|
||||
<a href="/logout" class="btn">Logout</a><br><br>
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<form method="POST" class="form-control">
|
||||
<table class="table-items-top">
|
||||
<tr>
|
||||
<td>
|
||||
Benutzername:
|
||||
</td>
|
||||
<td>
|
||||
{{ user_attrs.uid }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Anzeigename:
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="cn" value="{{ user_attrs.cn }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Nachname:
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="sn" value="{{ user_attrs.sn }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>Vorname:</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="givenName" value="{{ user_attrs.givenName }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>Mail:</label>
|
||||
</td>
|
||||
<td>
|
||||
{{ user_attrs.mail }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>SIP Password:</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" name="sipPassword" value="{{ user_attrs.sipPassword }}" readonly>
|
||||
<button class="btn btn-warning" onclick="show_sip_password(); e.preventDefault()" type="button">Anzeigen</button>
|
||||
<button class="btn btn-danger" onclick="regenerate_sip_password(); e.preventDefault()" type="button">Neu generieren</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>Zugriff auf interne Daten:</label>
|
||||
</td>
|
||||
<td>
|
||||
{{ '✅' if user_attrs.innerCircle == True else '❌' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>Geschützter Account:</label>
|
||||
</td>
|
||||
<td>
|
||||
{{ '✅' if user_attrs.protectedAccount == True else '❌' }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-success">Speichern</button>
|
||||
|
||||
<h3>Passwort ändern</h3>
|
||||
<table class="table-items-top">
|
||||
<tr>
|
||||
<td>
|
||||
<label>Neues Passwort:</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" name="password">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>Neues Passwort (wiederholen):</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" name="password_repeat">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-success">Speichern</button>
|
||||
|
||||
<h3>SSH Public Keys</h3>
|
||||
<textarea name="sshPublicKey" cols="120" rows="8" wrap="off">
|
||||
{%- for key in user_attrs.sshPublicKey -%}
|
||||
{{- key + '\n' }}
|
||||
{%- endfor -%}
|
||||
</textarea>
|
||||
<br>
|
||||
<button type="submit" class="btn btn-success">Speichern</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h3>Gruppen</h3>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
function show_sip_password() {
|
||||
document.getElementsByName("sipPassword")[0].type = document.getElementsByName("sipPassword")[0].type === "password" ? "text" : "password"
|
||||
}
|
||||
function regenerate_sip_password() {
|
||||
document.getElementsByName("sipPassword")[0].value = generatePassword(12)
|
||||
}
|
||||
function generatePassword (
|
||||
length = 20,
|
||||
characters = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
) {
|
||||
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
|
||||
.map((x) => characters[x % characters.length])
|
||||
.join('')
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user