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