Merge branch 'develop'

This commit is contained in:
Tim Gröger 2024-01-18 16:00:11 +01:00
commit 957796e90e
129 changed files with 7363 additions and 5048 deletions

3
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,3 @@
# Migrate code style to Black
41e60425a96eeddb45f3a9f9bfc5ef491ce8e071
36c4027c5dd1c631bf6ca84890b6ad9c916e1888

19
.gitignore vendored
View File

@ -55,7 +55,6 @@ coverage.xml
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
@ -68,6 +67,8 @@ instance/
# Sphinx documentation
docs/_build/
# pdoc
docs/html
# PyBuilder
target/
@ -117,12 +118,16 @@ dmypy.json
#ide
.idea
*.swp
*.swo
.vscode/
*.log
.fleet/
# custom
test_pricelist/
test_project/
config.yml
geruecht.config.yml
data/
# config
flaschengeist/flaschengeist.toml
# start flaschengeist in pycharme professional
run_flaschengeist_pycharm.py

6
.woodpecker/lint.yml Normal file
View File

@ -0,0 +1,6 @@
pipeline:
lint:
image: python:slim
commands:
- pip install black
- black --check --line-length 120 --target-version=py39 .

19
.woodpecker/test.yml Normal file
View File

@ -0,0 +1,19 @@
pipeline:
install:
image: python:${PYTHON}-slim
commands:
- python -m venv --clear venv
- export PATH=venv/bin:$PATH
- python -m pip install --upgrade pip
- pip install -v ".[tests]"
test:
image: python:${PYTHON}-slim
commands:
- export PATH=venv/bin:$PATH
- python -m pytest
matrix:
PYTHON:
- 3.10
- 3.9

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright 2021 Tim Gröger | Flaschengeist Developers
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

116
README.md Normal file
View File

@ -0,0 +1,116 @@
# Flaschengeist
![status-badge](https://ci.os-sc.org/api/badges/Flaschengeist/flaschengeist/status.svg)
This is the backend of the Flaschengeist.
## Installation
### Requirements
- `mysql` or `mariadb`
- maybe `libmariadb` development files[1]
- python 3.9+
- pip 21.0+
*[1] By default Flaschengeist uses mysql as database backend, if you are on Windows Flaschengeist uses `PyMySQL`, but on
Linux / Mac the faster `mysqlclient` is used, if it is not already installed installing from pypi requires the
development files for `libmariadb` to be present on your system.*
### Install python files
It is recommended to upgrade pip to the latest version before installing:
python -m pip install --upgrade pip
Default installation with *mariadb*/*mysql* support:
pip3 install --user ".[mysql]"
or with ldap support
pip3 install --user ".[ldap]"
or if you want to also run the tests:
pip3 install --user ".[ldap,tests]"
You will also need a MySQL driver, by default one of this is installed:
- `mysqlclient` (non Windows)
- `PyMySQL` (on Windows)
#### Hint on MySQL driver on Windows:
If you want to use `mysqlclient` instead of `PyMySQL` (performance?) you have to follow [this guide](https://www.radishlogic.com/coding/python-3/installing-mysqldb-for-python-3-in-windows/)
### Install database
The user needs to have full permissions to the database.
If not you need to create user and database manually do (or similar on Windows):
(
echo "CREATE DATABASE flaschengeist;"
echo "CREATE USER 'flaschengeist'@'localhost' IDENTIFIED BY 'flaschengeist';"
echo "GRANT ALL PRIVILEGES ON flaschengeist.* TO 'flaschengeist'@'localhost';"
echo "FLUSH PRIVILEGES;"
) | sudo mysql
Then you can install the database tables, this will update all tables from core + all enabled plugins.
And also install all enabled plugins:
$ flaschengeist install
*Hint:* To only install the database tables, or upgrade the database after plugins or core are updated later
you can use this command:
$ flaschengeist db upgrade heads
## Plugins
To only upgrade one plugin (for example the `events` plugin):
$ flaschengeist db upgrade events@head
## Configuration
Configuration is done within the a `flaschengeist.toml`file, you can copy the one located inside the module path
(where flaschegeist is installed) or create an empty one and place it inside either:
1. `~/.config/`
2. A custom path and set environment variable `FLASCHENGEIST_CONF`
Uncomment and change at least all the database parameters!
#### CRON
Some functionality used by some plugins rely on regular updates,
but as flaschengeists works as an WSGI app it can not controll when it gets called.
So you have to configure one of the following options to call flaschengeists CRON tasks:
1. Passive Web-CRON: Every time an users calls flaschengeist a task is scheduled (**NOT RECOMMENDED**)
- Pros: No external configuration needed
- Cons: Slower user experience, no guaranteed execution time of tasks
2. Active Web-CRON: You configure a webworker to call `<flaschengeist>/cron`
- Pros: Guaranteed execution interval, no impact on user experience (at least if you do not limit wsgi worker threads)
- Cons: Uses one of the webserver threads while executing
### Run
Flaschengeist provides a CLI, based on the flask CLI, respectivly called `flaschengeist`.
⚠️ When using the CLI for running Flaschengeist, please note that logging will happen as configured,
with the difference of the main logger will be forced to output to `stderr` and the logging level
of the CLI will override the logging level you have configured for the main logger.
$ flaschengeist run
or with debug messages:
$ flaschengeist run --debug
This will run the backend on http://localhost:5000
## Tests
$ pip install '.[test]'
$ pytest
Run with coverage report:
$ coverage run -m pytest
$ coverage report
Or with html output (open `htmlcov/index.html` in a browser):
$ coverage html
## Development
Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).

View File

@ -0,0 +1,57 @@
# Plugin Development
## File Structure
- your_plugin/
- __init__.py
- ...
- migrations/ (optional)
- ...
- setup.cfg
The basic layout of a plugin is quite simple, you will only need the `setup.cfg` or `setup.py` and
the package containing your plugin code, at lease a `__init__.py` file with your `Plugin` class.
If you use custom database tables you need to provide a `migrations` directory within your package,
see next section.
## Database Tables / Migrations
To allow upgrades of installed plugins, the database is versioned and handled
through [Alembic](https://alembic.sqlalchemy.org/en/latest/index.html) migrations.
Each plugin, which uses custom database tables, is represented as an other base.
So you could simply follow the Alembic tutorial on [how to work with multiple bases](https://alembic.sqlalchemy.org/en/latest/branches.html#creating-a-labeled-base-revision).
A quick overview on how to work with migrations for your plugin:
$ flaschengeist db revision -m "Create my super plugin" \
--head=base --branch-label=myplugin_name --version-path=your/plugin/migrations
This would add a new base named `myplugin_name`, which should be the same as the pypi name of you plugin.
If your tables depend on an other plugin or a specific base version you could of cause add
--depends-on=VERSION
or
--depends-on=other_plugin
### Plugin Removal and Database Tables
As generic downgrades are most often hard to write, your plugin is not required to provide such functionallity.
For Flaschengeist only instable versions provide meaningful downgrade migrations down to the latest stable version.
So this means if you do not provide downgrades you must at lease provide a series of migrations toward removal of
the database tables in case the users wants to delete the plugin.
(base) ----> 1.0 <----> 1.1 <----> 1.2
|
--> removal
After the removal step the database is stamped to to "remove" your
## Useful Hooks
There are some predefined hooks, which might get handy for you.
For more information, please refer to
- `flaschengeist.utils.hook.HookBefore` and
- `flaschengeist.utils.hook.HookAfter`

8
flaschengeist.wsgi Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/python3
# If you use a virtual env, this might become handy:
# activate_this = '/path/to/env/bin/activate_this.py'
# with open(activate_this) as file_:
# exec(file_.read(), dict(__file__=activate_this))
from flaschengeist.app import create_app
application = create_app()

View File

@ -0,0 +1,9 @@
"""Flaschengeist"""
import logging
from importlib.metadata import version
__version__ = version("flaschengeist")
__pdoc__ = {}
logger = logging.getLogger(__name__)
__pdoc__["logger"] = "Flaschengeist's logger instance (`werkzeug.local.LocalProxy`)"

View File

@ -0,0 +1,5 @@
from pathlib import Path
alembic_migrations_path = str(Path(__file__).resolve().parent / "migrations")
alembic_script_path = str(Path(__file__).resolve().parent)

View File

@ -0,0 +1,53 @@
# A generic, single database configuration.
# No used by flaschengeist
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
version_path_separator = os
version_locations = %(here)s/migrations
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,74 @@
import logging
from logging.config import fileConfig
from pathlib import Path
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(Path(config.get_main_option("script_location")) / config.config_file_name.split("/")[-1])
logger = logging.getLogger("alembic.env")
config.set_main_option("sqlalchemy.url", str(current_app.extensions["migrate"].db.get_engine().url).replace("%", "%%"))
target_metadata = current_app.extensions["migrate"].db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info("No changes in schema detected.")
connectable = current_app.extensions["migrate"].db.get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions["migrate"].configure_args,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,154 @@
"""Initial core db
Revision ID: 20482a003db8
Revises:
Create Date: 2022-08-25 15:13:34.900996
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
# revision identifiers, used by Alembic.
revision = "20482a003db8"
down_revision = None
branch_labels = ("flaschengeist",)
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"image",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("filename", sa.String(length=255), nullable=False),
sa.Column("mimetype", sa.String(length=127), nullable=False),
sa.Column("thumbnail", sa.String(length=255), nullable=True),
sa.Column("path", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_image")),
)
op.create_table(
"plugin",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("name", sa.String(length=127), nullable=False),
sa.Column("version", sa.String(length=30), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin")),
)
op.create_table(
"role",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_role")),
sa.UniqueConstraint("name", name=op.f("uq_role_name")),
)
op.create_table(
"permission",
sa.Column("name", sa.String(length=30), nullable=True),
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_permission_plugin_plugin")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_permission")),
sa.UniqueConstraint("name", name=op.f("uq_permission_name")),
)
op.create_table(
"plugin_setting",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=True),
sa.Column("name", sa.String(length=127), nullable=False),
sa.Column("value", sa.PickleType(), nullable=True),
sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_plugin_setting_plugin_plugin")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin_setting")),
)
op.create_table(
"user",
sa.Column("userid", sa.String(length=30), nullable=False),
sa.Column("display_name", sa.String(length=30), nullable=True),
sa.Column("firstname", sa.String(length=50), nullable=False),
sa.Column("lastname", sa.String(length=50), nullable=False),
sa.Column("deleted", sa.Boolean(), nullable=True),
sa.Column("birthday", sa.Date(), nullable=True),
sa.Column("mail", sa.String(length=60), nullable=True),
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("avatar", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(["avatar"], ["image.id"], name=op.f("fk_user_avatar_image")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_user")),
sa.UniqueConstraint("userid", name=op.f("uq_user_userid")),
)
op.create_table(
"notification",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("text", sa.Text(), nullable=True),
sa.Column("data", sa.PickleType(), nullable=True),
sa.Column("time", flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column("user", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_notification_plugin_plugin")),
sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_notification_user_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_notification")),
)
op.create_table(
"password_reset",
sa.Column("user", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("token", sa.String(length=32), nullable=True),
sa.Column("expires", flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_password_reset_user_user")),
sa.PrimaryKeyConstraint("user", name=op.f("pk_password_reset")),
)
op.create_table(
"role_x_permission",
sa.Column("role_id", flaschengeist.database.types.Serial(), nullable=True),
sa.Column("permission_id", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(
["permission_id"], ["permission.id"], name=op.f("fk_role_x_permission_permission_id_permission")
),
sa.ForeignKeyConstraint(["role_id"], ["role.id"], name=op.f("fk_role_x_permission_role_id_role")),
)
op.create_table(
"session",
sa.Column("expires", flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.Column("token", sa.String(length=32), nullable=True),
sa.Column("lifetime", sa.Integer(), nullable=True),
sa.Column("browser", sa.String(length=127), nullable=True),
sa.Column("platform", sa.String(length=64), nullable=True),
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("user_id", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_session_user_id_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_session")),
sa.UniqueConstraint("token", name=op.f("uq_session_token")),
)
op.create_table(
"user_attribute",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("user", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=True),
sa.Column("value", sa.PickleType(), nullable=True),
sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_user_attribute_user_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_user_attribute")),
)
op.create_table(
"user_x_role",
sa.Column("user_id", flaschengeist.database.types.Serial(), nullable=True),
sa.Column("role_id", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(["role_id"], ["role.id"], name=op.f("fk_user_x_role_role_id_role")),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_user_x_role_user_id_user")),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("user_x_role")
op.drop_table("user_attribute")
op.drop_table("session")
op.drop_table("role_x_permission")
op.drop_table("password_reset")
op.drop_table("notification")
op.drop_table("user")
op.drop_table("plugin_setting")
op.drop_table("permission")
op.drop_table("role")
op.drop_table("plugin")
op.drop_table("image")
# ### end Alembic commands ###

View File

@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

113
flaschengeist/app.py Normal file
View File

@ -0,0 +1,113 @@
import enum
import json
from flask import Flask
from flask_cors import CORS
from datetime import datetime, date
from flask.json import jsonify
from json import JSONEncoder
from flask.json.provider import JSONProvider
from sqlalchemy.exc import OperationalError
from werkzeug.exceptions import HTTPException
from flaschengeist import logger
from flaschengeist.controller import pluginController
from flaschengeist.utils.hook import Hook
from flaschengeist.config import configure_app
from flaschengeist.database import db
class CustomJSONEncoder(JSONEncoder):
def default(self, o):
try:
# Check if custom model
return o.serialize()
except AttributeError:
pass
if isinstance(o, datetime) or isinstance(o, date):
return o.isoformat()
if isinstance(o, enum.Enum):
return o.value
try:
# Check if iterable
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
return JSONEncoder.default(self, o)
class CustomJSONProvider(JSONProvider):
ensure_ascii: bool = True
sort_keys: bool = True
def dumps(self, obj, **kwargs):
kwargs.setdefault("ensure_ascii", self.ensure_ascii)
kwargs.setdefault("sort_keys", self.sort_keys)
return json.dumps(obj, **kwargs, cls=CustomJSONEncoder)
def loads(self, s: str | bytes, **kwargs):
return json.loads(s, **kwargs)
@Hook("plugins.loaded")
def load_plugins(app: Flask):
app.config["FG_PLUGINS"] = {}
for plugin in pluginController.get_enabled_plugins():
logger.debug(f"Searching for enabled plugin {plugin.name}")
try:
# Load class
cls = plugin.entry_point.load()
# plugin = cls.query.get(plugin.id) if plugin.id is not None else plugin
# plugin = db.session.query(cls).get(plugin.id) if plugin.id is not None else plugin
plugin = db.session.get(cls, plugin.id) if plugin.id is not None else plugin
# Custom loading tasks
plugin.load()
# Register blueprint
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
app.register_blueprint(plugin.blueprint)
except:
logger.error(
f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.",
exc_info=True,
)
continue
logger.info(f"Loaded plugin: {plugin.name}")
app.config["FG_PLUGINS"][plugin.name] = plugin
def create_app(test_config=None, cli=False):
app = Flask("flaschengeist")
app.json_provider_class = CustomJSONProvider
app.json = CustomJSONProvider(app)
CORS(app)
with app.app_context():
from flaschengeist.database import db, migrate
configure_app(app, test_config)
db.init_app(app)
migrate.init_app(app, db, compare_type=True)
load_plugins(app)
@app.route("/", methods=["GET"])
def __get_state():
from . import __version__ as version
return jsonify({"plugins": pluginController.get_loaded_plugins(), "version": version})
@app.errorhandler(Exception)
def handle_exception(e):
if isinstance(e, HTTPException):
logger.debug(e.description, exc_info=True)
return jsonify({"error": e.description}), e.code
if isinstance(e, OperationalError):
logger.error(e, exc_info=True)
return {"error": "Database unavailable"}, 504
logger.error(str(e), exc_info=True)
return jsonify({"error": "Internal server error occurred"}), 500
return app

View File

@ -0,0 +1,123 @@
import io
import sys
import inspect
import logging
class InterfaceGenerator:
known = []
classes = {}
mapper = {
"str": "string",
"int": "number",
"float": "number",
"date": "Date",
"datetime": "Date",
"NoneType": "null",
"bool": "boolean",
}
def __init__(self, namespace, filename, logger=logging.getLogger()):
self.basename = ""
self.namespace = namespace
self.filename = filename
self.this_type = None
self.logger = logger
def pytype(self, cls):
a = self._pytype(cls)
return a
def _pytype(self, cls):
import typing
origin = typing.get_origin(cls)
arguments = typing.get_args(cls)
if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef):
return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__
if origin is typing.Union:
if len(arguments) == 2 and arguments[1] is type(None):
return "?", self.pytype(arguments[0])[1]
else:
return "", "|".join([self.pytype(pt)[1] for pt in arguments])
if origin is list:
return "", "Array<{}>".format("|".join([self.pytype(a_type)[1] for a_type in arguments]))
if cls is typing.Any:
return "", "any"
name = cls.__name__ if hasattr(cls, "__name__") else cls if isinstance(cls, str) else None
if name is not None:
if name in self.mapper:
return "", self.mapper[name]
else:
return "", name
self.logger.warning(f"This python version might not detect all types (try >= 3.9). Could not identify >{cls}<")
return "?", "any"
def walker(self, module):
if sys.version_info < (3, 9):
raise RuntimeError("Python >= 3.9 is required to export API")
import typing
if (
inspect.ismodule(module[1])
and module[1].__name__.startswith(self.basename)
and module[1].__name__ not in self.known
):
self.known.append(module[1].__name__)
for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
self.walker(cls)
elif (
inspect.isclass(module[1])
and module[1].__module__.startswith(self.basename)
and module[0] not in self.classes
and not module[0].startswith("_")
and hasattr(module[1], "__annotations__")
):
self.this_type = module[0]
d = {}
for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items():
if not param.startswith("_") and not param.endswith("_"):
d[param] = self.pytype(ptype)
if len(d) == 1:
key, value = d.popitem()
self.classes[module[0]] = value[1]
else:
self.classes[module[0]] = d
def run(self, models):
self.basename = models.__name__
self.walker(("models", models))
def _write_types(self):
TYPE = "type {name} = {alias};\n"
INTERFACE = "interface {name} {{\n{properties}}}\n"
PROPERTY = "\t{name}{modifier}: {type};\n"
buffer = io.StringIO()
for cls, props in self.classes.items():
if isinstance(props, str):
buffer.write(TYPE.format(name=cls, alias=props))
else:
buffer.write(
INTERFACE.format(
name=cls,
properties="".join(
[PROPERTY.format(name=name, modifier=props[name][0], type=props[name][1]) for name in props]
),
)
)
return buffer
def write(self):
with open(self.filename, "w") if self.filename else sys.stdout as file:
if self.namespace:
file.write(f"declare namespace {self.namespace} {{\n")
for line in self._write_types().getvalue().split("\n"):
file.write(f"\t{line}\n")
file.write("}\n")
else:
file.write(self._write_types().getvalue())

View File

@ -0,0 +1,103 @@
from os import environ
import sys
import click
import logging
from flask.cli import FlaskGroup, with_appcontext
from flaschengeist import logger
from flaschengeist.app import create_app
LOGGING_MIN = 5 # TRACE (custom)
LOGGING_MAX = logging.ERROR
def get_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
import platform
from werkzeug import __version__ as werkzeug_version
from flask import __version__ as flask_version
from flaschengeist import __version__
click.echo(
f"Python {platform.python_version()}\n"
f"Flask {flask_version}\n"
f"Werkzeug {werkzeug_version}\n"
f"Flaschengeist {__version__}",
color=ctx.color,
)
ctx.exit()
def configure_logger(level):
"""Reconfigure main logger"""
global logger
# Handle TRACE -> meaning enable debug even for werkzeug
if level == 5:
level = 10
logging.getLogger("werkzeug").setLevel(level)
logger.setLevel(level)
environ["FG_LOGGING"] = logging.getLevelName(level)
for h in logger.handlers:
if isinstance(h, logging.StreamHandler) and h.name == "wsgi":
h.setLevel(level)
h.setStream(sys.stderr)
@with_appcontext
def verbosity(ctx, param, value):
"""Callback: Toggle verbosity between ERROR <-> TRACE"""
if not value or ctx.resilient_parsing:
return
configure_logger(LOGGING_MAX - max(LOGGING_MIN, min(value * 10, LOGGING_MAX - LOGGING_MIN)))
@click.group(
cls=FlaskGroup,
add_version_option=False,
add_default_commands=False,
create_app=create_app,
)
@click.option(
"--version",
help="Show the flask version",
expose_value=False,
callback=get_version,
is_flag=True,
is_eager=True,
)
@click.option(
"--verbose",
"-v",
help="Increase logging level",
callback=verbosity,
count=True,
expose_value=False,
)
def cli():
"""Management script for the Flaschengeist application."""
pass
def main(*args, **kwargs):
from .plugin_cmd import plugin
from .export_cmd import export
from .docs_cmd import docs
from .run_cmd import run
from .install_cmd import install
from .docker_cmd import docker
# Override logging level
environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX))
cli.add_command(export)
cli.add_command(docs)
cli.add_command(install)
cli.add_command(plugin)
cli.add_command(run)
cli.add_command(docker)
cli(*args, **kwargs)

View File

@ -0,0 +1,54 @@
import click
from click.decorators import pass_context
from flask.cli import with_appcontext
from os import environ
from flaschengeist import logger
from flaschengeist.controller import pluginController
from werkzeug.exceptions import NotFound
import traceback
@click.group()
def docker():
pass
@docker.command()
@with_appcontext
@pass_context
def setup(ctx):
"""Setup flaschengesit in docker container"""
click.echo("Setup docker")
plugins = environ.get("FG_ENABLE_PLUGINS")
if not plugins:
click.secho("no evironment variable is set for 'FG_ENABLE_PLUGINS'", fg="yellow")
click.secho("set 'FG_ENABLE_PLUGINS' to 'auth_ldap', 'mail', 'balance', 'pricelist_old', 'events'")
plugins = ("auth_ldap", "mail", "pricelist_old", "events", "balance")
else:
plugins = plugins.split(" ")
print(plugins)
for name in plugins:
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
try:
pluginController.install_plugin(name)
except Exception as e:
click.secho(" failed", fg="red")
if logger.getEffectiveLevel() > 10:
ctx.fail(f"[{e.__class__.__name__}] {e}")
else:
ctx.fail(traceback.format_exc())
else:
click.secho(" ok", fg="green")
for name in plugins:
click.echo(f"Enabling {name}{'.'*(20-len(name))}", nl=False)
try:
pluginController.enable_plugin(name)
click.secho(" ok", fg="green")
except NotFound:
click.secho(" not installed / not found", fg="red")

View File

@ -0,0 +1,38 @@
import click
import pathlib
import subprocess
@click.command()
@click.option(
"--output",
"-o",
help="Documentation output path",
default="./docs/html",
type=click.Path(file_okay=False, path_type=pathlib.Path),
)
@click.pass_context
def docs(ctx: click.Context, output: pathlib.Path):
"""Generate and export API documentation using pdoc"""
import pkg_resources
try:
pkg_resources.get_distribution("pdoc>=8.0.1")
except pkg_resources.DistributionNotFound:
click.echo(
f"Error: pdoc was not found, maybe you need to install it. Try:\n" "\n" '$ pip install "pdoc>=8.0.1"\n'
)
ctx.exit(1)
output.mkdir(parents=True, exist_ok=True)
command = [
"python",
"-m",
"pdoc",
"--docformat",
"google",
"--output-directory",
str(output),
"flaschengeist",
]
click.echo(f"Running command: {command}")
subprocess.check_call(command)

View File

@ -0,0 +1,29 @@
import click
from importlib.metadata import entry_points
@click.command()
@click.option("--output", "-o", help="Output file, default is stdout", type=click.Path())
@click.option("--namespace", "-n", help="TS namespace for the interfaces", type=str, show_default=True)
@click.option("--plugin", "-p", help="Also export types for a plugin (even if disabled)", multiple=True, type=str)
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
def export(namespace, output, no_core, plugin):
from flaschengeist import logger, models
from flaschengeist.cli.InterfaceGenerator import InterfaceGenerator
gen = InterfaceGenerator(namespace, output, logger)
if not no_core:
gen.run(models)
if plugin:
for entry_point in entry_points(group="flaschengeist.plugins"):
if len(plugin) == 0 or entry_point.name in plugin:
try:
plugin = entry_point.load()
gen.run(plugin.models)
except:
logger.error(
f"Plugin {entry_point.name} could not be loaded due to an error.",
exc_info=True,
)
continue
gen.write()

View File

@ -0,0 +1,23 @@
import click
from click.decorators import pass_context
from flask.cli import with_appcontext
from flask_migrate import upgrade
from flaschengeist.controller import pluginController
from flaschengeist.utils.hook import Hook
@click.command()
@with_appcontext
@pass_context
@Hook("plugins.installed")
def install(ctx: click.Context):
plugins = pluginController.get_enabled_plugins()
# Install database
upgrade(revision="flaschengeist@head")
# Install plugins
for plugin in plugins:
plugin = pluginController.install_plugin(plugin.name)
pluginController.enable_plugin(plugin.id)

View File

@ -0,0 +1,144 @@
import traceback
import click
from click.decorators import pass_context
from flask import current_app
from flask.cli import with_appcontext
from importlib.metadata import EntryPoint, entry_points
from flaschengeist import logger
from flaschengeist.controller import pluginController
from werkzeug.exceptions import NotFound
@click.group()
def plugin():
pass
@plugin.command()
@click.argument("plugin", nargs=-1, required=True, type=str)
@with_appcontext
@pass_context
def enable(ctx, plugin):
"""Enable one or more plugins"""
for name in plugin:
click.echo(f"Enabling {name}{'.'*(20-len(name))}", nl=False)
try:
pluginController.enable_plugin(name)
click.secho(" ok", fg="green")
except NotFound:
click.secho(" not installed / not found", fg="red")
@plugin.command()
@click.argument("plugin", nargs=-1, required=True, type=str)
@with_appcontext
@pass_context
def disable(ctx, plugin):
"""Disable one or more plugins"""
for name in plugin:
click.echo(f"Disabling {name}{'.'*(20-len(name))}", nl=False)
try:
pluginController.disable_plugin(name)
click.secho(" ok", fg="green")
except NotFound:
click.secho(" not installed / not found", fg="red")
@plugin.command()
@click.argument("plugin", nargs=-1, type=str)
@click.option("--all", help="Install all enabled plugins", is_flag=True)
@with_appcontext
@pass_context
def install(ctx: click.Context, plugin, all):
"""Install one or more plugins"""
all_plugins = entry_points(group="flaschengeist.plugins")
if all:
plugins = [ep.name for ep in all_plugins]
elif len(plugin) > 0:
plugins = plugin
for name in plugin:
if not all_plugins.select(name=name):
ctx.fail(f"Invalid plugin name, could not find >{name}<")
else:
ctx.fail("At least one plugin must be specified, or use `--all` flag.")
for name in plugins:
click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False)
try:
pluginController.install_plugin(name)
except Exception as e:
click.secho(" failed", fg="red")
if logger.getEffectiveLevel() > 10:
ctx.fail(f"[{e.__class__.__name__}] {e}")
else:
ctx.fail(traceback.format_exc())
else:
click.secho(" ok", fg="green")
@plugin.command()
@click.argument("plugin", nargs=-1, required=True, type=str)
@with_appcontext
@pass_context
def uninstall(ctx: click.Context, plugin):
"""Uninstall one or more plugins"""
plugins = {plg.name: plg for plg in pluginController.get_installed_plugins() if plg.name in plugin}
try:
for name in plugin:
pluginController.disable_plugin(plugins[name])
if (
click.prompt(
"You are going to uninstall:\n\n"
f"\t{', '.join([plugin_name for plugin_name in plugins.keys()])}\n\n"
"Are you sure?",
default="n",
show_choices=True,
type=click.Choice(["y", "N"], False),
).lower()
!= "y"
):
ctx.exit()
click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False)
pluginController.uninstall_plugin(plugins[name])
click.secho(" ok", fg="green")
except KeyError:
ctx.fail(f"Invalid plugin ID, could not find >{name}<")
@plugin.command()
@click.option("--enabled", "-e", help="List only enabled plugins", is_flag=True)
@click.option("--no-header", "-n", help="Do not show header", is_flag=True)
@with_appcontext
def ls(enabled, no_header):
def plugin_version(p):
if isinstance(p, EntryPoint):
return p.dist.version
return p.version
plugins = entry_points(group="flaschengeist.plugins")
installed_plugins = {plg.name: plg for plg in pluginController.get_installed_plugins()}
loaded_plugins = current_app.config["FG_PLUGINS"].keys()
if not no_header:
print(f"{' '*13}{'name': <20}| version | {' ' * 8} state")
print("-" * 63)
for plugin in plugins:
is_installed = plugin.name in installed_plugins.keys()
is_enabled = is_installed and installed_plugins[plugin.name].enabled
if enabled and is_enabled:
continue
print(f"{plugin.name: <33}|{plugin_version(plugin): >12} | ", end="")
if is_enabled:
if plugin.name in loaded_plugins:
print(click.style(" enabled", fg="green"))
else:
print(click.style("(failed to load)", fg="red"))
elif is_installed:
print(click.style(" disabled", fg="yellow"))
else:
print("not installed")
for name, plugin in installed_plugins.items():
if plugin.enabled and name not in loaded_plugins:
print(f"{name: <33}|{'': >12} |" f"{click.style(' failed to load', fg='red')}")

View File

@ -0,0 +1,37 @@
import click
from os import environ
from flask import current_app
from flask.cli import with_appcontext, run_command
class PrefixMiddleware(object):
def __init__(self, app, prefix=""):
self.app = app
self.prefix = prefix
def __call__(self, environ, start_response):
if environ["PATH_INFO"].startswith(self.prefix):
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
environ["SCRIPT_NAME"] = self.prefix
return self.app(environ, start_response)
else:
start_response("404", [("Content-Type", "text/plain")])
return ["This url does not belong to the app.".encode()]
@click.command()
@click.option("--host", help="set hostname to listen on", default="127.0.0.1", show_default=True)
@click.option("--port", help="set port to listen on", type=int, default=5000, show_default=True)
@click.option("--debug", help="run in debug mode", is_flag=True)
@with_appcontext
@click.pass_context
def run(ctx, host, port, debug):
"""Run Flaschengeist using a development server."""
from flaschengeist.config import config
current_app.wsgi_app = PrefixMiddleware(current_app.wsgi_app, prefix=config["FLASCHENGEIST"].get("root", ""))
if debug:
environ["FLASK_DEBUG"] = "1"
environ["FLASK_ENV"] = "development"
ctx.invoke(run_command, reload=True, host=host, port=port, debugger=debug)

122
flaschengeist/config.py Normal file
View File

@ -0,0 +1,122 @@
import os
import toml
import collections.abc
from pathlib import Path
from logging.config import dictConfig
from werkzeug.middleware.proxy_fix import ProxyFix
from flaschengeist import logger
# Default config:
config = {"DATABASE": {"engine": "mysql", "port": 3306}}
def update_dict(d, u):
for k, v in u.items():
if isinstance(v, collections.abc.Mapping):
d[k] = update_dict(d.get(k, {}), v)
else:
d[k] = v
return d
def read_configuration(test_config):
global config
paths = [Path(__file__).parent]
if not test_config:
paths.append(Path.home() / ".config")
if "FLASCHENGEIST_CONF" in os.environ:
paths.append(Path(str(os.environ.get("FLASCHENGEIST_CONF"))))
for loc in paths:
try:
with (loc / "flaschengeist.toml").open() as source:
logger.warning(f"Reading config file from >{loc}<") # default root logger, goes to stderr
update_dict(config, toml.load(source))
except IOError:
pass
if test_config:
update_dict(config, test_config)
def configure_logger():
"""Configure the logger
force_console: Force a console handler
"""
def set_level(level):
# TRACE means even with werkzeug's request traces
if isinstance(level, str) and level.lower() == "trace":
level = "DEBUG"
logger_config["loggers"]["werkzeug"] = {"level": level}
logger_config["loggers"]["flaschengeist"] = {"level": level}
logger_config["handlers"]["wsgi"]["level"] = level
# Read default config
logger_config = toml.load(Path(__file__).parent / "logging.toml")
if "LOGGING" in config:
# Override with user config
update_dict(logger_config, config.get("LOGGING"))
# Check for shortcuts
if "level" in config["LOGGING"]:
set_level(config["LOGGING"]["level"])
# Override logging, used e.g. by CLI
if "FG_LOGGING" in os.environ:
set_level(os.environ.get("FG_LOGGING", "CRITICAL"))
dictConfig(logger_config)
def configure_app(app, test_config=None):
global config
read_configuration(test_config)
configure_logger()
if "secret_key" not in config["FLASCHENGEIST"]:
logger.critical("No secret key was configured, please configure one for production systems!")
raise RuntimeError("No secret key was configured")
app.config["SECRET_KEY"] = config["FLASCHENGEIST"]["secret_key"]
if test_config is not None:
config["DATABASE"]["engine"] = "sqlite"
if config["DATABASE"]["engine"] == "mysql":
engine = "mysql"
try:
# Try mysqlclient first
from MySQLdb import _mysql
except ModuleNotFoundError:
engine += "+pymysql"
options = "?charset=utf8mb4"
elif config["DATABASE"]["engine"] == "postgres":
engine = "postgresql+psycopg2"
options = "?client_encoding=utf8"
elif config["DATABASE"]["engine"] == "sqlite":
engine = "sqlite"
options = ""
host = ""
else:
logger.error(f"Invalid database engine configured. >{config['DATABASE']['engine']}< is unknown")
raise Exception
if config["DATABASE"]["engine"] in ["mysql", "postgresql"]:
host = "{user}:{password}@{host}:{port}".format(
user=config["DATABASE"]["user"],
password=config["DATABASE"]["password"],
host=config["DATABASE"]["host"],
port=config["DATABASE"]["port"],
)
app.config["SQLALCHEMY_DATABASE_URI"] = f"{engine}://{host}/{config['DATABASE']['database']}{options}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
if "root" in config["FLASCHENGEIST"]:
logger.debug("Setting application root to >{}<".format(config["FLASCHENGEIST"]["root"]))
app.config["APPLICATION_ROOT"] = config["FLASCHENGEIST"]["root"]
if config["FLASCHENGEIST"].get("proxy", False):
logger.debug("Fixing wsgi_app for using behind a proxy server")
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

View File

@ -0,0 +1 @@
"""Basic controllers for interaction with the Flaschengeist core"""

View File

@ -0,0 +1,64 @@
from datetime import date
from pathlib import Path
from flask import send_file
from PIL import Image as PImage
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import NotFound, UnprocessableEntity
from ..models import Image
from ..database import db
from ..config import config
def check_mimetype(mime: str):
return mime in config["FILES"].get("allowed_mimetypes", [])
def send_image(id: int = None, image: Image = None):
if image is None:
image = Image.query.get(id)
if not image:
raise NotFound
return send_file(image.path_, mimetype=image.mimetype_, download_name=image.filename_)
def send_thumbnail(id: int = None, image: Image = None):
if image is None:
image = Image.query.get(id)
if not image:
raise NotFound
if not image.thumbnail_:
with PImage.open(image.open()) as im:
im.thumbnail(tuple(config["FILES"].get("thumbnail_size")))
s = image.path_.split(".")
s.insert(len(s) - 1, "thumbnail")
im.save(".".join(s))
image.thumbnail_ = ".".join(s)
db.session.commit()
return send_file(image.thumbnail_, mimetype=image.mimetype_, download_name=image.filename_)
def upload_image(file: FileStorage):
if not check_mimetype(file.mimetype):
raise UnprocessableEntity
path = Path(config["FILES"].get("data_path")) / str(date.today().year)
path.mkdir(mode=int("0700", 8), parents=True, exist_ok=True)
if file.filename.count(".") < 1:
name = secure_filename(file.filename + "." + file.mimetype.split("/")[-1])
else:
name = secure_filename(file.filename)
img = Image(mimetype_=file.mimetype, filename_=name)
db.session.add(img)
db.session.flush()
try:
img.path_ = str((path / f"{img.id}.{img.filename_.split('.')[-1]}").resolve())
file.save(img.path_)
except:
db.session.delete(img)
raise
finally:
db.session.commit()
return img

View File

@ -0,0 +1,14 @@
from ..utils.hook import Hook
from ..models import User, Role
class Message:
def __init__(self, receiver: User or Role, message: str, subject: str):
self.message = message
self.subject = subject
self.receiver = receiver
@Hook
def send_message(message: Message):
pass

View File

@ -0,0 +1,172 @@
"""Controller for Plugin logic
Used by plugins for setting and notification functionality.
"""
from typing import Union, List
from flask import current_app
from werkzeug.exceptions import NotFound, BadRequest
from sqlalchemy.exc import OperationalError, ProgrammingError
from flask_migrate import upgrade as database_upgrade
from importlib.metadata import entry_points
from flaschengeist import version as flaschengeist_version
from .. import logger
from ..database import db
from ..utils.hook import Hook
from ..plugins import Plugin, AuthPlugin
from ..models import Notification
__required_plugins = ["users", "roles", "scheduler", "auth"]
def get_authentication_provider():
return [
current_app.config["FG_PLUGINS"][plugin.name]
for plugin in get_loaded_plugins().values()
if isinstance(plugin, AuthPlugin)
]
def get_loaded_plugins(plugin_name: str = None):
"""Get loaded plugin(s)"""
plugins = current_app.config["FG_PLUGINS"]
if plugin_name is not None:
plugins = [plugins[plugin_name]]
return {name: db.session.merge(plugins[name], load=False) for name in plugins}
def get_installed_plugins() -> list[Plugin]:
"""Get all installed plugins"""
return Plugin.query.all()
def get_enabled_plugins() -> list[Plugin]:
"""Get all installed and enabled plugins"""
try:
enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all()
except (OperationalError, ProgrammingError) as e:
logger.error("Could not connect to database or database not initialized! No plugins enabled!")
logger.debug("Can not query enabled plugins", exc_info=True)
# Fake load required plugins so the database can at least be installed
enabled_plugins = [
entry_points(group="flaschengeist.plugins", name=name)[0].load()(
name=name, enabled=True, installed_version=flaschengeist_version
)
for name in __required_plugins
]
return enabled_plugins
def notify(plugin_id: int, user, text: str, data=None):
"""Create a new notification for an user
Args:
plugin_id: ID of the plugin
user: `flaschengeist.models.user.User` to notify
text: Visibile notification text
data: Optional data passed to the notificaton
Returns:
ID of the created `flaschengeist.models.notification.Notification`
Hint: use the data for frontend actions.
"""
if not user.deleted:
n = Notification(text=text, data=data, plugin_id_=plugin_id, user_=user)
db.session.add(n)
db.session.commit()
return n.id
def get_notifications(plugin_id) -> List[Notification]:
"""Get all notifications for a plugin
Args:
plugin_id: ID of the plugin
Returns:
List of `flaschengeist.models.notification.Notification`
"""
return db.session.execute(db.select(Notification).where(Notification.plugin_id_ == plugin_id)).scalars().all()
@Hook("plugins.installed")
def install_plugin(plugin_name: str):
logger.debug(f"Installing plugin {plugin_name}")
entry_point = entry_points(group="flaschengeist.plugins", name=plugin_name)
if not entry_point:
raise NotFound
cls = entry_point[0].load()
plugin: Plugin = cls.query.filter(Plugin.name == plugin_name).one_or_none()
if plugin is None:
plugin = cls(name=plugin_name, installed_version=entry_point[0].dist.version)
db.session.add(plugin)
db.session.commit()
# Custom installation steps
plugin.install()
# Check migrations
directory = entry_point[0].dist.locate_file("")
logger.debug(f"Checking for migrations in {directory}")
for loc in entry_point[0].module.split(".") + ["migrations"]:
directory /= loc
logger.debug(f"Checking for migrations with loc in {directory}")
if directory.exists():
logger.debug(f"Found migrations in {directory}")
database_upgrade(revision=f"{plugin_name}@head")
db.session.commit()
return plugin
@Hook("plugin.uninstalled")
def uninstall_plugin(plugin_id: Union[str, int, Plugin]):
plugin = disable_plugin(plugin_id)
logger.debug(f"Uninstall plugin {plugin.name}")
plugin.uninstall()
db.session.delete(plugin)
db.session.commit()
@Hook("plugins.enabled")
def enable_plugin(plugin_id: Union[str, int]) -> Plugin:
logger.debug(f"Enabling plugin {plugin_id}")
plugin = Plugin.query
if isinstance(plugin_id, str):
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
elif isinstance(plugin_id, int):
plugin = plugin.get(plugin_id)
else:
raise TypeError
if plugin is None:
raise NotFound
plugin.enabled = True
db.session.commit()
plugin = plugin.entry_point.load().query.get(plugin.id)
current_app.config["FG_PLUGINS"][plugin.name] = plugin
return plugin
@Hook("plugins.disabled")
def disable_plugin(plugin_id: Union[str, int, Plugin]):
logger.debug(f"Disabling plugin {plugin_id}")
plugin: Plugin = Plugin.query
if isinstance(plugin_id, str):
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
elif isinstance(plugin_id, int):
plugin = plugin.get(plugin_id)
elif isinstance(plugin_id, Plugin):
plugin = plugin_id
else:
raise TypeError
if plugin is None:
raise NotFound
if plugin.name in __required_plugins:
raise BadRequest
plugin.enabled = False
db.session.commit()
if plugin.name in current_app.config["FG_PLUGINS"].keys():
del current_app.config["FG_PLUGINS"][plugin.name]
return plugin

View File

@ -0,0 +1,85 @@
from typing import Union
from sqlalchemy.exc import IntegrityError
from werkzeug.exceptions import BadRequest, Conflict, NotFound
from .. import logger
from ..models import Role, Permission
from ..database import db, case_sensitive
from ..utils.hook import Hook
def get_all():
return Role.query.all()
def get(role_name: Union[int, str]) -> Role:
"""Get role by ID or name
Args:
role_name: Name or ID of the role
Returns:
Matching role
Raises:
NotFound
"""
if type(role_name) is int:
role = Role.query.get(role_name)
else:
role = Role.query.filter(Role.name == role_name).one_or_none()
if not role:
raise NotFound("no such role")
return role
def get_permissions():
return Permission.query.all()
@Hook
def update_role(role, new_name):
if new_name is None or not isinstance(new_name, str):
raise BadRequest("Invalid new name")
else:
if role.name == new_name or db.session.query(db.exists().where(Role.name == case_sensitive(new_name))).scalar():
raise BadRequest("Name already used")
role.name = new_name
db.session.commit()
def set_permissions(role, permissions):
perms = Permission.query.filter(Permission.name.in_(permissions)).all()
if len(perms) < len(permissions):
raise BadRequest("Invalid permission name")
role.permissions = list(perms)
db.session.commit()
def create_permissions(permissions):
for permission in permissions:
if Permission.query.filter(Permission.name == permission).count() > 0:
continue
p = Permission(name=permission)
db.session.add(p)
db.session.commit()
def create_role(name: str, permissions=[]):
logger.debug(f"Create new role with name: {name}")
try:
role = Role(name=name)
db.session.add(role)
set_permissions(role, permissions)
db.session.commit()
logger.debug(f"Created role: {role.serialize()}")
except IntegrityError:
raise BadRequest("role already exists")
return role
def delete(role):
role.permissions.clear()
try:
db.session.delete(role)
db.session.commit()
except IntegrityError:
logger.debug("IntegrityError: Role might still be in use", exc_info=True)
raise Conflict("Role still in use")

View File

@ -0,0 +1,144 @@
import secrets
from datetime import datetime, timezone
from werkzeug.exceptions import Forbidden, Unauthorized
from ua_parser import user_agent_parser
from .. import logger
from ..models import Session
from ..database import db
lifetime = 1800
def get_user_agent(request_headers):
return user_agent_parser.Parse(request_headers.get("User-Agent", "") if request_headers else "")
def validate_token(token, request_headers, permission):
"""Verify session
Verify a Session and Roles so if the User has permission or not.
Retrieves the access token if valid else retrieves False
Args:
token: Token to verify.
request_headers: Headers to validate user agent of browser
permission: Permission needed to access restricted routes
Returns:
A Session for this given Token
Raises:
Unauthorized: If token is invalid or expired
Forbidden: If permission is insufficient
"""
logger.debug("check token {{ {} }} is valid".format(token))
session = Session.query.filter_by(token=token).one_or_none()
if session:
logger.debug("token found, check if expired or invalid user agent differs")
user_agent = get_user_agent(request_headers)
platform = user_agent["os"]["family"]
browser = user_agent["user_agent"]["family"]
if session.expires >= datetime.now(timezone.utc) and (
session.browser == browser and session.platform == platform
):
if not permission or session.user_.has_permission(permission):
session.refresh()
db.session.commit()
return session
else:
raise Forbidden
else:
logger.debug("access token is out of date or invalid client used")
delete_session(session)
logger.debug("no valid access token with token: {{ {} }} and permission: {{ {} }}".format(token, permission))
raise Unauthorized
def create(user, request_headers=None) -> Session:
"""Create a Session
Args:
user: For which User is to create a Session
request_headers: Headers to validate user agent of browser
Returns:
Session: A created Token for User
"""
logger.debug("create access token")
token_str = secrets.token_hex(16)
user_agent = get_user_agent(request_headers)
logger.debug(f"platform: {user_agent['os']['family']}, browser: {user_agent['user_agent']['family']}")
session = Session(
token=token_str,
user_=user,
lifetime=lifetime,
platform=user_agent["os"]["family"],
browser=user_agent["user_agent"]["family"],
)
session.refresh()
db.session.add(session)
db.session.commit()
logger.debug("access token is {{ {} }}".format(session.token))
return session
def get_session(token, owner=None):
"""Retrieves Session from token string
Args:
token (str): Token string
owner (User, optional): User owning the token
Raises:
Forbidden: Raised if owner is set but does not match
Returns:
Session: Token object identified by given token string
"""
session = Session.query.filter(Session.token == token).one_or_none()
if session and (owner and owner != session.user_):
raise Forbidden
return session
def get_users_sessions(user):
return Session.query.filter(Session.user_ == user)
def delete_sessions(user):
"""Deletes all active sessions of a user
Args:
user (User): User to delete all sessions for
"""
Session.query.filter(Session.user_.id_ == user.id_).delete()
db.session.commit()
def delete_session(token: Session):
"""Deletes given Session
Args:
token (Session): Token to delete
"""
db.session.delete(token)
db.session.commit()
def update_session(session):
session.refresh()
db.session.commit()
def set_lifetime(session, lifetime):
session.lifetime = lifetime
update_session(session)
def clear_expired():
"""Remove expired tokens from database"""
logger.debug("Clear expired Sessions")
deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete()
logger.debug("{} sessions have been removed".format(deleted))
db.session.commit()

View File

@ -0,0 +1,327 @@
import re
import secrets
import hashlib
from io import BytesIO
from typing import Optional, Union
from flask import make_response
from flask.json import provider
from sqlalchemy import exc
from sqlalchemy_utils import merge_references
from datetime import datetime, timedelta, timezone, date
from flask.helpers import send_file
from werkzeug.exceptions import NotFound, BadRequest, Forbidden
from .. import logger
from ..config import config
from ..database import db
from ..models import Notification, User, Role
from ..models.user import _PasswordReset
from ..utils.hook import Hook
from ..utils.datetime import from_iso_format
from ..controller import (
imageController,
messageController,
pluginController,
sessionController,
)
from ..plugins import AuthPlugin
def __active_users():
"""Return query for not deleted users"""
return User.query.filter(User.deleted == False)
def _generate_password_reset(user):
"""Generate a password reset link for the user"""
reset = _PasswordReset.query.get(user.id_)
if not reset:
reset = _PasswordReset(_user_id=user.id_)
db.session.add(reset)
expires = datetime.now(tz=timezone.utc)
if not reset.expires or reset.expires < expires:
expires = expires + timedelta(hours=12)
reset.expires = expires
reset.token = secrets.token_urlsafe(24)
db.session.commit()
return reset
def get_provider(userid: str) -> AuthPlugin:
return [p for p in pluginController.get_authentication_provider() if p.user_exists(userid)][0]
@Hook
def update_user(user: User, backend: Optional[AuthPlugin] = None):
"""Update user data from backend
This is seperate function to provide a hook"""
if not backend:
backend = get_provider(user.userid)
backend.update_user(user)
if not user.display_name:
user.display_name = "{} {}.".format(user.firstname, user.lastname[0])
db.session.commit()
def login_user(username, password):
logger.info("login user {{ {} }}".format(username))
for provider in pluginController.get_authentication_provider():
uid = provider.login(username, password)
if isinstance(uid, str):
user = get_user(uid)
if not user:
logger.debug("User not found in Database.")
user = User(userid=uid)
db.session.add(user)
update_user(user, provider)
return user
return None
def request_reset(user: User):
logger.debug(f"New password reset request for {user.userid}")
reset = _generate_password_reset(user)
subject = str(config["MESSAGES"]["password_subject"]).format(name=user.display_name, username=user.userid)
text = str(config["MESSAGES"]["password_text"]).format(
name=user.display_name,
username=user.userid,
link=f'https://{config["FLASCHENGEIST"]["domain"]}/reset?token={reset.token}',
)
messageController.send_message(messageController.Message(user, text, subject))
def reset_password(token: str, password: str):
if len(token) != 32:
raise BadRequest
reset = _PasswordReset.query.filter(_PasswordReset.token == token).one_or_none()
logger.debug(f"Token is {'valid' if reset else 'invalid'}")
if not reset or reset.expires < datetime.now(tz=timezone.utc):
raise Forbidden
modify_user(reset.user, None, password)
sessionController.delete_sessions(reset.user)
db.session.delete(reset)
db.session.commit()
def set_roles(user: User, roles: list[str], create=False):
"""Set roles of user
Args:
user: User to set roles of
roles: List of role names
create: If set to true, create not existing roles
Raises:
BadRequest if invalid arguments given or not all roles found while *create* is set to false
"""
from .roleController import create_role
if not isinstance(roles, list) and any([not isinstance(r, str) for r in roles]):
raise BadRequest("Invalid role name")
fetched = Role.query.filter(Role.name.in_(roles)).all()
if len(fetched) < len(roles):
if not create:
raise BadRequest("Invalid role name, role not found")
# Create all new roles
fetched += [create_role(role_name) for role_name in roles if not any([role_name == r.name for r in fetched])]
user.roles_ = fetched
def modify_user(user: User, password: str, new_password: str = None):
"""Modify given user on the backend
Args:
user: User object to sync with backend
password: Current password (most backends are needing this)
new_password (optional): New password, if password should be changed
Raises:
NotImplemented: If backend is not capable of this operation
BadRequest: Password is wrong or other logic issues
"""
provider = get_provider(user.userid)
provider.modify_user(user, password, new_password)
if new_password:
logger.debug(f"Password changed for user {user.userid}")
subject = str(config["MESSAGES"]["password_changed_subject"]).format(
name=user.display_name, username=user.userid
)
text = str(config["MESSAGES"]["password_changed_text"]).format(
name=user.display_name,
username=user.userid,
)
messageController.send_message(messageController.Message(user, text, subject))
def get_users(deleted=False):
query = __active_users() if not deleted else User.query
return query.all()
def get_user_by_role(role: Role):
return User.query.join(User.roles_).filter_by(role_id=role.id).all()
def get_user(uid, deleted=False) -> User:
"""Get an user by userid from database
Args:
uid: Userid to search for
deleted: Set to true to also search deleted users
Returns:
User fround
Raises:
NotFound if not found"""
user = (__active_users() if not deleted else User.query).filter(User.userid == uid).one_or_none()
if not user:
raise NotFound
return user
@Hook
def delete_user(user: User):
"""Delete given user"""
# First let the backend delete the user, as this might fail
provider = get_provider(user.userid)
provider.delete_user(user)
# Clear all easy relationships
user.avatar_ = None
user._attributes.clear()
user.roles_.clear()
user.sessions_.clear()
user.reset_requests_.clear()
# Now move all other references to the DELETED_USER
try:
deleted_user = get_user("__deleted_user__", True)
except NotFound:
deleted_user = User(
userid="__deleted_user__",
firstname="USER",
lastname="DELETED",
display_name="DELETED USER",
deleted=True,
)
db.session.add(user)
db.session.flush()
merge_references(user, deleted_user)
db.session.commit()
# Now try to delete the user for real
try:
db.session.delete(user)
db.session.commit()
except exc.IntegrityError:
logger.error(
"Delete of user failed, there might be ForeignKey contraits from disabled plugins",
exec_info=True,
)
# Remove at least all personal data
user.userid = f"__deleted_user__{user.id_}"
user.display_name = "DELETED USER"
user.firstname = ""
user.lastname = ""
user.deleted = True
user.birthday = None
user.mail = None
db.session.commit()
def register(data, passwd=None):
"""Register a new user
Args:
data: dictionary containing valid user properties
passwd: optional a password, default: 16byte random
"""
allowed_keys = User().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
roles = values.pop("roles", [])
if "birthday" in data:
if isinstance(data["birthday"], date):
values["birthday"] = data["birthday"]
else:
values["birthday"] = from_iso_format(data["birthday"]).date()
if "mail" in data and not re.match(r"[^@]+@[^@]+\.[^@]+", data["mail"]):
raise BadRequest("Invalid mail given")
user = User(**values)
set_roles(user, roles)
password = passwd if passwd else secrets.token_urlsafe(16)
try:
provider = [p for p in pluginController.get_authentication_provider() if p.can_register()][0]
provider.create_user(user, password)
db.session.add(user)
db.session.commit()
except IndexError as e:
logger.error("No authentication backend, allowing registering new users, found.")
raise e
except exc.IntegrityError:
raise BadRequest("userid already in use")
if user.mail:
reset = _generate_password_reset(user)
subject = str(config["MESSAGES"]["welcome_subject"]).format(name=user.display_name, username=user.userid)
text = str(config["MESSAGES"]["welcome_text"]).format(
name=user.display_name,
username=user.userid,
password_link=f'https://{config["FLASCHENGEIST"]["domain"]}/reset?token={reset.token}',
)
messageController.send_message(messageController.Message(user, text, subject))
provider.update_user(user)
return user
def get_last_modified(user: User):
"""Get the last modification date of the user"""
return get_provider(user.userid).get_last_modified(user)
def load_avatar(user: User, etag: Union[str, None] = None):
if user.avatar_ is not None:
return imageController.send_image(image=user.avatar_)
else:
provider = get_provider(user.userid)
avatar = provider.get_avatar(user)
new_etag = hashlib.md5(avatar.binary).hexdigest()
if new_etag == etag:
return make_response("", 304)
if len(avatar.binary) > 0:
return send_file(BytesIO(avatar.binary), avatar.mimetype, etag=new_etag)
raise NotFound
def save_avatar(user, file):
get_provider(user.userid).set_avatar(user, file)
db.session.commit()
def delete_avatar(user):
get_provider(user.userid).delete_avatar(user)
db.session.commit()
def persist(user=None):
if user:
db.session.add(user)
db.session.commit()
def get_notifications(user, start=None):
query = Notification.query.filter(Notification.user_id_ == user.id_)
if start is not None:
query = query.filter(Notification.time > start)
return query.order_by(Notification.time).all()
def delete_notification(nid, user):
Notification.query.filter(Notification.id == nid).filter(Notification.user_ == user).delete()
db.session.commit()

View File

@ -0,0 +1,75 @@
import os
from flask_migrate import Migrate, Config
from flask_sqlalchemy import SQLAlchemy
from importlib.metadata import EntryPoint, entry_points, distribution
from sqlalchemy import MetaData
from flaschengeist.alembic import alembic_script_path
from flaschengeist import logger
# from flaschengeist.controller import pluginController
# https://alembic.sqlalchemy.org/en/latest/naming.html
metadata = MetaData(
naming_convention={
"pk": "pk_%(table_name)s",
"ix": "ix_%(table_name)s_%(column_0_name)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
}
)
db = SQLAlchemy(metadata=metadata, session_options={"expire_on_commit": False})
migrate = Migrate()
@migrate.configure
def configure_alembic(config: Config):
"""Alembic configuration hook
Inject all migrations paths into the ``version_locations`` config option.
This includes even disabled plugins, as simply disabling a plugin without
uninstall can break the alembic version management.
"""
# Set main script location
config.set_main_option("script_location", alembic_script_path)
# Set Flaschengeist's migrations
migrations = [config.get_main_option("script_location") + "/migrations"]
# Gather all migration paths
for entry_point in entry_points(group="flaschengeist.plugins"):
try:
directory = entry_point.dist.locate_file("")
for loc in entry_point.module.split(".") + ["migrations"]:
directory /= loc
if directory.exists():
logger.debug(f"Adding migration version path {directory}")
migrations.append(str(directory.resolve()))
except:
logger.warning(f"Could not load migrations of plugin {entry_point.name} for database migration.")
logger.debug("Plugin loading failed", exc_info=True)
# write back seperator (we changed it if neither seperator nor locations were specified)
config.set_main_option("version_path_separator", os.pathsep)
config.set_main_option("version_locations", os.pathsep.join(set(migrations)))
return config
def case_sensitive(s):
"""
Compare string as case sensitive on the database
Args:
s: string to compare
Example:
User.query.filter(User.name == case_sensitive(some_string))
"""
if db.session.bind.dialect.name == "mysql":
from sqlalchemy import func
return func.binary(s)
return s

View File

@ -0,0 +1,97 @@
from importlib import import_module
import datetime
from sqlalchemy import BigInteger, util
from sqlalchemy.dialects import mysql, sqlite
from sqlalchemy.types import DateTime, TypeDecorator
class ModelSerializeMixin:
"""Mixin class used for models to serialize them automatically
Ignores private and protected members as well as members marked as not to publish (name ends with _)
"""
def __is_optional(self, param):
import typing
module = import_module("flaschengeist.models").__dict__
try:
hint = typing.get_type_hints(self.__class__, globalns=module, locals=locals())[param]
if (
typing.get_origin(hint) is typing.Union
and len(typing.get_args(hint)) == 2
and typing.get_args(hint)[1] is type(None)
):
return getattr(self, param) is None
except:
pass
def serialize(self):
"""Serialize class to dict
Returns:
Dict of all not private or protected annotated member variables.
"""
d = {
param: getattr(self, param)
for param in self.__class__.__annotations__
if not param.startswith("_") and not param.endswith("_") and not self.__is_optional(param)
}
if len(d) == 1:
_, value = d.popitem()
return value
return d
def __str__(self) -> str:
return self.serialize().__str__()
class Serial(TypeDecorator):
"""Same as MariaDB Serial used for IDs"""
cache_ok = True
impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql").with_variant(sqlite.INTEGER(), "sqlite")
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)
class UtcDateTime(TypeDecorator):
"""Almost equivalent to `sqlalchemy.types.DateTime` with
``timezone=True`` option, but it differs from that by:
- Never silently take naive :class:`datetime.datetime`, instead it
always raise :exc:`ValueError` unless time zone aware value.
- :class:`datetime.datetime` value's :attr:`datetime.datetime.tzinfo`
is always converted to UTC.
- Unlike SQLAlchemy's built-in :class:`sqlalchemy.types.DateTime`,
it never return naive :class:`datetime.datetime`, but time zone
aware value, even with SQLite or MySQL.
"""
cache_ok = True
impl = DateTime(timezone=True)
@staticmethod
def current_utc():
return datetime.datetime.now(tz=datetime.timezone.utc)
def process_bind_param(self, value, dialect):
if value is not None:
if not isinstance(value, datetime.datetime):
raise TypeError("expected datetime.datetime, not " + repr(value))
elif value.tzinfo is None:
raise ValueError("naive datetime is disallowed")
return value.astimezone(datetime.timezone.utc)
def process_result_value(self, value, dialect):
if value is not None:
if value.tzinfo is not None:
value = value.astimezone(datetime.timezone.utc)
value = value.replace(tzinfo=datetime.timezone.utc)
return value
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html?highlight=custom%20column#affecting-the-rendering-of-types-themselves
def __repr__(self) -> str:
return util.generic_repr(self)

View File

@ -0,0 +1,127 @@
# This is the example configuation and the default configuration
# All default values are uncommented, so set enabled ones to False to disabled them
[FLASCHENGEIST]
# Select authentication provider (builtin: auth_plain, auth_ldap)
auth = "auth_plain"
# Enable if you run flaschengeist behind a proxy, e.g. nginx + gunicorn
#proxy = false
# Set root path, prefixes all routes
root = "/api"
# Set secret key
secret_key = "V3ryS3cr3t"
# Domain used by frontend
[DATABASE]
# engine = "mysql" (default)
host = "localhost"
user = "flaschengeist"
password = "flaschengeist"
database = "flaschengeist"
[LOGGING]
# You can override all settings from the logging.toml here
# Default: Logging to WSGI stream (commonly stderr)
# Logging level, possible: TRACE DEBUG INFO WARNING ERROR CRITICAL
# On TRACE level additionally every request will get logged
level = "DEBUG"
# If you want the logger to log to a file, you could use:
#[LOGGING.handlers.file]
# class = "logging.handlers.WatchedFileHandler"
# level = "WARNING"
# formatter = "extended"
# encoding = "utf8"
# filename = "flaschengeist.log"
[FILES]
# Path for file / image uploads
data_path = "./data"
# Thumbnail size
thumbnail_size = [192, 192]
# Accepted mimetypes
allowed_mimetypes = [
"image/avif",
"image/jpeg",
"image/png",
"image/webp"
]
[scheduler]
# Possible values are: "passive_web" (default), "active_web" and "system"
# See documentation
# cron = "passive_web"
[auth_ldap]
# Full documentation https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/plugins_auth_ldap
# host = "localhost"
# port = 389
# base_dn = "dc=example,dc=com"
# root_dn = "cn=Manager,dc=example,dc=com"
# root_secret = "SuperS3cret"
# Uncomment to use secured LDAP (ldaps)
# use_ssl = true
[MESSAGES]
welcome_subject = "Welcome to Flaschengeist {name}"
welcome_text = '''
Hello {name}!
Welcome to Flaschengeist!
Your username is {username}, please set your password:
{password_link}
(If the link expires, please just use the Forgot-Password-function).
Have fun :)
'''
password_subject = "Flaschengeist - Password reset"
password_text = '''
Hello {name}!
There was a password reset request for username: {username}
To change your password, click on this link:
{link}
'''
password_changed_subject = "Flaschengeist - Password changed"
password_changed_text = '''
Hello {name}!
Your password was changed for username: {username}
If this was not you, please contact the support.
'''
##################
# PLUGINS #
##################
[users]
# always enabled
## allowed values: false, "managed", "public"
## false: Disable registration
## "managed": only users with matching permission are allowed to register new users
## "public": Also unauthenticated users can register an account
# registration = False
############################
# Configuration of plugins #
############################
[mail]
# enabled = true
# SERVER =
# PORT =
# USER =
# PASSWORD =
# MAIL =
# SSL or STARTLS
# CRYPT = SSL
[balance]
# enabled = true
# Enable a default limit, will be set if no other limit is set
# limit = -10.00
# Uncomment to allow active services to debit other users (requires events plugin)
# allow_service_debit = true

View File

@ -0,0 +1,26 @@
# This is the default flaschengeist logger configuration
# If you want to customize it, use the flaschengeist.toml
version = 1
disable_existing_loggers = false
[formatters]
[formatters.simple]
format = "[%(asctime)s] %(levelname)s - %(message)s"
[formatters.extended]
format = "[%(asctime)s] %(levelname)s %(filename)s - %(funcName)s - %(lineno)d - %(threadName)s - %(name)s — %(message)s"
[handlers]
[handlers.wsgi]
stream = "ext://flask.logging.wsgi_errors_stream"
class = "logging.StreamHandler"
formatter = "simple"
level = "DEBUG"
[loggers]
[loggers.werkzeug]
level = "WARNING"
[root]
level = "WARNING"
handlers = ["wsgi"]

View File

@ -0,0 +1,5 @@
from .session import *
from .user import *
from .plugin import *
from .notification import *
from .image import *

View File

@ -0,0 +1,32 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from sqlalchemy import event
from pathlib import Path
from ..database import db
from ..database.types import ModelSerializeMixin, Serial
class Image(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "image"
id: int = db.Column(Serial, primary_key=True)
filename_: str = db.Column("filename", db.String(255), nullable=False)
mimetype_: str = db.Column("mimetype", db.String(127), nullable=False)
thumbnail_: str = db.Column("thumbnail", db.String(255))
path_: str = db.Column("path", db.String(255))
def open(self):
return open(self.path_, "rb")
@event.listens_for(Image, "before_delete")
def clear_file(mapper, connection, target: Image):
if target.path_:
p = Path(target.path_)
if p.exists():
p.unlink()
if target.thumbnail_:
p = Path(target.thumbnail_)
if p.exists():
p.unlink()

View File

@ -0,0 +1,28 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from datetime import datetime
from typing import Any
from ..database import db
from ..database.types import Serial, UtcDateTime, ModelSerializeMixin
class Notification(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "notification"
id: int = db.Column("id", Serial, primary_key=True)
text: str = db.Column(db.Text)
data: Any = db.Column(db.PickleType(protocol=4))
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
user_id_: int = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"), nullable=False)
user_: User = db.relationship("User")
plugin_: Plugin = db.relationship(
"Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan")
)
plugin: str
@property
def plugin(self) -> str:
return self.plugin_.name

View File

@ -0,0 +1,74 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Any, List, Dict
from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db
from ..database.types import Serial
class PluginSetting(db.Model):
__allow_unmapped__ = True
__tablename__ = "plugin_setting"
id = db.Column("id", Serial, primary_key=True)
plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
name: str = db.Column(db.String(127), nullable=False)
value: Any = db.Column(db.PickleType(protocol=4))
class BasePlugin(db.Model):
__allow_unmapped__ = True
__tablename__ = "plugin"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(127), nullable=False)
"""Name of the plugin, loaded from distribution"""
installed_version: str = db.Column("version", db.String(30), nullable=False)
"""The latest installed version"""
enabled: bool = db.Column(db.Boolean, default=False)
"""Enabled state of the plugin"""
permissions: List["Permission"] = db.relationship(
"Permission", cascade="all, delete, delete-orphan", back_populates="plugin_", lazy="select"
)
"""Optional list of custom permissions used by the plugin
A good style is to name the permissions with a prefix related to the plugin name,
to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*.
"""
__settings: Dict[str, "PluginSetting"] = db.relationship(
"PluginSetting",
collection_class=attribute_mapped_collection("name"),
cascade="all, delete, delete-orphan",
lazy="subquery",
)
def get_setting(self, name: str, **kwargs):
"""Get plugin setting
Args:
name: string identifying the setting
default: Default value
Returns:
Value stored in database (native python)
Raises:
`KeyError` if no such setting exists in the database
"""
try:
return self.__settings[name].value
except KeyError as e:
if "default" in kwargs:
return kwargs["default"]
raise e
def set_setting(self, name: str, value):
"""Save setting in database
Args:
name: String identifying the setting
value: Value to be stored
"""
if value is None and name in self.__settings.keys():
del self.__settings[name]
else:
setting = self.__settings.setdefault(name, PluginSetting(plugin_id=self.id, name=name, value=None))
setting.value = value

View File

@ -0,0 +1,49 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from datetime import datetime, timedelta, timezone
from secrets import compare_digest
from .. import logger
from ..database import db
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
class Session(db.Model, ModelSerializeMixin):
"""Model for a Session
Args:
expires: Is a Datetime from current Time.
user: Is an User.
token: String to verify access later.
"""
__allow_unmapped__ = True
__tablename__ = "session"
expires: datetime = db.Column(UtcDateTime)
token: str = db.Column(db.String(32), unique=True)
lifetime: int = db.Column(db.Integer)
browser: str = db.Column(db.String(127))
platform: str = db.Column(db.String(64))
userid: str = ""
_id = db.Column("id", Serial, primary_key=True)
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"))
user_: User = db.relationship("User", back_populates="sessions_")
@property
def userid(self):
return self.user_.userid
def refresh(self):
"""Update the Timestamp
Update the Timestamp to the current Time.
"""
logger.debug("update timestamp from session with token {{ {} }}".format(self.token))
self.expires = datetime.now(timezone.utc) + timedelta(seconds=self.lifetime)
def __eq__(self, token):
if isinstance(token, str):
return compare_digest(self.token, token)
else:
return super(Session, self).__eq__(token)

View File

@ -0,0 +1,138 @@
from __future__ import (
annotations,
) # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Optional, Union, List
from datetime import date, datetime
from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db
from ..database.types import ModelSerializeMixin, UtcDateTime, Serial
association_table = db.Table(
"user_x_role",
db.Column("user_id", Serial, db.ForeignKey("user.id")),
db.Column("role_id", Serial, db.ForeignKey("role.id")),
)
role_permission_association_table = db.Table(
"role_x_permission",
db.Column("role_id", Serial, db.ForeignKey("role.id")),
db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
)
class Permission(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "permission"
name: str = db.Column(db.String(30), unique=True)
id_ = db.Column("id", Serial, primary_key=True)
plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
plugin_ = db.relationship("Plugin", lazy="subquery", back_populates="permissions", enable_typechecks=False)
class Role(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "role"
id: int = db.Column(Serial, primary_key=True)
name: str = db.Column(db.String(30), unique=True)
permissions: List[Permission] = db.relationship("Permission", secondary=role_permission_association_table)
class User(db.Model, ModelSerializeMixin):
"""Database Object for User
Table for all saved User
Attributes:
id: Id in Database as Primary Key.
userid: User ID used by authentication provider
display_name: Name to show
firstname: Firstname of the User
lastname: Lastname of the User
mail: mail address of the User
birthday: Birthday of the user
"""
__allow_unmapped__ = True
__tablename__ = "user"
userid: str = db.Column(db.String(30), unique=True, nullable=False)
display_name: str = db.Column(db.String(30))
firstname: str = db.Column(db.String(50), nullable=False)
lastname: str = db.Column(db.String(50), nullable=False)
deleted: bool = db.Column(db.Boolean(), default=False)
birthday: Optional[date] = db.Column(db.Date)
mail: str = db.Column(db.String(60))
roles: List[str] = []
permissions: Optional[list[str]] = []
# Protected stuff for backend use only
id_ = db.Column("id", Serial, primary_key=True)
roles_: List[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge")
sessions_: List[Session] = db.relationship("Session", back_populates="user_", cascade="all, delete, delete-orphan")
avatar_: Optional[Image] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True)
reset_requests_: List["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan")
# Private stuff for internal use
_avatar_id = db.Column("avatar", Serial, db.ForeignKey("image.id"))
_attributes = db.relationship(
"_UserAttribute",
collection_class=attribute_mapped_collection("name"),
cascade="all, delete, delete-orphan",
)
@property
def roles(self) -> List[str]:
return [role.name for role in self.roles_]
def set_attribute(self, name, value):
if name in self._attributes:
self._attributes[name].value = value
else:
self._attributes[name] = _UserAttribute(name=name, value=value)
def has_attribute(self, name):
return name in self._attributes
def get_attribute(self, name, default=None):
if name in self._attributes:
return self._attributes[name].value
return default
def delete_attribute(self, name):
if name in self._attributes:
self._attributes.pop(name)
def get_permissions(self):
return ["user"] + [permission.name for role in self.roles_ for permission in role.permissions]
def has_permission(self, permission):
return permission in self.get_permissions()
class _UserAttribute(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "user_attribute"
id = db.Column("id", Serial, primary_key=True)
user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
name: str = db.Column(db.String(30))
value: any = db.Column(db.PickleType(protocol=4))
class _PasswordReset(db.Model):
"""Table containing password reset requests"""
__allow_unmapped__ = True
__tablename__ = "password_reset"
_user_id: User = db.Column("user", Serial, db.ForeignKey("user.id"), primary_key=True)
user: User = db.relationship("User", back_populates="reset_requests_", foreign_keys=[_user_id])
token: str = db.Column(db.String(32))
expires: datetime = db.Column(UtcDateTime)
class _Avatar:
"""Wrapper class for avatar binaries"""
mimetype = ""
binary = bytearray()

View File

@ -0,0 +1,304 @@
"""Flaschengeist Plugins
.. include:: docs/plugin_development.md
"""
from typing import Union, List
from importlib.metadata import entry_points
from werkzeug.exceptions import NotFound
from werkzeug.datastructures import FileStorage
from flaschengeist.models.plugin import BasePlugin
from flaschengeist.models.user import _Avatar, Permission
from flaschengeist.utils.hook import HookBefore, HookAfter
__all__ = [
"plugins_installed",
"plugins_loaded",
"before_delete_user",
"before_role_updated",
"before_update_user",
"after_role_updated",
"Plugin",
"AuthPlugin",
]
# Documentation hacks, see https://github.com/mitmproxy/pdoc/issues/320
plugins_installed = HookAfter("plugins.installed")
plugins_installed.__doc__ = """Hook decorator for when all plugins are installed
Possible use case would be to populate the database with some presets.
"""
plugins_loaded = HookAfter("plugins.loaded")
plugins_loaded.__doc__ = """Hook decorator for when all plugins are loaded
Possible use case would be to check if a specific other plugin is loaded and change own behavior
Passed args:
- *app:* Current flask app instance (args)
"""
before_role_updated = HookBefore("update_role")
before_role_updated.__doc__ = """Hook decorator for when roles are modified
Passed args:
- *role:* `flaschengeist.models.user.Role` to modify
- *new_name:* New name if the name was changed (*None* if delete)
"""
after_role_updated = HookAfter("update_role")
after_role_updated.__doc__ = """Hook decorator for when roles are modified
Passed args:
- *role:* modified `flaschengeist.models.user.Role`
- *new_name:* New name if the name was changed (*None* if deleted)
"""
before_update_user = HookBefore("update_user")
before_update_user.__doc__ = """Hook decorator, when ever an user update is done, this is called before.
Passed args:
- *user:* `flaschengeist.models.user.User` object
"""
before_delete_user = HookBefore("delete_user")
before_delete_user.__doc__ = """Hook decorator,this is called before an user gets deleted.
Passed args:
- *user:* `flaschengeist.models.user.User` object
"""
class Plugin(BasePlugin):
"""Base class for all Plugins
All plugins must derived from this class.
Optional:
- *blueprint*: `flask.Blueprint` providing your routes
- *permissions*: List of your custom permissions
- *models*: Your models, used for API export
"""
blueprint = None
"""Optional `flask.blueprint` if the plugin uses custom routes"""
models = None
"""Optional module containing the SQLAlchemy models used by the plugin"""
@property
def version(self) -> str:
"""Version of the plugin, loaded from Distribution"""
return self.dist.version
@property
def dist(self):
"""Distribution of this plugin"""
return self.entry_point.dist
@property
def entry_point(self):
ep = tuple(entry_points(group="flaschengeist.plugins", name=self.name))
return ep[0]
def load(self):
"""__init__ like function that is called when the plugin is initially loaded"""
pass
def install(self):
"""Installation routine
Also called when updating the plugin, compare `version` and `installed_version`.
Is always called with Flask application context,
it is called after the plugin permissions are installed.
"""
pass
def uninstall(self):
"""Uninstall routine
If the plugin has custom database tables, make sure to remove them.
This can be either done by downgrading the plugin *head* to the *base*.
Or use custom migrations for the uninstall and *stamp* some version.
Is always called with Flask application context.
"""
pass
def notify(self, user, text: str, data=None):
"""Create a new notification for an user
Args:
user: `flaschengeist.models.user.User` to notify
text: Visibile notification text
data: Optional data passed to the notificaton
Returns:
ID of the created `flaschengeist.models.notification.Notification`
Hint: use the data for frontend actions.
"""
from ..controller import pluginController
return pluginController.notify(self.id, user, text, data)
@property
def notifications(self) -> List["Notification"]:
"""Get all notifications for this plugin
Returns:
List of `flaschengeist.models.notification.Notification`
"""
from ..controller import pluginController
return pluginController.get_notifications(self.id)
def serialize(self):
"""Serialize a plugin into a dict
Returns:
Dict containing version and permissions of the plugin
"""
return {"version": self.version, "permissions": self.permissions}
def install_permissions(self, permissions: list[str]):
"""Helper for installing a list of strings as permissions
Args:
permissions: List of permissions to install
"""
cur_perm = set(x for x in self.permissions or [])
all_perm = set(permissions)
new_perms = all_perm - cur_perm
_perms = [Permission(name=x, plugin_=self) for x in new_perms]
# self.permissions = list(filter(lambda x: x.name in permissions, self.permissions and isinstance(self.permissions, list) or []))
self.permissions.extend(_perms)
class AuthPlugin(Plugin):
"""Base class for all authentification plugins
See also `Plugin`
"""
def login(self, login_name, password) -> Union[bool, str]:
"""Login routine, MUST BE IMPLEMENTED!
Args:
login_name: The name the user entered
password: The password the user used to log in
Returns:
Must return False if not found or invalid credentials, otherwise the UID is returned
"""
raise NotImplemented
def update_user(self, user: "User"):
"""If backend is using external data, then update this user instance with external data
Args:
user: User object
"""
pass
def user_exists(self, userid) -> bool:
"""Check if user exists on this backend
Args:
userid: Userid to search
Returns:
True or False
"""
raise NotImplemented
def modify_user(self, user, password, new_password=None):
"""If backend is using (writeable) external data, then update the external database with the user provided.
User might have roles not existing on the external database, so you might have to create those.
Args:
user: User object
password: Password (some backends need the current password for changes) if None force edit (admin)
new_password: If set a password change is requested
Raises:
BadRequest: Logic error, e.g. password is wrong.
Error: Other errors if backend went mad (are not handled and will result in a 500 error)
"""
pass
def can_register(self):
"""Check if this backend allows to register new users"""
return False
def create_user(self, user, password):
"""If backend is using (writeable) external data, then create a new user on the external database.
Args:
user: User object
password: string
"""
raise NotImplementedError
def delete_user(self, user):
"""If backend is using (writeable) external data, then delete the user from external database.
Args:
user: User object
"""
raise NotImplementedError
def get_modified_time(self, user):
"""If backend is using external data, then return the timestamp of the last modification
Args:
user: User object
Returns:
Timestamp of last modification
"""
pass
def get_avatar(self, user) -> _Avatar:
"""Retrieve avatar for given user (if supported by auth backend)
Default behavior is to use native Image objects,
so by default this function is never called, as the userController checks
native Image objects first.
Args:
user: User to retrieve the avatar for
Raises:
NotFound: If no avatar found or not implemented
"""
raise NotFound
def set_avatar(self, user, file: FileStorage):
"""Set the avatar for given user (if supported by auth backend)
Default behavior is to use native Image objects stored on the Flaschengeist server
Args:
user: User to set the avatar for
file: `werkzeug.datastructures.FileStorage` uploaded by the user
Raises:
MethodNotAllowed: If not supported by Backend
Any valid HTTP exception
"""
# By default save the image to the avatar,
# deleting would happen by unsetting it
from ..controller import imageController
user.avatar_ = imageController.upload_image(file)
def delete_avatar(self, user):
"""Delete the avatar for given user (if supported by auth backend)
Default behavior is to use the imageController and native Image objects.
Args:
user: Uset to delete the avatar for
Raises:
MethodNotAllowed: If not supported by Backend
"""
user.avatar_ = None

View File

@ -0,0 +1,176 @@
"""Authentication plugin, provides basic routes
Allow management of authentication, login, logout, etc.
"""
from flask import Blueprint, request, jsonify
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
from flaschengeist import logger
from flaschengeist.plugins import Plugin
from flaschengeist.utils.HTTP import no_content, created
from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import sessionController, userController
class AuthRoutePlugin(Plugin):
blueprint = Blueprint("auth", __name__)
@AuthRoutePlugin.blueprint.route("/auth", methods=["POST"])
def login():
"""Login in an user and create a session
Route: ``/auth`` | Method: ``POST``
POST-data: ``{userid: string, password: string}``
Returns:
A JSON object with `flaschengeist.models.user.User` and created
`flaschengeist.models.session.Session` or HTTP error
"""
logger.debug("Start log in.")
data = request.get_json()
try:
userid = str(data["userid"])
password = str(data["password"])
except (KeyError, ValueError, TypeError):
raise BadRequest("Missing parameter(s)")
logger.debug(f"search user {userid} in database")
user = userController.login_user(userid, password)
if not user:
raise Unauthorized
session = sessionController.create(user, request_headers=request.headers)
logger.debug(f"token is {session.token}")
logger.info(f"User {userid} logged in.")
# Lets cleanup the DB
sessionController.clear_expired()
return created(session)
@AuthRoutePlugin.blueprint.route("/auth", methods=["GET"])
@login_required()
def get_sessions(current_session, **kwargs):
"""Get all valid sessions of current user
Route: ``/auth`` | Method: ``GET``
Returns:
A JSON array of `flaschengeist.models.session.Session` or HTTP error
"""
sessions = sessionController.get_users_sessions(current_session.user_)
return jsonify(sessions)
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"])
@login_required()
def delete_session(token, current_session, **kwargs):
"""Delete a session aka "logout"
Route: ``/auth/<token>`` | Method: ``DELETE``
Returns:
200 Status (empty) or HTTP error
"""
logger.debug("Try to delete access token {{ {} }}".format(token))
session = sessionController.get_session(token, current_session.user_)
if not session:
logger.debug("Token not found in database!")
# Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same
raise Forbidden
sessionController.delete_session(session)
sessionController.clear_expired()
return ""
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"])
@login_required()
def get_session(token, current_session, **kwargs):
"""Retrieve information about a session
Route: ``/auth/<token>`` | Method: ``GET``
Attributes:
token: Token identifying session to retrieve
current_session: Session sent with Authorization Header
Returns:
JSON encoded `flaschengeist.models.session.Session` or HTTP error
"""
logger.debug("get token {{ {} }}".format(token))
session = sessionController.get_session(token, current_session.user_)
if not session:
# Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same
raise Forbidden
return jsonify(session)
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"])
@login_required()
def set_lifetime(token, current_session, **kwargs):
"""Set lifetime of a session
Route: ``/auth/<token>`` | Method: ``PUT``
POST-data: ``{value: int}``
Attributes:
token: Token identifying the session
current_session: Session sent with Authorization Header
Returns:
HTTP-204 or HTTP error
"""
session = sessionController.get_session(token, current_session.user_)
if not session:
# Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same
raise Forbidden
try:
lifetime = request.get_json()["value"]
logger.debug(f"set lifetime >{lifetime}< to access token >{token}<")
sessionController.set_lifetime(session, lifetime)
return jsonify(sessionController.get_session(token, current_session.user_))
except (KeyError, TypeError):
raise BadRequest
@AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"])
@login_required()
def get_assocd_user(token, current_session, **kwargs):
"""Retrieve user owning a session
Route: ``/auth/<token>/user`` | Method: ``GET``
Attributes:
token: Token identifying the session
current_session: Session sent with Authorization Header
Returns:
JSON encoded `flaschengeist.models.user.User` or HTTP error
"""
logger.debug("get token {{ {} }}".format(token))
session = sessionController.get_session(token, current_session.user_)
if not session:
# Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same
raise Forbidden
return jsonify(session.user_)
@AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"])
def reset_password():
data = request.get_json()
if "userid" in data:
user = userController.get_user(data["userid"])
if user:
userController.request_reset(user)
elif "password" in data and "token" in data:
userController.reset_password(data["token"], data["password"])
else:
raise BadRequest("Missing parameter(s)")
return no_content()

View File

@ -0,0 +1,332 @@
"""LDAP Authentication Provider Plugin"""
import os
import ssl
from PIL import Image
from io import BytesIO
from flask_ldapconn import LDAPConn
from flask import current_app as app
from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE
from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from werkzeug.datastructures import FileStorage
from datetime import datetime
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.controller import userController
from flaschengeist.models import User, Role
from flaschengeist.models.user import _Avatar
from flaschengeist.plugins import AuthPlugin, before_role_updated
class AuthLDAP(AuthPlugin):
def load(self):
self.config = config.get("auth_ldap", None)
if self.config is None:
logger.error("auth_ldap was not configured in flaschengeist.toml", exc_info=True)
raise InternalServerError
app.config.update(
LDAP_SERVER=self.config.get("host", "localhost"),
LDAP_PORT=self.config.get("port", 389),
LDAP_BINDDN=self.config.get("bind_dn", None),
LDAP_SECRET=self.config.get("secret", None),
LDAP_USE_SSL=self.config.get("use_ssl", False),
# That's not TLS, its dirty StartTLS on unencrypted LDAP
LDAP_USE_TLS=False,
LDAP_TLS_VERSION=ssl.PROTOCOL_TLS,
FORCE_ATTRIBUTE_VALUE_AS_LIST=True,
)
if "ca_cert" in config:
app.config["LDAP_CA_CERTS_FILE"] = self.config["ca_cert"]
else:
# Default is CERT_REQUIRED
app.config["LDAP_REQUIRE_CERT"] = ssl.CERT_OPTIONAL
self.ldap = LDAPConn(app)
self.base_dn = self.config["base_dn"]
self.search_dn = self.config.get("search_dn", "ou=people,{base_dn}").format(base_dn=self.base_dn)
self.group_dn = self.config.get("group_dn", "ou=group,{base_dn}").format(base_dn=self.base_dn)
self.password_hash = self.config.get("password_hash", "SSHA").upper()
self.object_classes = self.config.get("object_classes", ["inetOrgPerson"])
self.user_attributes: dict = self.config.get("user_attributes", {})
self.dn_template = self.config.get("dn_template")
# TODO: might not be set if modify is called
self.root_dn = self.config.get("root_dn", None)
self.root_secret = self.config.get("root_secret", None)
@before_role_updated
def _role_updated(role, new_name):
logger.debug(f"LDAP: before_role_updated called with ({role}, {new_name})")
self.__modify_role(role, new_name)
def login(self, login_name, password):
if not login_name:
return False
return login_name if self.ldap.authenticate(login_name, password, "uid", self.base_dn) else False
def user_exists(self, userid) -> bool:
attr = self.__find(userid, None)
return attr is not None
def update_user(self, user):
attr = self.__find(user.userid)
self.__update(user, attr)
def can_register(self):
return self.root_dn is not None
def create_user(self, user, password):
try:
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
attributes = self.user_attributes.copy()
if "uidNumber" in attributes:
self.ldap.connection.search(
self.search_dn,
"(uidNumber=*)",
SUBTREE,
attributes=["uidNumber"],
)
resp = sorted(
self.ldap.response(),
key=lambda i: i["attributes"]["uidNumber"],
reverse=True,
)
attributes["uidNumber"] = resp[0]["attributes"]["uidNumber"] + 1 if resp else attributes["uidNumber"]
dn = self.dn_template.format(
user=user,
base_dn=self.base_dn,
)
if "default_gid" in attributes:
default_gid = attributes.pop("default_gid")
attributes["gidNumber"] = default_gid
if "homeDirectory" in attributes:
attributes["homeDirectory"] = attributes.get("homeDirectory").format(
firstname=user.firstname,
lastname=user.lastname,
userid=user.userid,
mail=user.mail,
display_name=user.display_name,
)
attributes.update(
{
"sn": user.lastname,
"givenName": user.firstname,
"uid": user.userid,
"userPassword": self.__hash(password),
"mail": user.mail,
}
)
if user.display_name:
attributes.update({"displayName": user.display_name})
ldap_conn.add(dn, self.object_classes, attributes)
self._set_roles(user)
self.update_user(user)
except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest
def modify_user(self, user: User, password=None, new_password=None):
try:
dn = user.get_attribute("DN")
logger.debug(f"LDAP: modify_user for user {user.userid} with dn {dn}")
if password:
logger.debug(f"LDAP: modify_user for user {user.userid} with password")
ldap_conn = self.ldap.connect(dn, password)
else:
logger.debug(f"LDAP: modify_user for user {user.userid} with root_dn")
if self.root_dn is None:
logger.error("root_dn missing in ldap config!")
raise InternalServerError
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
modifier = {}
for name, ldap_name in [
("firstname", "givenName"),
("lastname", "sn"),
("mail", "mail"),
("display_name", "displayName"),
]:
if hasattr(user, name):
attribute = getattr(user, name)
if attribute:
modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])]
if new_password:
modifier["userPassword"] = [(MODIFY_REPLACE, [self.__hash(new_password)])]
if "userPassword" in modifier:
logger.debug(f"LDAP: modify_user for user {user.userid} with password change (can't show >modifier<)")
else:
logger.debug(f"LDAP: modify_user for user {user.userid} with modifier {modifier}")
ldap_conn.modify(dn, modifier)
self._set_roles(user)
except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest
def get_modified_time(self, user):
self.ldap.connection.search(
self.search_dn,
"(uid={})".format(user.userid),
SUBTREE,
attributes=["modifyTimestamp"],
)
r = self.ldap.connection.response[0]["attributes"]
modified_time = r["modifyTimestamp"][0]
return datetime.strptime(modified_time, "%Y%m%d%H%M%SZ")
def get_avatar(self, user):
self.ldap.connection.search(
self.search_dn,
"(uid={})".format(user.userid),
SUBTREE,
attributes=["jpegPhoto"],
)
r = self.ldap.connection.response[0]["attributes"]
if "jpegPhoto" in r and len(r["jpegPhoto"]) > 0:
avatar = _Avatar()
avatar.mimetype = "image/jpeg"
avatar.binary = bytearray(r["jpegPhoto"][0])
return avatar
else:
raise NotFound
def set_avatar(self, user: User, file: FileStorage):
if self.root_dn is None:
logger.error("root_dn missing in ldap config!")
raise InternalServerError
image_bytes = BytesIO()
try:
# Make sure converted to RGB, e.g. png support RGBA but jpeg does not
image = Image.open(file).convert("RGB")
image.save(image_bytes, format="JPEG")
except IOError:
logger.debug(f"Could not convert avatar from '{file.mimetype}' to JPEG")
raise BadRequest("Unsupported image format")
dn = user.get_attribute("DN")
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [image_bytes.getvalue()])]})
def delete_avatar(self, user):
if self.root_dn is None:
logger.error("root_dn missing in ldap config!")
dn = user.get_attribute("DN")
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [])]})
def __find(self, userid, mail=None):
"""Find attributes of an user by uid or mail in LDAP"""
con = self.ldap.connection
if not con:
con = self.ldap.connect(self.root_dn, self.root_secret)
con.search(
self.search_dn,
f"(| (uid={userid})(mail={mail}))" if mail else f"(uid={userid})",
SUBTREE,
attributes=["uid", "givenName", "sn", "mail"],
)
return con.response[0]["attributes"] if len(con.response) > 0 else None
def __update(self, user, attr):
"""Update an User object with LDAP attributes"""
if attr["uid"][0] == user.userid:
user.set_attribute("DN", self.ldap.connection.response[0]["dn"])
user.firstname = attr["givenName"][0]
user.lastname = attr["sn"][0]
if attr["mail"]:
user.mail = attr["mail"][0]
if "displayName" in attr:
user.display_name = attr["displayName"][0]
userController.set_roles(user, self._get_groups(user.userid), create=True)
def __modify_role(
self,
role: Role,
new_name,
):
if self.root_dn is None:
logger.error("root_dn missing in ldap config!")
raise InternalServerError
try:
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
ldap_conn.search(self.group_dn, f"(cn={role.name})", SUBTREE, attributes=["cn"])
if len(ldap_conn.response) > 0:
dn = ldap_conn.response[0]["dn"]
if new_name:
ldap_conn.modify_dn(dn, f"cn={new_name}")
else:
ldap_conn.delete(dn)
except LDAPPasswordIsMandatoryError:
raise BadRequest
except LDAPBindError:
logger.debug(f"Could not bind to LDAP server", exc_info=True)
raise InternalServerError
def __hash(self, password):
if self.password_hash == "ARGON2":
from argon2 import PasswordHasher
return f"{{ARGON2}}{PasswordHasher().hash(password)}"
else:
from hashlib import pbkdf2_hmac, sha1
import base64
salt = os.urandom(16)
if self.password_hash == "PBKDF2":
rounds = 200000
password_hash = base64.b64encode(pbkdf2_hmac("sha512", password.encode("utf-8"), salt, rounds)).decode()
return f"{{PBKDF2-SHA512}}{rounds}${base64.b64encode(salt).decode()}${password_hash}"
else:
return f"{{SSHA}}{base64.b64encode(sha1(password.encode() + salt).digest() + salt).decode()}"
def _get_groups(self, uid):
groups = []
self.ldap.connection.search(
self.group_dn,
"(memberUID={})".format(uid),
SUBTREE,
attributes=["cn"],
)
groups_data = self.ldap.connection.response
for data in groups_data:
groups.append(data["attributes"]["cn"][0])
return groups
def _get_all_roles(self):
self.ldap.connection.search(
self.group_dn,
"(cn=*)",
SUBTREE,
attributes=["cn", "gidNumber", "memberUid"],
)
return self.ldap.response()
def _set_roles(self, user: User):
try:
ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
ldap_roles = self._get_all_roles()
gid_numbers = sorted(ldap_roles, key=lambda i: i["attributes"]["gidNumber"], reverse=True)
gid_number = gid_numbers[0]["attributes"]["gidNumber"] + 1
for user_role in user.roles:
if user_role not in [role["attributes"]["cn"][0] for role in ldap_roles]:
ldap_conn.add(
f"cn={user_role},{self.group_dn}",
["posixGroup"],
attributes={"gidNumber": gid_number},
)
ldap_roles = self._get_all_roles()
for ldap_role in ldap_roles:
if ldap_role["attributes"]["cn"][0] in user.roles:
modify = {"memberUid": [(MODIFY_ADD, [user.userid])]}
else:
modify = {"memberUid": [(MODIFY_DELETE, [user.userid])]}
ldap_conn.modify(ldap_role["dn"], modify)
except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest
except IndexError as e:
logger.error("Roles in LDAP", exc_info=True)

View File

@ -0,0 +1,47 @@
import click
from flask import current_app
from flask.cli import with_appcontext
from werkzeug.exceptions import NotFound
@click.command(no_args_is_help=True)
@click.option("--sync", is_flag=True, default=False, help="Synchronize users from LDAP -> database")
@click.option("--sync-ldap", is_flag=True, default=False, help="Synchronize users from database -> LDAP")
@with_appcontext
@click.pass_context
def ldap(ctx, sync, sync_ldap):
"""Tools for the LDAP authentification"""
from flaschengeist.controller import userController
from flaschengeist.plugins.auth_ldap import AuthLDAP
if sync:
click.echo("Synchronizing users from LDAP -> database")
from ldap3 import SUBTREE
from flaschengeist.models import User
from flaschengeist.database import db
auth_ldap: AuthLDAP = current_app.config.get("FG_PLUGINS").get("auth_ldap")
if auth_ldap is None or not isinstance(auth_ldap, AuthLDAP):
ctx.fail("auth_ldap plugin not found or not enabled!")
conn = auth_ldap.ldap.connection
if not conn:
conn = auth_ldap.ldap.connect(auth_ldap.root_dn, auth_ldap.root_secret)
conn.search(auth_ldap.search_dn, "(uid=*)", SUBTREE, attributes=["uid", "givenName", "sn", "mail"])
ldap_users_response = conn.response
for ldap_user in ldap_users_response:
uid = ldap_user["attributes"]["uid"][0]
try:
user = userController.get_user(uid)
except NotFound:
user = User(userid=uid)
db.session.add(user)
userController.update_user(user, auth_ldap)
if sync_ldap:
click.echo("Synchronizing users from database -> LDAP")
auth_ldap: AuthLDAP = current_app.config.get("FG_PLUGINS").get("auth_ldap")
if auth_ldap is None or not isinstance(auth_ldap, AuthLDAP):
ctx.fail("auth_ldap plugin not found or not enabled!")
users = userController.get_users()
for user in users:
userController.update_user(user, auth_ldap)

View File

@ -0,0 +1,65 @@
"""Authentication Provider Plugin
Allows simple authentication using Username-Password pair with password saved into
Flaschengeist database (as User attribute)
"""
import os
import hashlib
import binascii
from werkzeug.exceptions import BadRequest
from flaschengeist.plugins import AuthPlugin
from flaschengeist.models import User, Role, Permission
from flaschengeist.database import db
from flaschengeist import logger
class AuthPlain(AuthPlugin):
def can_register(self):
return True
def login(self, login_name, password):
users: list[User] = (
User.query.filter((User.userid == login_name) | (User.mail == login_name))
.filter(User._attributes.any(name="password"))
.all()
)
for user in users:
if AuthPlain._verify_password(user.get_attribute("password"), password):
return user.userid
return False
def modify_user(self, user, password, new_password=None):
if password is not None and not self.login(user, password):
raise BadRequest
if new_password:
user.set_attribute("password", AuthPlain._hash_password(new_password))
def user_exists(self, userid) -> bool:
return (
db.session.query(User.id_).filter(User.userid == userid, User._attributes.any(name="password")).first()
is not None
)
def create_user(self, user, password):
if not user.userid:
raise BadRequest("userid is missing for new user")
hashed = AuthPlain._hash_password(password)
user.set_attribute("password", hashed)
def delete_user(self, user):
pass
@staticmethod
def _hash_password(password):
salt = hashlib.sha256(os.urandom(60)).hexdigest().encode("ascii")
pass_hash = hashlib.pbkdf2_hmac("sha3-512", password.encode("utf-8"), salt, 100000)
pass_hash = binascii.hexlify(pass_hash)
return (salt + pass_hash).decode("ascii")
@staticmethod
def _verify_password(stored_password: str, provided_password: str):
salt = stored_password[:64]
stored_password = stored_password[64:]
pass_hash = hashlib.pbkdf2_hmac("sha3-512", provided_password.encode("utf-8"), salt.encode("ascii"), 100000)
pass_hash = binascii.hexlify(pass_hash).decode("ascii")
return pass_hash == stored_password

View File

@ -0,0 +1,89 @@
"""Balance plugin
Extends users plugin with balance functions
"""
from flask import current_app
from werkzeug.exceptions import NotFound
from werkzeug.local import LocalProxy
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.plugins import Plugin, plugins_loaded, before_update_user
from flaschengeist.plugins.scheduler import add_scheduled
from . import permissions, models
def service_debit():
from flaschengeist.database import db
from flaschengeist.models import UtcDateTime
from flaschengeist.models.user import User
from flaschengeist.controller import roleController
from flaschengeist_events.models import Service, Job
role = BalancePlugin.plugin.get_setting("service_role", default=None)
if role is None:
try:
role = roleController.get("__has_service")
except NotFound:
role = roleController.create_role("__has_service", [permissions.DEBIT])
BalancePlugin.plugin.set_setting("service_role", role.id)
else:
role = roleController.get(role)
active_services = (
User.query.join(Service, User.id_ == Service._user_id)
.join(Job, Service.job_)
.filter(Job.start <= UtcDateTime.current_utc(), Job.end.is_(None) | (Job.end >= UtcDateTime.current_utc()))
.distinct(User.id_)
.all()
)
previous_services = BalancePlugin.plugin.get_setting("service_debit", default=[])
logger.debug(f"Found {len(active_services)} users doing their service.")
if len(previous_services) > 0:
previous_services = User.query.filter(User.userid.in_(previous_services)).all()
# Remove not active users
for user in [u for u in previous_services if u not in active_services]:
user.roles_ = [r for r in user.roles_ if r.id != role.id]
# Add active
for user in [u for u in active_services if u not in previous_services]:
if role not in user.roles_:
user.roles_.append(role)
db.session.commit()
BalancePlugin.plugin.set_setting("service_debit", [u.userid for u in active_services])
class BalancePlugin(Plugin):
# id = "dev.flaschengeist.balance"
models = models
def install(self):
self.install_permissions(permissions.permissions)
def load(self):
from .routes import blueprint
self.blueprint = blueprint
@plugins_loaded
def post_loaded(*args, **kwargs):
if config.get("allow_service_debit", False) and "events" in current_app.config["FG_PLUGINS"]:
add_scheduled(f"{id}.service_debit", service_debit, minutes=1)
@before_update_user
def set_default_limit(user, *args):
from . import balance_controller
try:
limit = self.get_setting("limit")
logger.debug("Setting default limit of {} to user {}".format(limit, user.userid))
balance_controller.set_limit(user, limit, override=False)
except KeyError:
pass
@staticmethod
def getPlugin() -> LocalProxy["BalancePlugin"]:
return LocalProxy(lambda: current_app.config["FG_PLUGINS"]["balance"])

View File

@ -0,0 +1,335 @@
# German: Soll -> Abgang vom Konto
# Haben -> Zugang aufs Konto
# English: Debit -> from account
# Credit -> to account
from enum import IntEnum
from sqlalchemy import func, case, and_, or_
from sqlalchemy.ext.hybrid import hybrid_property
from datetime import datetime
from werkzeug.exceptions import BadRequest, NotFound, Conflict
from flaschengeist.database import db
from flaschengeist.models.user import User, _UserAttribute
from flaschengeist.app import logger
from .models import Transaction
from . import permissions, BalancePlugin
__attribute_limit = "balance_limit"
class NotifyType(IntEnum):
SEND_TO = 0x01
SEND_FROM = 0x02
ADD_FROM = 0x03
SUB_FROM = 0x04
def set_limit(user: User, limit: float, override=True):
if override or not user.has_attribute(__attribute_limit):
user.set_attribute(__attribute_limit, limit)
db.session.commit()
def get_limit(user: User) -> float:
return user.get_attribute(__attribute_limit, default=None)
def get_balance(user, start: datetime = None, end: datetime = None):
query = db.session.query(func.sum(Transaction._amount))
if start:
query = query.filter(start <= Transaction.time)
if end:
query = query.filter(Transaction.time <= end)
credit = query.filter(Transaction.receiver_ == user).scalar() or 0
debit = query.filter(Transaction.sender_ == user).scalar() or 0
return credit, debit, credit - debit
def get_balances(
start: datetime = None,
end: datetime = None,
limit=None,
offset=None,
descending=None,
sortBy=None,
_filter=None,
):
logger.debug(
f"get_balances(start={start}, end={end}, limit={limit}, offset={offset}, descending={descending}, sortBy={sortBy}, _filter={_filter})"
)
class _User(User):
_debit = db.relationship(Transaction, back_populates="sender_", foreign_keys=[Transaction._sender_id])
_credit = db.relationship(
Transaction,
back_populates="receiver_",
foreign_keys=[Transaction._receiver_id],
)
@hybrid_property
def debit(self):
return sum([cred.amount for cred in self._debit])
@debit.expression
def debit(cls):
a = (
db.select(func.sum(Transaction._amount))
.where(cls.id_ == Transaction._sender_id, Transaction._amount)
.scalar_subquery()
)
return case([(a, a)], else_=0)
@hybrid_property
def credit(self):
return sum([cred.amount for cred in self._credit])
@credit.expression
def credit(cls):
b = (
db.select(func.sum(Transaction._amount))
.where(cls.id_ == Transaction._receiver_id, Transaction._amount)
.scalar_subquery()
)
return case([(b, b)], else_=0)
@hybrid_property
def limit(self):
return self.get_attribute("balance_limit", None)
@limit.expression
def limit(cls):
return (
db.select(_UserAttribute.value)
.where(
and_(
cls.id_ == _UserAttribute.user,
_UserAttribute.name == "balance_limit",
)
)
.scalar_subquery()
)
def get_debit(self, start: datetime = None, end: datetime = None):
if start and end:
return sum([deb.amount for deb in self._debit if start <= deb.time and deb.time <= end])
if start:
return sum([deb.amount for deb in self._dedit if start <= deb.time])
if end:
return sum([deb.amount for deb in self._dedit if deb.time <= end])
return self.debit
def get_credit(self, start: datetime = None, end: datetime = None):
if start and end:
return sum([cred.amount for cred in self._credit if start <= cred.time and cred.time <= end])
if start:
return sum([cred.amount for cred in self._credit if start <= cred.time])
if end:
return sum([cred.amount for cred in self._credit if cred.time <= end])
return self.credit
query = _User.query
if start:
q1 = query.join(_User._credit).filter(start <= Transaction.time)
q2 = query.join(_User._debit).filter(start <= Transaction.time)
query = q1.union(q2)
if end:
q1 = query.join(_User._credit).filter(Transaction.time <= end)
q2 = query.join(_User._debit).filter(Transaction.time <= end)
query = q1.union(q2)
if _filter:
query = query.filter(
or_(
_User.firstname.ilike(f"%{_filter.lower()}%"),
_User.lastname.ilike(f"%{_filter.lower()}%"),
)
)
if sortBy == "balance":
if descending:
query = query.order_by(
(_User.credit - _User.debit).desc(),
_User.lastname.asc(),
_User.firstname.asc(),
)
else:
query = query.order_by(
(_User.credit - _User.debit).asc(),
_User.lastname.asc(),
_User.firstname.asc(),
)
elif sortBy == "limit":
if descending:
query = query.order_by(_User.limit.desc(), User.lastname.asc(), User.firstname.asc())
else:
query = query.order_by(_User.limit.asc(), User.lastname.asc(), User.firstname.asc())
elif sortBy == "firstname":
if descending:
query = query.order_by(User.firstname.desc(), User.lastname.desc())
else:
query = query.order_by(User.firstname.asc(), User.lastname.asc())
elif sortBy == "lastname":
if descending:
query = query.order_by(User.lastname.desc(), User.firstname.desc())
else:
query = query.order_by(User.lastname.asc(), User.firstname.asc())
count = None
if limit:
count = query.count()
query = query.limit(limit)
if offset:
query = query.offset(offset)
users = query
all = {}
for user in users:
all[user.userid] = [user.get_credit(start, end), 0]
all[user.userid][1] = user.get_debit(start, end)
return all, count
def send(sender: User, receiver, amount: float, author: User):
"""Send credit from one user to an other
Args:
sender: User who sends the amount
receiver: User who receives the amount
amount: Amount to send
author: User authoring this transaction
Returns:
Transaction that was created
Raises:
BadRequest if amount <= 0
"""
logger.debug(f"send(sender={sender}, receiver={receiver}, amount={amount}, author={author})")
if amount <= 0:
raise BadRequest
if sender and sender.has_attribute(__attribute_limit):
if (get_balance(sender)[2] - amount) < sender.get_attribute(__attribute_limit) and not author.has_permission(
permissions.EXCEED_LIMIT
):
raise Conflict("Limit exceeded")
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
db.session.add(transaction)
db.session.commit()
if sender is not None and sender.id_ != author.id_:
if receiver is not None:
BalancePlugin.getPlugin().notify(
sender,
"Neue Transaktion",
{
"type": NotifyType.SEND_FROM,
"receiver_id": receiver.userid,
"author_id": author.userid,
"amount": amount,
},
)
else:
BalancePlugin.getPlugin().notify(
sender,
"Neue Transaktion",
{
"type": NotifyType.SUB_FROM,
"author_id": author.userid,
"amount": amount,
},
)
if receiver is not None and receiver.id_ != author.id_:
if sender is not None:
BalancePlugin.getPlugin().notify(
receiver,
"Neue Transaktion",
{
"type": NotifyType.SEND_TO,
"sender_id": sender.userid,
"amount": amount,
},
)
else:
BalancePlugin.getPlugin().notify(
receiver,
"Neue Transaktion",
{
"type": NotifyType.ADD_FROM,
"author_id": author.userid,
"amount": amount,
},
)
return transaction
def change_balance(user, amount: float, author):
"""Change balance of user
Args:
user: User to change balance
amount: Amount to change balance
author: User authoring this transaction
"""
sender = user if amount < 0 else None
receiver = user if amount > 0 else None
return send(sender, receiver, abs(amount), author)
def get_transaction(transaction_id) -> Transaction:
transaction = Transaction.query.get(transaction_id)
if not transaction:
raise NotFound
return transaction
def get_transactions(
user,
start=None,
end=None,
limit=None,
offset=None,
show_reversal=False,
show_cancelled=True,
descending=False,
):
count = None
query = Transaction.query.filter((Transaction.sender_ == user) | (Transaction.receiver_ == user))
if start:
query = query.filter(start <= Transaction.time)
if end:
query = query.filter(Transaction.time <= end)
# Do not show reversals if disabled or cancelled ones are hidden
if not show_reversal or not show_cancelled:
query = query.filter(Transaction.original_ == None)
if not show_cancelled:
query = query.filter(Transaction.reversal_id.is_(None))
if descending:
query = query.order_by(Transaction.time.desc())
else:
query = query.order_by(Transaction.time)
if limit is not None:
count = query.count()
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
return query.all(), count
def reverse_transaction(transaction: Transaction, author: User):
"""Reverse a transaction
Args:
transaction: Transaction to reverse
author: User that wants the transaction to be reverted
"""
if transaction.reversal_:
raise Conflict
reversal = send(transaction.receiver_, transaction.sender_, transaction.amount, author)
reversal.original_ = transaction
transaction.reversal = reversal
db.session.commit()
return reversal

View File

@ -0,0 +1,47 @@
"""balance: initial
Revision ID: 98f2733bbe45
Revises:
Create Date: 2022-02-23 14:41:03.089145
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
# revision identifiers, used by Alembic.
revision = "98f2733bbe45"
down_revision = None
branch_labels = ("balance",)
depends_on = "flaschengeist"
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"balance_transaction",
sa.Column("receiver_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("sender_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("author_id", flaschengeist.models.Serial(), nullable=False),
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("time", flaschengeist.models.UtcDateTime(), nullable=False),
sa.Column("amount", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=False),
sa.Column("reversal_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["author_id"], ["user.id"], name=op.f("fk_balance_transaction_author_id_user")),
sa.ForeignKeyConstraint(["receiver_id"], ["user.id"], name=op.f("fk_balance_transaction_receiver_id_user")),
sa.ForeignKeyConstraint(
["reversal_id"],
["balance_transaction.id"],
name=op.f("fk_balance_transaction_reversal_id_balance_transaction"),
),
sa.ForeignKeyConstraint(["sender_id"], ["user.id"], name=op.f("fk_balance_transaction_sender_id_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_balance_transaction")),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("balance_transaction")
# ### end Alembic commands ###

View File

@ -0,0 +1,72 @@
from datetime import datetime
from typing import Optional
from sqlalchemy.ext.hybrid import hybrid_property
from math import floor
from flaschengeist import logger
from flaschengeist.database import db
from flaschengeist.models.user import User
from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
class Transaction(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "balance_transaction"
# Protected foreign key properties
_receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id"))
_sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"))
_author_id = db.Column("author_id", Serial, db.ForeignKey("user.id"), nullable=False)
# Public and exported member
id: int = db.Column("id", Serial, primary_key=True)
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
_amount: float = db.Column("amount", db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
reversal_id: Optional[int] = db.Column(Serial, db.ForeignKey("balance_transaction.id"))
amount: float
# Dummy properties used for JSON serialization (userid instead of full user)
author_id: Optional[str] = None
sender_id: Optional[str] = None
original_id: Optional[int] = None
receiver_id: Optional[str] = None
# Not exported relationships just in backend only
sender_: User = db.relationship("User", foreign_keys=[_sender_id])
receiver_: User = db.relationship("User", foreign_keys=[_receiver_id])
author_: User = db.relationship("User", foreign_keys=[_author_id])
original_ = db.relationship("Transaction", uselist=False, backref=db.backref("reversal_", remote_side=[id]))
@hybrid_property
def sender_id(self):
return self.sender_.userid if self.sender_ else None
@sender_id.expression
def sender_id(cls):
return db.select([User.userid]).where(cls._sender_id == User.id_).scalar_subquery()
@hybrid_property
def receiver_id(self):
return self.receiver_.userid if self.receiver_ else None
@receiver_id.expression
def receiver_id(cls):
return db.select([User.userid]).where(cls._receiver_id == User.id_).scalar_subquery()
@property
def author_id(self):
return self.author_.userid
@property
def original_id(self):
return self.original_.id if self.original_ else None
@property
def amount(self):
return self._amount
@amount.setter
def amount(self, value):
self._amount = floor(value * 100) / 100
def __repr__(self):
return f"<Transaction {self.id} {self.amount} {self.time} {self.sender_id} {self.receiver_id} {self.author_id}>"

View File

@ -0,0 +1,27 @@
SHOW = "balance_show"
"""Show own balance"""
SHOW_OTHER = "balance_show_others"
"""Show others balance"""
CREDIT = "balance_credit"
"""Credit balances (give)"""
DEBIT = "balance_debit"
"""Debit balances (take)"""
DEBIT_OWN = "balance_debit_own"
"""Debit own balance"""
SEND = "balance_send"
"""Send from to other"""
SEND_OTHER = "balance_send_others"
"""Send from other to another"""
SET_LIMIT = "balance_set_limit"
"""Can set limit for users"""
EXCEED_LIMIT = "balance_exceed_limit"
"""Allow sending / sub while exceeding the set limit"""
REVERSAL = "balance_reversal"
"""Allow reverting transactions"""
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -0,0 +1,338 @@
from datetime import datetime, timezone
from logging import log
from werkzeug.exceptions import Forbidden, BadRequest
from flask import Blueprint, request, jsonify
from flaschengeist.utils import HTTP
from flaschengeist.models.session import Session
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import userController
from flaschengeist.app import logger
from . import BalancePlugin, balance_controller, permissions
def str2bool(string: str):
if string.lower() in ["true", "yes", "1"]:
return True
elif string.lower() in ["false", "no", "0"]:
return False
raise ValueError
blueprint = Blueprint("balance", __package__)
@blueprint.route("/users/<userid>/balance/shortcuts", methods=["GET", "PUT"])
@login_required()
def get_shortcuts(userid, current_session: Session):
"""Get balance shortcuts of an user
Route: ``/users/<userid>/balance/shortcuts`` | Method: ``GET`` or ``PUT``
POST-data: On ``PUT`` json encoded array of floats
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
GET: JSON object containing the shortcuts as float array or HTTP error
PUT: HTTP-created or HTTP error
"""
if userid != current_session.user_.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("balance_shortcuts", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, (int, float)) for n in data):
raise BadRequest
data.sort(reverse=True)
user.set_attribute("balance_shortcuts", data)
userController.persist()
return HTTP.no_content()
@blueprint.route("/users/<userid>/balance/limit", methods=["GET"])
@login_required()
def get_limit(userid, current_session: Session):
"""Get limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``GET``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
JSON object containing the limit (or Null if no limit set) or HTTP error
"""
user = userController.get_user(userid)
if (user != current_session.user_ and not current_session.user_.has_permission(permissions.SET_LIMIT)) or (
user == current_session.user_ and not user.has_permission(permissions.SHOW)
):
raise Forbidden
return {"limit": balance_controller.get_limit(user)}
@blueprint.route("/users/<userid>/balance/limit", methods=["PUT"])
@login_required(permissions.SET_LIMIT)
def set_limit(userid, current_session: Session):
"""Set the limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``PUT``
POST-data: ``{limit: float}``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
HTTP-200 or HTTP error
"""
user = userController.get_user(userid)
data = request.get_json()
try:
limit = data["limit"]
except (TypeError, KeyError):
raise BadRequest
balance_controller.set_limit(user, limit)
return HTTP.no_content()
@blueprint.route("/users/balance/limit", methods=["GET", "PUT"])
@login_required(permission=permissions.SET_LIMIT)
def limits(current_session: Session):
"""Get, Modify limit of all users
Args:
current_ession: Session sent with Authorization Header
Returns:
JSON encoded array of userid with limit or HTTP-error
"""
users = userController.get_users()
if request.method == "GET":
return jsonify([{"userid": user.userid, "limit": user.get_attribute("balance_limit")} for user in users])
data = request.get_json()
try:
limit = data["limit"]
except (TypeError, KeyError):
raise BadRequest
for user in users:
balance_controller.set_limit(user, limit)
return HTTP.no_content()
@blueprint.route("/users/<userid>/balance", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_balance(userid, current_session: Session):
"""Get balance of user, optionally filtered
Route: ``/users/<userid>/balance`` | Method: ``GET``
GET-parameters: ``{from?: string, to?: string}``
Args:
userid: Userid of user to get balance from
current_session: Session sent with Authorization Header
Returns:
JSON object containing credit, debit and balance or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
else:
start = datetime.fromtimestamp(0, tz=timezone.utc)
end = request.args.get("to")
if end:
end = from_iso_format(end)
else:
end = datetime.now(tz=timezone.utc)
balance = balance_controller.get_balance(user, start, end)
logger.debug(f"Balance of {user.userid} from {start} to {end}: {balance}")
return {"credit": balance[0], "debit": balance[1], "balance": balance[2]}
@blueprint.route("/users/<userid>/balance/transactions", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_transactions(userid, current_session: Session):
"""Get transactions of user, optionally filtered
Returns also count of transactions if limit is set (e.g. just count with limit = 0)
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
GET-parameters: ``{from?: string, to?: string, limit?: int, offset?: int}``
Args:
userid: Userid of user to get transactions from
current_session: Session sent with Authorization Header
Returns:
JSON Object {transactions: Transaction[], count?: number} or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
end = request.args.get("to")
if end:
end = from_iso_format(end)
show_reversals = request.args.get("showReversals", False)
show_cancelled = request.args.get("showCancelled", True)
limit = request.args.get("limit")
offset = request.args.get("offset")
descending = request.args.get("descending", False)
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if not isinstance(show_reversals, bool):
show_reversals = str2bool(show_reversals)
if not isinstance(show_cancelled, bool):
show_cancelled = str2bool(show_cancelled)
if not isinstance(descending, bool):
descending = str2bool(descending)
except ValueError:
raise BadRequest
transactions, count = balance_controller.get_transactions(
user,
start,
end,
limit,
offset,
show_reversal=show_reversals,
show_cancelled=show_cancelled,
descending=descending,
)
logger.debug(f"transactions: {transactions}")
return {"transactions": transactions, "count": count}
@blueprint.route("/users/<userid>/balance", methods=["PUT"])
@login_required()
def change_balance(userid, current_session: Session):
"""Change balance of an user
If ``sender`` is preset in POST-data, the action is handled as a transfer from ``sender`` to user.
Route: ``/users/<userid>/balance`` | Method: ``PUT``
POST-data: ``{amount: float, sender: string}``
Args:
userid: userid identifying user to change balance
current_session: Session sent with Authorization Header
Returns:
JSON encoded transaction (201) or HTTP error
"""
data = request.get_json()
try:
amount = data["amount"]
except (TypeError, KeyError):
raise BadRequest
sender = data.get("sender", None)
user = userController.get_user(userid)
if sender:
sender = userController.get_user(sender)
if sender == user:
raise BadRequest
if (sender == current_session.user_ and sender.has_permission(permissions.SEND)) or (
sender != current_session.user_ and current_session.user_.has_permission(permissions.SEND_OTHER)
):
return HTTP.created(balance_controller.send(sender, user, amount, current_session.user_))
elif (
amount < 0
and (
(user == current_session.user_ and user.has_permission(permissions.DEBIT_OWN))
or current_session.user_.has_permission(permissions.DEBIT)
)
) or (amount > 0 and current_session.user_.has_permission(permissions.CREDIT)):
return HTTP.created(balance_controller.change_balance(user, data["amount"], current_session.user_))
raise Forbidden
@blueprint.route("/balance/<int:transaction_id>", methods=["DELETE"])
@login_required()
def reverse_transaction(transaction_id, current_session: Session):
"""Reverse a transaction
Route: ``/balance/<int:transaction_id>`` | Method: ``DELETE``
Args:
transaction_id: Identifier of the transaction
current_session: Session sent with Authorization Header
Returns:
JSON encoded reversal (transaction) (201) or HTTP error
"""
transaction = balance_controller.get_transaction(transaction_id)
if current_session.user_.has_permission(permissions.REVERSAL) or (
transaction.sender_ == current_session.user_
and (datetime.now(tz=timezone.utc) - transaction.time).total_seconds() < 10
):
reversal = balance_controller.reverse_transaction(transaction, current_session.user_)
return HTTP.created(reversal)
raise Forbidden
@blueprint.route("/balance", methods=["GET"])
@login_required(permission=permissions.SHOW_OTHER)
def get_balances(current_session: Session):
"""Get all balances
Route: ``/balance`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON Array containing credit, debit and userid for each user or HTTP error
"""
limit = request.args.get("limit", type=int)
offset = request.args.get("offset", type=int)
descending = request.args.get("descending", False, type=bool)
sortBy = request.args.get("sortBy", type=str)
_filter = request.args.get("filter", None, type=str)
logger.debug(f"request.args: {request.args}")
balances, count = balance_controller.get_balances(
limit=limit,
offset=offset,
descending=descending,
sortBy=sortBy,
_filter=_filter,
)
return jsonify(
{
"balances": [{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()],
"count": count,
}
)

View File

@ -0,0 +1,60 @@
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from werkzeug.exceptions import InternalServerError
from flaschengeist import logger
from flaschengeist.models import User
from flaschengeist.plugins import Plugin
from flaschengeist.utils.hook import HookAfter
from flaschengeist.controller import userController
from flaschengeist.controller.messageController import Message
from flaschengeist.config import config
class MailMessagePlugin(Plugin):
def load(self):
self.config = config.get("mail", None)
if self.config is None:
logger.error("mail was not configured in flaschengeist.toml")
raise InternalServerError
self.server = self.config["SERVER"]
self.port = self.config["PORT"]
self.user = self.config["USER"]
self.password = self.config["PASSWORD"]
self.crypt = self.config["CRYPT"]
self.mail = self.config["MAIL"]
@HookAfter("send_message")
def dummy_send(msg, *args, **kwargs):
logger.info(f"(dummy_send) Sending message to {msg.receiver}")
self.send_mail(msg)
def send_mail(self, msg: Message):
logger.debug(f"Sending mail to {msg.receiver} with subject {msg.subject}")
if isinstance(msg.receiver, User):
if not msg.receiver.mail:
logger.warning("Could not send Mail, mail missing: {}".format(msg.receiver))
return
recipients = [msg.receiver.mail]
else:
recipients = userController.get_user_by_role(msg.receiver)
mail = MIMEMultipart()
mail["From"] = self.mail
mail["To"] = ", ".join(recipients)
mail["Subject"] = msg.subject
mail.attach(MIMEText(msg.message))
with self.__connect() as smtp:
smtp.sendmail(self.mail, recipients, mail.as_string())
def __connect(self):
if self.crypt == "SSL":
self.smtp = smtplib.SMTP_SSL(self.server, self.port)
elif self.crypt == "STARTTLS":
self.smtp = smtplib.SMTP(self.server, self.port)
self.smtp.starttls()
else:
raise ValueError("Invalid CRYPT given")
self.smtp.login(self.user, self.password)
return self.smtp

View File

@ -0,0 +1,753 @@
"""Pricelist plugin"""
from flask import Blueprint, jsonify, request
from werkzeug.exceptions import BadRequest, Forbidden, NotFound, Unauthorized
from flaschengeist import logger
from flaschengeist.controller import userController
from flaschengeist.controller.imageController import send_image, send_thumbnail
from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required, extract_session
from flaschengeist.utils.HTTP import no_content
from . import models
from . import pricelist_controller, permissions
class PriceListPlugin(Plugin):
models = models
blueprint = Blueprint("pricelist", __name__, url_prefix="/pricelist")
def install(self):
self.install_permissions(permissions.permissions)
def load(self):
config = {"discount": 0}
config.update(config)
@PriceListPlugin.blueprint.route("/drink-types", methods=["GET"])
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"])
def get_drink_types(identifier=None):
"""Get DrinkType(s)
Route: ``/pricelist/drink-types`` | Method: ``GET``
Route: ``/pricelist/drink-types/<identifier>`` | Method: ``GET``
Args:
identifier: If querying a spicific DrinkType
Returns:
JSON encoded (list of) DrinkType(s) or HTTP-error
"""
if identifier is None:
result = pricelist_controller.get_drink_types()
else:
result = pricelist_controller.get_drink_type(identifier)
return jsonify(result)
@PriceListPlugin.blueprint.route("/drink-types", methods=["POST"])
@login_required(permission=permissions.CREATE_TYPE)
def new_drink_type(current_session):
"""Create new DrinkType
Route ``/pricelist/drink-types`` | Method: ``POST``
POST-data: ``{name: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded DrinkType or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.create_drink_type(data["name"])
return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TYPE)
def update_drink_type(identifier, current_session):
"""Modify DrinkType
Route ``/pricelist/drink-types/<identifier>`` | METHOD ``PUT``
POST-data: ``{name: string}``
Args:
identifier: Identifier of DrinkType
current_session: Session sent with Authorization Header
Returns:
JSON encoded DrinkType or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.rename_drink_type(identifier, data["name"])
return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TYPE)
def delete_drink_type(identifier, current_session):
"""Delete DrinkType
Route: ``/pricelist/drink-types/<identifier>`` | Method: ``DELETE``
Args:
identifier: Identifier of DrinkType
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_drink_type(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/tags", methods=["GET"])
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"])
def get_tags(identifier=None):
"""Get Tag(s)
Route: ``/pricelist/tags`` | Method: ``GET``
Route: ``/pricelist/tags/<identifier>`` | Method: ``GET``
Args:
identifier: Identifier of Tag
Returns:
JSON encoded (list of) Tag(s) or HTTP-error
"""
if identifier:
result = pricelist_controller.get_tag(identifier)
else:
result = pricelist_controller.get_tags()
return jsonify(result)
@PriceListPlugin.blueprint.route("/tags", methods=["POST"])
@login_required(permission=permissions.CREATE_TAG)
def new_tag(current_session):
"""Create Tag
Route: ``/pricelist/tags`` | Method: ``POST``
POST-data: ``{name: string, color: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded Tag or HTTP-error
"""
data = request.get_json()
drink_type = pricelist_controller.create_tag(data)
return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TAG)
def update_tag(identifier, current_session):
"""Modify Tag
Route: ``/pricelist/tags/<identifier>`` | Methods: ``PUT``
POST-data: ``{name: string, color: string}``
Args:
identifier: Identifier of Tag
current_session: Session sent with Authorization Header
Returns:
JSON encoded Tag or HTTP-error
"""
data = request.get_json()
tag = pricelist_controller.update_tag(identifier, data)
return jsonify(tag)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TAG)
def delete_tag(identifier, current_session):
"""Delete Tag
Route: ``/pricelist/tags/<identifier>`` | Methods: ``DELETE``
Args:
identifier: Identifier of Tag
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_tag(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/drinks", methods=["GET"])
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"])
def get_drinks(identifier=None):
"""Get Drink(s)
Route: ``/pricelist/drinks`` | Method: ``GET``
Route: ``/pricelist/drinks/<identifier>`` | Method: ``GET``
Args:
identifier: Identifier of Drink
Returns:
JSON encoded (list of) Drink(s) or HTTP-error
"""
public = True
try:
extract_session()
public = False
except Unauthorized:
public = True
if identifier:
result = pricelist_controller.get_drink(identifier, public=public)
return jsonify(result)
else:
limit = request.args.get("limit")
offset = request.args.get("offset")
search_name = request.args.get("search_name")
search_key = request.args.get("search_key")
ingredient = request.args.get("ingredient", type=bool)
receipt = request.args.get("receipt", type=bool)
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if ingredient is not None:
ingredient = bool(ingredient)
if receipt is not None:
receipt = bool(receipt)
except ValueError:
raise BadRequest
drinks, count = pricelist_controller.get_drinks(
public=public,
limit=limit,
offset=offset,
search_name=search_name,
search_key=search_key,
ingredient=ingredient,
receipt=receipt,
)
mop = drinks.copy()
logger.debug(f"GET drink {drinks}, {count}")
# return jsonify({"drinks": drinks, "count": count})
return jsonify({"drinks": drinks, "count": count})
@PriceListPlugin.blueprint.route("/list", methods=["GET"])
def get_pricelist():
"""Get Priclist
Route: ``/pricelist/list`` | Method: ``GET``
Returns:
JSON encoded list of DrinkPrices or HTTP-KeyError
"""
public = True
try:
extract_session()
public = False
except Unauthorized:
public = True
limit = request.args.get("limit")
offset = request.args.get("offset")
search_name = request.args.get("search_name")
search_key = request.args.get("search_key")
ingredient = request.args.get("ingredient", type=bool)
receipt = request.args.get("receipt", type=bool)
descending = request.args.get("descending", type=bool)
sortBy = request.args.get("sortBy")
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if ingredient is not None:
ingredient = bool(ingredient)
if receipt is not None:
receipt = bool(receipt)
if descending is not None:
descending = bool(descending)
except ValueError:
raise BadRequest
pricelist, count = pricelist_controller.get_pricelist(
public=public,
limit=limit,
offset=offset,
search_name=search_name,
search_key=search_key,
descending=descending,
sortBy=sortBy,
)
logger.debug(f"GET pricelist {pricelist}, {count}")
return jsonify({"pricelist": pricelist, "count": count})
@PriceListPlugin.blueprint.route("/drinks/search/<string:name>", methods=["GET"])
def search_drinks(name):
"""Search Drink
Route: ``/pricelist/drinks/search/<name>`` | Method: ``GET``
Args:
name: Name to search
Returns:
JSON encoded list of Drinks or HTTP-error
"""
public = True
try:
extract_session()
public = False
except Unauthorized:
public = True
return jsonify(pricelist_controller.get_drinks(name, public=public))
@PriceListPlugin.blueprint.route("/drinks", methods=["POST"])
@login_required(permission=permissions.CREATE)
def create_drink(current_session):
"""Create Drink
Route: ``/pricelist/drinks`` | Method: ``POST``
POST-data :
``{
article_id?: string
cost_per_package?: float,
cost_per_volume?: float,
name: string,
package_size?: number,
receipt?: list[string],
tags?: list[Tag],
type: DrinkType,
uuid?: string,
volume?: float,
volumes?: list[
{
ingredients?: list[{
id: int
drink_ingredient?: {
ingredient_id: int,
volume: float
},
extra_ingredient?: {
id: number,
}
}],
prices?: list[
{
price: float
public: boolean
}
],
volume: float
}
]
}``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded Drink or HTTP-error
"""
data = request.get_json()
return jsonify(pricelist_controller.set_drink(data))
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def update_drink(identifier, current_session):
"""Modify Drink
Route: ``/pricelist/drinks/<identifier>`` | Method: ``PUT``
POST-data :
``{
article_id?: string
cost_per_package?: float,
cost_per_volume?: float,
name: string,
package_size?: number,
receipt?: list[string],
tags?: list[Tag],
type: DrinkType,
uuid?: string,
volume?: float,
volumes?: list[
{
ingredients?: list[{
id: int
drink_ingredient?: {
ingredient_id: int,
volume: float
},
extra_ingredient?: {
id: number,
}
}],
prices?: list[
{
price: float
public: boolean
}
],
volume: float
}
]
}``
Args:
identifier: Identifier of Drink
current_session: Session sent with Authorization Header
Returns:
JSON encoded Drink or HTTP-error
"""
data = request.get_json()
logger.debug(f"update drink {data}")
return jsonify(pricelist_controller.update_drink(identifier, data))
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_drink(identifier, current_session):
"""Delete Drink
Route: ``/pricelist/drinks/<identifier>`` | Method: ``DELETE``
Args:
identifier: Identifier of Drink
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_drink(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_PRICE)
def delete_price(identifier, current_session):
"""Delete Price
Route: ``/pricelist/prices/<identifier>`` | Methods: ``DELETE``
Args:
identifier: Identiefer of Price
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_price(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_VOLUME)
def delete_volume(identifier, current_session):
"""Delete DrinkPriceVolume
Route: ``/pricelist/volumes/<identifier>`` | Method: ``DELETE``
Args:
identifier: Identifier of DrinkPriceVolume
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_volume(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"])
@login_required()
def get_extra_ingredients(current_session):
"""Get ExtraIngredients
Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of ExtraIngredients or HTTP-error
"""
return jsonify(pricelist_controller.get_extra_ingredients())
@PriceListPlugin.blueprint.route("/ingredients/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_INGREDIENTS_DRINK)
def delete_ingredient(identifier, current_session):
"""Delete Ingredient
Route: ``/pricelist/ingredients/<identifier>`` | Method: ``DELETE``
Args:
identifier: Identifier of Ingredient
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_ingredient(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"])
@login_required(permission=permissions.EDIT_INGREDIENTS)
def set_extra_ingredient(current_session):
"""Create ExtraIngredient
Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``POST``
POST-data: ``{ name: string, price: float }``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded ExtraIngredient or HTTP-error
"""
data = request.get_json()
return jsonify(pricelist_controller.set_extra_ingredient(data))
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_INGREDIENTS)
def update_extra_ingredient(identifier, current_session):
"""Modify ExtraIngredient
Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``PUT``
POST-data: ``{ name: string, price: float }``
Args:
identifier: Identifier of ExtraIngredient
current_session: Session sent with Authorization Header
Returns:
JSON encoded ExtraIngredient or HTTP-error
"""
data = request.get_json()
return jsonify(pricelist_controller.update_extra_ingredient(identifier, data))
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_INGREDIENTS)
def delete_extra_ingredient(identifier, current_session):
"""Delete ExtraIngredient
Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``DELETE``
Args:
identifier: Identifier of ExtraIngredient
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
pricelist_controller.delete_extra_ingredient(identifier)
return no_content()
@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["GET"])
@login_required()
def get_pricelist_settings_min_prices(current_session):
"""Get MinPrices
Route: ``/pricelist/settings/min_prices`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of MinPrices
"""
# TODO: Handle if no prices are set!
try:
min_prices = PriceListPlugin.plugin.get_setting("min_prices")
except KeyError:
min_prices = []
return jsonify(min_prices)
@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST"])
@login_required(permission=permissions.EDIT_MIN_PRICES)
def post_pricelist_settings_min_prices(current_session):
"""Create MinPrices
Route: ``/pricelist/settings/min_prices`` | Method: ``POST``
POST-data: ``list[int]``
Args:
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, int) for n in data):
raise BadRequest
data.sort()
PriceListPlugin.plugin.set_setting("min_prices", data)
return no_content()
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
@login_required()
def get_columns(userid, current_session):
"""Get pricecalc_columns of an user
Route: ``/users/<userid>/pricelist/pricecac_columns`` | Method: ``GET`` or ``PUT``
POST-data: On ``PUT`` json encoded array of floats
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
GET: JSON object containing the shortcuts as float array or HTTP error
PUT: HTTP-created or HTTP error
"""
if userid != current_session.user_.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("pricecalc_columns", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, str) for n in data):
raise BadRequest
data.sort(reverse=True)
user.set_attribute("pricecalc_columns", data)
userController.persist()
return no_content()
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns_order", methods=["GET", "PUT"])
@login_required()
def get_columns_order(userid, current_session):
"""Get pricecalc_columns_order of an user
Route: ``/users/<userid>/pricelist/pricecac_columns_order`` | Method: ``GET`` or ``PUT``
POST-data: On ``PUT`` json encoded array of floats
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
GET: JSON object containing the shortcuts as object array or HTTP error
PUT: HTTP-created or HTTP error
"""
if userid != current_session.user_.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("pricecalc_columns_order", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, str) for mop in data for n in mop.values()):
raise BadRequest
user.set_attribute("pricecalc_columns_order", data)
userController.persist()
return no_content()
@PriceListPlugin.blueprint.route("/users/<userid>/pricelist", methods=["GET", "PUT"])
@login_required()
def get_priclist_setting(userid, current_session):
"""Get pricelistsetting of an user
Route: ``/pricelist/user/<userid>/pricelist`` | Method: ``GET`` or ``PUT``
POST-data: on ``PUT`` ``{value: boolean}``
Args:
userid: Userid identifying the user
current_session: Session sent wth Authorization Header
Returns:
GET: JSON object containing the value as boolean or HTTP-error
PUT: HTTP-NoContent or HTTP-error
"""
if userid != current_session.user_.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("pricelist_view", {"value": False}))
else:
data = request.get_json()
if not isinstance(data, dict) or not "value" in data or not isinstance(data["value"], bool):
raise BadRequest
user.set_attribute("pricelist_view", data)
userController.persist()
return no_content()
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>/picture", methods=["POST", "DELETE"])
@login_required(permission=permissions.EDIT)
def set_picture(identifier, current_session):
"""Get, Create, Delete Drink Picture
Route: ``/pricelist/<identifier>/picture`` | Method: ``GET,POST,DELETE``
POST-data: (if remaining) ``Form-Data: mime: 'image/*'``
Args:
identifier: Identifier of Drink
current_session: Session sent with Authorization
Returns:
Picture or HTTP-error
"""
if request.method == "DELETE":
pricelist_controller.delete_drink_picture(identifier)
return no_content()
file = request.files.get("file")
if file:
return jsonify(pricelist_controller.save_drink_picture(identifier, file))
else:
raise BadRequest
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>/picture", methods=["GET"])
# @headers({"Cache-Control": "private, must-revalidate"})
def _get_picture(identifier):
"""Get Picture
Args:
identifier: Identifier of Drink
Returns:
Picture or HTTP-error
"""
drink = pricelist_controller.get_drink(identifier)
if drink.has_image:
if request.args.get("thumbnail"):
return send_thumbnail(image=drink.image_)
return send_image(image=drink.image_)
raise NotFound

View File

@ -0,0 +1,141 @@
"""pricelist: initial
Revision ID: 58ab9b6a8839
Revises:
Create Date: 2022-02-23 14:45:30.563647
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
# revision identifiers, used by Alembic.
revision = "58ab9b6a8839"
down_revision = None
branch_labels = ("pricelist",)
depends_on = "flaschengeist"
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"drink_extra_ingredient",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("price", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_extra_ingredient")),
sa.UniqueConstraint("name", name=op.f("uq_drink_extra_ingredient_name")),
)
op.create_table(
"drink_tag",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("color", sa.String(length=7), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_tag")),
sa.UniqueConstraint("name", name=op.f("uq_drink_tag_name")),
)
op.create_table(
"drink_type",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_type")),
sa.UniqueConstraint("name", name=op.f("uq_drink_type_name")),
)
op.create_table(
"drink",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("article_id", sa.String(length=64), nullable=True),
sa.Column("package_size", sa.Integer(), nullable=True),
sa.Column("name", sa.String(length=60), nullable=False),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.Column("cost_per_volume", sa.Numeric(precision=5, scale=3, asdecimal=False), nullable=True),
sa.Column("cost_per_package", sa.Numeric(precision=5, scale=3, asdecimal=False), nullable=True),
sa.Column("receipt", sa.PickleType(), nullable=True),
sa.Column("type_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("image_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["image_id"], ["image.id"], name=op.f("fk_drink_image_id_image")),
sa.ForeignKeyConstraint(["type_id"], ["drink_type.id"], name=op.f("fk_drink_type_id_drink_type")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink")),
)
op.create_table(
"drink_ingredient",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=False),
sa.Column("ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["ingredient_id"], ["drink.id"], name=op.f("fk_drink_ingredient_ingredient_id_drink")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_ingredient")),
)
op.create_table(
"drink_price_volume",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_price_volume_drink_id_drink")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_price_volume")),
)
op.create_table(
"drink_x_tag",
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("tag_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_x_tag_drink_id_drink")),
sa.ForeignKeyConstraint(["tag_id"], ["drink_tag.id"], name=op.f("fk_drink_x_tag_tag_id_drink_tag")),
)
op.create_table(
"drink_x_type",
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("type_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_x_type_drink_id_drink")),
sa.ForeignKeyConstraint(["type_id"], ["drink_type.id"], name=op.f("fk_drink_x_type_type_id_drink_type")),
)
op.create_table(
"drink_ingredient_association",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("volume_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("_drink_ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("_extra_ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(
["_drink_ingredient_id"],
["drink_ingredient.id"],
name=op.f("fk_drink_ingredient_association__drink_ingredient_id_drink_ingredient"),
),
sa.ForeignKeyConstraint(
["_extra_ingredient_id"],
["drink_extra_ingredient.id"],
name=op.f("fk_drink_ingredient_association__extra_ingredient_id_drink_extra_ingredient"),
),
sa.ForeignKeyConstraint(
["volume_id"],
["drink_price_volume.id"],
name=op.f("fk_drink_ingredient_association_volume_id_drink_price_volume"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_ingredient_association")),
)
op.create_table(
"drink_price",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("price", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.Column("volume_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("public", sa.Boolean(), nullable=True),
sa.Column("description", sa.String(length=30), nullable=True),
sa.ForeignKeyConstraint(
["volume_id"], ["drink_price_volume.id"], name=op.f("fk_drink_price_volume_id_drink_price_volume")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_price")),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("drink_price")
op.drop_table("drink_ingredient_association")
op.drop_table("drink_x_type")
op.drop_table("drink_x_tag")
op.drop_table("drink_price_volume")
op.drop_table("drink_ingredient")
op.drop_table("drink")
op.drop_table("drink_type")
op.drop_table("drink_tag")
op.drop_table("drink_extra_ingredient")
# ### end Alembic commands ###

View File

@ -0,0 +1,180 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Optional
from flaschengeist.database import db
from flaschengeist.database.types import ModelSerializeMixin, Serial
from flaschengeist.models import Image
drink_tag_association = db.Table(
"drink_x_tag",
db.Column("drink_id", Serial, db.ForeignKey("drink.id")),
db.Column("tag_id", Serial, db.ForeignKey("drink_tag.id")),
)
drink_type_association = db.Table(
"drink_x_type",
db.Column("drink_id", Serial, db.ForeignKey("drink.id")),
db.Column("type_id", Serial, db.ForeignKey("drink_type.id")),
)
class Tag(db.Model, ModelSerializeMixin):
"""
Tag
"""
__tablename__ = "drink_tag"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
color: str = db.Column(db.String(7), nullable=False)
class DrinkType(db.Model, ModelSerializeMixin):
"""
DrinkType
"""
__tablename__ = "drink_type"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
class DrinkPrice(db.Model, ModelSerializeMixin):
"""
PriceFromVolume
"""
__tablename__ = "drink_price"
id: int = db.Column("id", Serial, primary_key=True)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
volume_id_ = db.Column("volume_id", Serial, db.ForeignKey("drink_price_volume.id"))
volume: "DrinkPriceVolume" = None
_volume: "DrinkPriceVolume" = db.relationship("DrinkPriceVolume", back_populates="_prices", join_depth=1)
public: bool = db.Column(db.Boolean, default=True)
description: Optional[str] = db.Column(db.String(30))
def __repr__(self):
return f"DrinkPric({self.id},{self.price},{self.public},{self.description})"
class ExtraIngredient(db.Model, ModelSerializeMixin):
"""
ExtraIngredient
"""
__tablename__ = "drink_extra_ingredient"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(30), unique=True, nullable=False)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
class DrinkIngredient(db.Model, ModelSerializeMixin):
"""
Drink Ingredient
"""
__tablename__ = "drink_ingredient"
id: int = db.Column("id", Serial, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
ingredient_id: int = db.Column(Serial, db.ForeignKey("drink.id"))
cost_per_volume: float
name: str
_drink_ingredient: Drink = db.relationship("Drink")
@property
def cost_per_volume(self):
return self._drink_ingredient.cost_per_volume if self._drink_ingredient else None
@property
def name(self):
return self._drink_ingredient.name if self._drink_ingredient else None
class Ingredient(db.Model, ModelSerializeMixin):
"""
Ingredient Associationtable
"""
__tablename__ = "drink_ingredient_association"
id: int = db.Column("id", Serial, primary_key=True)
volume_id = db.Column(Serial, db.ForeignKey("drink_price_volume.id"))
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient, cascade="all,delete")
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
_drink_ingredient_id = db.Column(Serial, db.ForeignKey("drink_ingredient.id"))
_extra_ingredient_id = db.Column(Serial, db.ForeignKey("drink_extra_ingredient.id"))
class MinPrices(ModelSerializeMixin):
"""
MinPrices
"""
percentage: float
price: float
class DrinkPriceVolume(db.Model, ModelSerializeMixin):
"""
Drink Volumes and Prices
"""
__tablename__ = "drink_price_volume"
id: int = db.Column("id", Serial, primary_key=True)
drink_id = db.Column(Serial, db.ForeignKey("drink.id"))
drink: "Drink" = None
_drink: "Drink" = db.relationship("Drink", back_populates="_volumes")
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
min_prices: list[MinPrices] = []
# ingredients: list[Ingredient] = []
prices: list[DrinkPrice] = []
_prices: list[DrinkPrice] = db.relationship(
DrinkPrice, back_populates="_volume", cascade="all,delete,delete-orphan"
)
ingredients: list[Ingredient] = db.relationship(
"Ingredient",
foreign_keys=Ingredient.volume_id,
cascade="all,delete,delete-orphan",
)
def __repr__(self):
return f"DrinkPriceVolume({self.id},{self.drink_id},{self.volume},{self.prices})"
class Drink(db.Model, ModelSerializeMixin):
"""
DrinkPrice
"""
__tablename__ = "drink"
id: int = db.Column("id", Serial, primary_key=True)
article_id: Optional[str] = db.Column(db.String(64))
package_size: Optional[int] = db.Column(db.Integer)
name: str = db.Column(db.String(60), nullable=False)
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
cost_per_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
has_image: bool = False
receipt: Optional[list[str]] = db.Column(db.PickleType(protocol=4))
_type_id = db.Column("type_id", Serial, db.ForeignKey("drink_type.id"))
_image_id = db.Column("image_id", Serial, db.ForeignKey("image.id"))
image_: Image = db.relationship("Image", cascade="all, delete", foreign_keys=[_image_id])
tags: Optional[list[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge")
type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id])
volumes: list[DrinkPriceVolume] = []
_volumes: list[DrinkPriceVolume] = db.relationship(
DrinkPriceVolume, back_populates="_drink", cascade="all,delete,delete-orphan"
)
def __repr__(self):
return f"Drink({self.id},{self.name},{self.volumes})"
@property
def has_image(self):
return self.image_ is not None

View File

@ -0,0 +1,37 @@
CREATE = "drink_create"
"""Can create drinks"""
EDIT = "drink_edit"
"""Can edit drinks"""
DELETE = "drink_delete"
"""Can delete drinks"""
CREATE_TAG = "drink_tag_create"
"""Can create and edit Tags"""
EDIT_PRICE = "edit_price"
DELETE_PRICE = "delete_price"
EDIT_VOLUME = "edit_volume"
DELETE_VOLUME = "delete_volume"
EDIT_INGREDIENTS_DRINK = "edit_ingredients_drink"
DELETE_INGREDIENTS_DRINK = "delete_ingredients_drink"
EDIT_INGREDIENTS = "edit_ingredients"
DELETE_INGREDIENTS = "delete_ingredients"
EDIT_TAG = "drink_tag_edit"
DELETE_TAG = "drink_tag_delete"
CREATE_TYPE = "drink_type_create"
EDIT_TYPE = "drink_type_edit"
DELETE_TYPE = "drink_type_delete"
EDIT_MIN_PRICES = "edit_min_prices"
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -0,0 +1,540 @@
from werkzeug.exceptions import BadRequest, NotFound
from sqlalchemy.exc import IntegrityError
from flaschengeist import logger
from flaschengeist.database import db
from flaschengeist.utils.decorators import extract_session
from .models import (
Drink,
DrinkPrice,
Ingredient,
Tag,
DrinkType,
DrinkPriceVolume,
DrinkIngredient,
ExtraIngredient,
)
from .permissions import EDIT_VOLUME, EDIT_PRICE, EDIT_INGREDIENTS_DRINK
import flaschengeist.controller.imageController as image_controller
def update():
db.session.commit()
def get_tags():
return Tag.query.all()
def get_tag(identifier):
if isinstance(identifier, int):
ret = Tag.query.get(identifier)
elif isinstance(identifier, str):
ret = Tag.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for Tag")
raise BadRequest
if ret is None:
raise NotFound
return ret
def create_tag(data):
try:
if "id" in data:
data.pop("id")
allowed_keys = Tag().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
tag = Tag(**values)
db.session.add(tag)
update()
return tag
except IntegrityError:
raise BadRequest("Name already exists")
def update_tag(identifier, data):
tag = get_tag(identifier)
allowed_keys = Tag().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
for key, value in values.items():
setattr(tag, key, value)
try:
update()
except IntegrityError:
raise BadRequest("Name already exists")
def delete_tag(identifier):
tag = get_tag(identifier)
db.session.delete(tag)
try:
update()
except IntegrityError:
raise BadRequest("Tag still in use")
def get_drink_types():
return DrinkType.query.all()
def get_drink_type(identifier):
if isinstance(identifier, int):
ret = DrinkType.query.get(identifier)
elif isinstance(identifier, str):
ret = DrinkType.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for DrinkType")
raise BadRequest
if ret is None:
raise NotFound
return ret
def create_drink_type(name):
try:
drink_type = DrinkType(name=name)
db.session.add(drink_type)
update()
return drink_type
except IntegrityError:
raise BadRequest("Name already exists")
def rename_drink_type(identifier, new_name):
drink_type = get_drink_type(identifier)
drink_type.name = new_name
try:
update()
except IntegrityError:
raise BadRequest("Name already exists")
return drink_type
def delete_drink_type(identifier):
drink_type = get_drink_type(identifier)
db.session.delete(drink_type)
try:
update()
except IntegrityError:
raise BadRequest("DrinkType still in use")
def _create_public_drink(drink):
_volumes = []
for volume in drink.volumes:
_prices = []
for price in volume.prices:
price: DrinkPrice
if price.public:
_prices.append(price)
volume.prices = _prices
if len(volume.prices) > 0:
_volumes.append(volume)
drink.volumes = _volumes
if len(drink.volumes) > 0:
return drink
return None
def get_drinks(
name=None,
public=False,
limit=None,
offset=None,
search_name=None,
search_key=None,
ingredient=False,
receipt=None,
):
count = None
if name:
query = Drink.query.filter(Drink.name.contains(name))
else:
query = Drink.query
if ingredient:
query = query.filter(Drink.cost_per_volume >= 0)
if receipt:
query = query.filter(Drink._volumes.any(DrinkPriceVolume.ingredients != None))
if public:
query = query.filter(Drink._volumes.any(DrinkPriceVolume._prices.any(DrinkPrice.public)))
if search_name:
if search_key == "name":
query = query.filter(Drink.name.contains(search_name))
elif search_key == "article_id":
query = query.filter(Drink.article_id.contains(search_name))
elif search_key == "drink_type":
query = query.filter(Drink.type.has(DrinkType.name.contains(search_name)))
elif search_key == "tags":
query = query.filter(Drink.tags.any(Tag.name.contains(search_name)))
else:
query = query.filter(
(Drink.name.contains(search_name))
| (Drink.article_id.contains(search_name))
| (Drink.type.has(DrinkType.name.contains(search_name)))
| (Drink.tags.any(Tag.name.contains(search_name)))
)
query = query.order_by(Drink.name.asc())
if limit is not None:
count = query.count()
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
drinks = query.all()
for drink in drinks:
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drinks, count
def get_pricelist(
public=False,
limit=None,
offset=None,
search_name=None,
search_key=None,
sortBy=None,
descending=False,
):
count = None
query = DrinkPrice.query
if public:
query = query.filter(DrinkPrice.public)
if search_name:
if search_key == "name":
query = query.filter(DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.name.contains(search_name))))
if search_key == "type":
query = query.filter(
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.type.has(DrinkType.name.contains(search_name)))
)
)
if search_key == "tags":
query = query.filter(
DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.tags.any(Tag.name.conaitns(search_name))))
)
if search_key == "volume":
query = query.filter(DrinkPrice._volume.has(DrinkPriceVolume.volume == float(search_name)))
if search_key == "price":
query = query.filter(DrinkPrice.price == float(search_name))
if search_key == "description":
query = query.filter(DrinkPrice.description.contains(search_name))
else:
try:
search_name = float(search_name)
query = query.filter(
(DrinkPrice._volume.has(DrinkPriceVolume.volume == float(search_name)))
| (DrinkPrice.price == float(search_name))
)
except:
query = query.filter(
(DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.name.contains(search_name))))
| (
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.type.has(DrinkType.name.contains(search_name)))
)
)
| (
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.tags.any(Tag.name.contains(search_name)))
)
)
| (DrinkPrice.description.contains(search_name))
)
if sortBy == "type":
query = (
query.join(DrinkPrice._volume)
.join(DrinkPriceVolume._drink)
.join(Drink.type)
.order_by(DrinkType.name.desc() if descending else DrinkType.name.asc())
)
elif sortBy == "volume":
query = query.join(DrinkPrice._volume).order_by(
DrinkPriceVolume.volume.desc() if descending else DrinkPriceVolume.volume.asc()
)
elif sortBy == "price":
query = query.order_by(DrinkPrice.price.desc() if descending else DrinkPrice.price.asc())
else:
query = (
query.join(DrinkPrice._volume)
.join(DrinkPriceVolume._drink)
.order_by(Drink.name.desc() if descending else Drink.name.asc())
)
if limit is not None:
count = query.count()
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
prices = query.all()
for price in prices:
price._volume.drink = price._volume._drink
price.volume = price._volume
return prices, count
def get_drink(identifier, public=False):
drink = None
if isinstance(identifier, int):
drink = Drink.query.get(identifier)
elif isinstance(identifier, str):
drink = Drink.query.filter(Tag.name == identifier).one_or_none()
else:
raise BadRequest("Invalid identifier type for Drink")
if drink is None:
raise NotFound
if public:
return _create_public_drink(drink)
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drink
def set_drink(data):
return update_drink(-1, data)
def update_drink(identifier, data):
try:
session = extract_session()
if "id" in data:
data.pop("id")
volumes = data.pop("volumes") if "volumes" in data else None
tags = []
if "tags" in data:
_tags = data.pop("tags")
if isinstance(_tags, list):
for _tag in _tags:
if isinstance(_tag, dict) and "id" in _tag:
tags.append(get_tag(_tag["id"]))
drink_type = data.pop("type")
if isinstance(drink_type, dict) and "id" in drink_type:
drink_type = drink_type["id"]
drink_type = get_drink_type(drink_type)
if identifier == -1:
drink = Drink()
db.session.add(drink)
else:
drink = get_drink(identifier)
for key, value in data.items():
if hasattr(drink, key) and key != "has_image":
setattr(drink, key, value if value != "" else None)
if drink_type:
drink.type = drink_type
if volumes is not None and session.user_.has_permission(EDIT_VOLUME):
drink._volumes = []
drink._volumes = set_volumes(volumes)
if len(tags) > 0:
drink.tags = tags
db.session.commit()
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drink
except (NotFound, KeyError):
raise BadRequest
def set_volumes(volumes):
retVal = []
if not isinstance(volumes, list):
raise BadRequest
for volume in volumes:
retVal.append(set_volume(volume))
return retVal
def delete_drink(identifier):
drink = get_drink(identifier)
db.session.delete(drink)
db.session.commit()
def get_volume(identifier):
return DrinkPriceVolume.query.get(identifier)
def get_volumes(drink_id=None):
if drink_id:
return DrinkPriceVolume.query.filter(DrinkPriceVolume.drink_id == drink_id).all()
return DrinkPriceVolume.query.all()
def set_volume(data):
session = extract_session()
allowed_keys = DrinkPriceVolume().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
prices = None
ingredients = None
if "prices" in values:
prices = values.pop("prices")
if "ingredients" in values:
ingredients = values.pop("ingredients")
values.pop("id", None)
volume = DrinkPriceVolume(**values)
db.session.add(volume)
if prices and session.user_.has_permission(EDIT_PRICE):
set_prices(prices, volume)
if ingredients and session.user_.has_permission(EDIT_INGREDIENTS_DRINK):
set_ingredients(ingredients, volume)
return volume
def set_prices(prices, volume):
if isinstance(prices, list):
_prices = []
for _price in prices:
price = set_price(_price)
_prices.append(price)
volume._prices = _prices
def set_ingredients(ingredients, volume):
if isinstance(ingredients, list):
_ingredietns = []
for _ingredient in ingredients:
ingredient = set_ingredient(_ingredient)
_ingredietns.append(ingredient)
volume.ingredients = _ingredietns
def delete_volume(identifier):
volume = get_volume(identifier)
db.session.delete(volume)
db.session.commit()
def get_price(identifier):
if isinstance(identifier, int):
return DrinkPrice.query.get(identifier)
raise NotFound
def get_prices(volume_id=None):
if volume_id:
return DrinkPrice.query.filter(DrinkPrice.volume_id_ == volume_id).all()
return DrinkPrice.query.all()
def set_price(data):
allowed_keys = list(DrinkPrice().serialize().keys())
allowed_keys.append("description")
logger.debug(f"allowed_key {allowed_keys}")
values = {key: value for key, value in data.items() if key in allowed_keys}
values.pop("id", -1)
price = DrinkPrice(**values)
db.session.add(price)
return price
def delete_price(identifier):
price = get_price(identifier)
db.session.delete(price)
db.session.commit()
def set_drink_ingredient(data):
allowed_keys = DrinkIngredient().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
if "cost_per_volume" in values:
values.pop("cost_per_volume")
if "name" in values:
values.pop("name")
values.pop("id", -1)
drink_ingredient = DrinkIngredient(**values)
db.session.add(drink_ingredient)
return drink_ingredient
def get_ingredient(identifier):
return Ingredient.query.get(identifier)
def set_ingredient(data):
drink_ingredient_value = None
extra_ingredient_value = None
if "drink_ingredient" in data:
drink_ingredient_value = data.pop("drink_ingredient")
if "extra_ingredient" in data:
extra_ingredient_value = data.pop("extra_ingredient")
data.pop("id", -1)
ingredient = Ingredient(**data)
db.session.add(ingredient)
if drink_ingredient_value:
ingredient.drink_ingredient = set_drink_ingredient(drink_ingredient_value)
if extra_ingredient_value:
if "id" in extra_ingredient_value:
ingredient.extra_ingredient = get_extra_ingredient(extra_ingredient_value.get("id"))
return ingredient
def delete_ingredient(identifier):
ingredient = get_ingredient(identifier)
if ingredient.drink_ingredient:
db.session.delete(ingredient.drink_ingredient)
db.session.delete(ingredient)
db.session.commit()
def get_extra_ingredients():
return ExtraIngredient.query.all()
def get_extra_ingredient(identifier):
return ExtraIngredient.query.get(identifier)
def set_extra_ingredient(data):
allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = ExtraIngredient(**values)
db.session.add(extra_ingredient)
db.session.commit()
return extra_ingredient
def update_extra_ingredient(identifier, data):
allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = get_extra_ingredient(identifier)
if extra_ingredient:
for key, value in values.items():
setattr(extra_ingredient, key, value)
db.session.commit()
return extra_ingredient
def delete_extra_ingredient(identifier):
extra_ingredient = get_extra_ingredient(identifier)
db.session.delete(extra_ingredient)
db.session.commit()
def save_drink_picture(identifier, file):
drink = delete_drink_picture(identifier)
drink.image_ = image_controller.upload_image(file)
db.session.commit()
return drink
def delete_drink_picture(identifier):
drink = get_drink(identifier)
if drink.image_:
db.session.delete(drink.image_)
drink.image_ = None
db.session.commit()
return drink

View File

@ -0,0 +1,141 @@
"""Roles plugin
Provides routes used to configure roles and permissions of users / roles.
"""
from werkzeug.exceptions import BadRequest
from flask import Blueprint, request, jsonify
from flaschengeist.plugins import Plugin
from flaschengeist.controller import roleController
from flaschengeist.utils.HTTP import created, no_content
from flaschengeist.utils.decorators import login_required
from . import permissions
class RolesPlugin(Plugin):
blueprint = Blueprint("roles", __name__)
def install(self):
self.install_permissions(permissions.permissions)
@RolesPlugin.blueprint.route("/roles", methods=["GET"])
@login_required()
def list_roles(current_session):
"""List all existing roles
Route: ``/roles`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded array of `flaschengeist.models.user.Role`
"""
roles = roleController.get_all()
return jsonify(roles)
@RolesPlugin.blueprint.route("/roles", methods=["POST"])
@login_required(permission=permissions.EDIT)
def create_role(current_session):
"""Create new role
Route: ``/roles`` | Method: ``POST``
POST-data: ``{name: string, permissions?: string[]}``
Args:
current_session: Session sent with Authorization Header
Returns:
HTTP-201 and json encoded created Role or HTTP error
"""
data = request.get_json()
if not data or "name" not in data:
raise BadRequest
if "permissions" in data:
permissions = data["permissions"]
return created(roleController.create_role(data["name"], permissions))
@RolesPlugin.blueprint.route("/roles/permissions", methods=["GET"])
@login_required()
def list_permissions(current_session):
"""List all existing permissions
Route: ``/roles/permissions`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of `flaschengeist.models.user.Permission`
"""
permissions = roleController.get_permissions()
return jsonify(permissions)
@RolesPlugin.blueprint.route("/roles/<role_name>", methods=["GET"])
@login_required()
def get_role(role_name, current_session):
"""Get role by name
Route: ``/roles/<role_name>`` | Method: ``GET``
Args:
role_name: Name of role to retrieve
current_session: Session sent with Authorization Header
Returns:
JSON encoded `flaschengeist.models.user.Role` or HTTP error
"""
role = roleController.get(role_name)
return jsonify(role)
@RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def edit_role(role_id, current_session):
"""Edit role, rename and / or set permissions
Route: ``/roles/<role_id>`` | Method: ``PUT``
POST-data: ``{name?: string, permissions?: string[]}``
Args:
role_id: Identifier of the role
current_session: Session sent with Authorization Header
Returns:
HTTP-200 or HTTP error
"""
role = roleController.get(role_id)
data = request.get_json()
if "permissions" in data:
roleController.set_permissions(role, data["permissions"])
if "name" in data and data["name"] != role.name:
roleController.update_role(role, data["name"])
return no_content()
@RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_role(role_id, current_session):
"""Delete role
Route: ``/roles/<role_id>`` | Method: ``DELETE``
Args:
role_id: Identifier of the role
current_session: Session sent with Authorization Header
Returns:
HTTP-204 or HTTP error (HTTP-409 Conflict if role still in use)
"""
role = roleController.get(role_id)
roleController.delete(role)
return no_content()

View File

@ -0,0 +1,7 @@
EDIT = "roles_edit"
"""Can edit roles, assign permissions to roles and change names"""
DELETE = "roles_delete"
"""Can delete roles"""
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -0,0 +1,86 @@
from flask import Blueprint
from datetime import datetime, timedelta
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.plugins import Plugin
from flaschengeist.utils.HTTP import no_content
class __Task:
def __init__(self, function, **kwags):
self.function = function
self.interval = timedelta(**kwags)
_scheduled_tasks: dict[__Task] = dict()
def add_scheduled(id: str, function, replace=False, **kwargs):
if id not in _scheduled_tasks or replace:
_scheduled_tasks[id] = __Task(function, **kwargs)
logger.info(f"Registered task: {id}")
else:
logger.debug(f"Skipping already registered task: {id}")
def scheduled(id: str, replace=False, **kwargs):
"""
kwargs: days, hours, minutes
"""
def real_decorator(function):
add_scheduled(id, function, replace, **kwargs)
return function
if not isinstance(id, str):
raise TypeError
return real_decorator
class SchedulerPlugin(Plugin):
blueprint = Blueprint("scheduler", __name__)
def load(self):
def __view_func():
self.run_tasks()
return no_content()
def __passiv_func(v):
try:
self.run_tasks()
except:
logger.error("Error while executing scheduled tasks!", exc_info=True)
cron = config.get("scheduler", {}).get("cron", "passive_web").lower()
if cron == "passive_web":
self.blueprint.teardown_app_request(__passiv_func)
elif cron == "active_web":
self.blueprint.add_url_rule("/cron", view_func=__view_func)
def run_tasks(self):
from ..database import db
self = db.session.merge(self)
changed = False
now = datetime.now()
status = self.get_setting("status", default=dict())
for id, task in _scheduled_tasks.items():
last_run = status.setdefault(id, now)
if last_run + task.interval <= now:
logger.debug(
f"Run task {id}, was scheduled for {last_run + task.interval}, next iteration: {now + task.interval}"
)
task.function()
changed = True
else:
logger.debug(f"Skip task {id}, is scheduled for {last_run + task.interval}")
if changed:
# Remove not registered tasks
for id in status.keys():
if id not in _scheduled_tasks.keys():
del status[id]
self.set_setting("status", status)

View File

@ -0,0 +1,262 @@
"""Users plugin
Provides routes used to manage users
"""
from http.client import CREATED
from flask import Blueprint, request, jsonify, make_response, after_this_request, Response
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed
from datetime import datetime
from . import permissions
from flaschengeist import logger
from flaschengeist.config import config
from flaschengeist.plugins import Plugin
from flaschengeist.models import User
from flaschengeist.utils.decorators import login_required, extract_session, headers
from flaschengeist.controller import userController
from flaschengeist.utils.HTTP import created, no_content
from flaschengeist.utils.datetime import from_iso_format
class UsersPlugin(Plugin):
blueprint = Blueprint("users", __name__)
def install(self):
self.install_permissions(permissions.permissions)
@UsersPlugin.blueprint.route("/users", methods=["POST"])
def register():
"""Register a new user
The password will be set to a random string of at lease 16byte entropy.
The user will receive a mail containing a link to set their own password.
Route: ``/users`` | Method: ``POST``
POST-data: Same as `flaschengeist.models.user.User`
Returns:
JSON encoded `flaschengeist.models.user.User` or HTTP error
"""
registration = config["users"].get("registration", False)
if not registration or registration not in ["managed", "public"]:
logger.debug("Config for Registration is set to >{}<".format(registration))
raise MethodNotAllowed
if registration == "managed":
extract_session(permissions.REGISTER)
data = request.get_json()
if not data:
raise BadRequest
for required in ["firstname", "lastname", "mail"]:
if required not in data:
raise BadRequest("Missing required parameters")
logger.debug("Register new User...")
return make_response(jsonify(userController.register(data)), CREATED)
@UsersPlugin.blueprint.route("/users", methods=["GET"])
@login_required()
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
def list_users(current_session):
"""List all existing users
Route: ``/users`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded array of `flaschengeist.models.user.User` or HTTP error
"""
logger.debug("Retrieve list of all users")
users = userController.get_users()
return jsonify(users)
@UsersPlugin.blueprint.route("/users/<userid>", methods=["GET"])
@login_required()
@headers({"Cache-Control": "private, must-revalidate, max-age=300"})
def get_user(userid, current_session):
"""Retrieve user by userid
Route: ``/users/<userid>`` | Method: ``GET``
Args:
userid: UserID of user to retrieve
current_session: Session sent with Authorization Header
Returns:
JSON encoded `flaschengeist.models.user.User` or if userid is current user also containing permissions or HTTP error
"""
logger.debug("Get information of user {{ {} }}".format(userid))
user: User = userController.get_user(
userid, True
) # This is the only API point that should return data for deleted users
serial = user.serialize()
if userid == current_session.user_.userid:
serial["permissions"] = user.get_permissions()
return jsonify(serial)
@UsersPlugin.blueprint.route("/users/<userid>/frontend", methods=["POST", "GET"])
@login_required()
def frontend(userid, current_session):
if current_session.user_.userid != userid:
raise Forbidden
if request.method == "POST":
if request.content_length > 1024**2:
raise BadRequest
current_session.user_.set_attribute("frontend", request.get_json())
return no_content()
else:
content = current_session.user_.get_attribute("frontend", None)
if content is None:
return no_content()
return jsonify(content)
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"])
@headers({"Cache-Control": "public, must-revalidate, max-age=10"})
def get_avatar(userid):
etag = None
if "If-None-Match" in request.headers:
etag = request.headers["If-None-Match"]
user = userController.get_user(userid)
return userController.load_avatar(user, etag)
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"])
@login_required()
def set_avatar(userid, current_session):
user = userController.get_user(userid)
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.EDIT):
raise Forbidden
file = request.files.get("file")
if file:
userController.save_avatar(user, file)
return created()
else:
raise BadRequest
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["DELETE"])
@login_required()
def delete_avatar(userid, current_session):
user = userController.get_user(userid)
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.EDIT):
raise Forbidden
userController.delete_avatar(user)
return no_content()
@UsersPlugin.blueprint.route("/users/<userid>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_user(userid, current_session):
"""Delete user by userid
Route: ``/users/<userid>`` | Method: ``DELETE``
Args:
userid: UserID of user to retrieve
current_session: Session sent with Authorization Header
Returns:
HTTP-204 or HTTP error
"""
logger.debug("Delete user {{ {} }}".format(userid))
user = userController.get_user(userid)
userController.delete_user(user)
return no_content()
@UsersPlugin.blueprint.route("/users/<userid>", methods=["PUT"])
@login_required()
def edit_user(userid, current_session):
"""Modify user by userid
Route: ``/users/<userid>`` | Method: ``PUT``
POST-data: ```{firstname?: string, lastname?: string, display_name?: string, mail?: string,
password?: string, roles?: string[]}```
Args:
userid: UserID of user to retrieve
current_session: Session sent with Authorization Header
Returns:
HTTP-204 or HTTP error
"""
logger.debug("Modify information of user {{ {} }}".format(userid))
user = userController.get_user(userid)
data = request.get_json()
password = None
new_password = data["new_password"] if "new_password" in data else None
author = user
if userid != current_session.user_.userid:
author = current_session.user_
if not author.has_permission(permissions.EDIT):
raise Forbidden
else:
if "password" not in data:
raise BadRequest("Password is missing")
password = data["password"]
for key in ["firstname", "lastname", "display_name", "mail"]:
if key in data:
setattr(user, key, data[key])
if "birthday" in data:
user.birthday = from_iso_format(data["birthday"])
if "roles" in data:
roles = set(data["roles"])
if not author.has_permission(permissions.SET_ROLES):
if len(roles) != len(user.roles) or set(user.roles) != roles:
raise Forbidden
else:
userController.set_roles(user, roles)
userController.modify_user(user, password, new_password)
userController.update_user(
user,
)
return no_content()
@UsersPlugin.blueprint.route("/notifications", methods=["GET"])
@login_required()
def notifications(current_session):
f = request.args.get("from", None)
if f is not None:
f = from_iso_format(f)
return jsonify(userController.get_notifications(current_session.user_, f))
@UsersPlugin.blueprint.route("/notifications/<nid>", methods=["DELETE"])
@login_required()
def remove_notification(nid, current_session):
userController.delete_notification(nid, current_session.user_)
return no_content()
@UsersPlugin.blueprint.route("/users/<userid>/shortcuts", methods=["GET", "PUT"])
@login_required()
def shortcuts(userid, current_session):
if userid != current_session.user_.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("users_link_shortcuts", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, dict) for n in data):
raise BadRequest
user.set_attribute("users_link_shortcuts", data)
userController.persist()
return no_content()

View File

@ -0,0 +1,91 @@
import click
import sqlalchemy.exc
from flask.cli import with_appcontext
from werkzeug.exceptions import NotFound
from flaschengeist import logger
from flaschengeist.database import db
from flaschengeist.controller import roleController, userController
USER_KEY = f"{__name__}.user"
def user(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.echo("Adding new user")
ctx.meta[USER_KEY] = {}
try:
ctx.meta[USER_KEY]["userid"] = click.prompt("userid", type=str)
ctx.meta[USER_KEY]["firstname"] = click.prompt("firstname", type=str)
ctx.meta[USER_KEY]["lastname"] = click.prompt("lastname", type=str)
ctx.meta[USER_KEY]["display_name"] = click.prompt("displayed name", type=str, default="")
ctx.meta[USER_KEY]["mail"] = click.prompt("mail", type=str, default="")
ctx.meta[USER_KEY]["password"] = click.prompt("password", type=str, confirmation_prompt=True, hide_input=True)
ctx.meta[USER_KEY] = {k: v for k, v in ctx.meta[USER_KEY].items() if v != ""}
except click.Abort:
click.echo("\n!!! User was not added, aborted.")
del ctx.meta[USER_KEY]
@click.command()
@click.option("--create", help="Add new role", is_flag=True)
@click.option("--delete", help="Delete role", is_flag=True)
@click.option("--set-admin", is_flag=True, help="Make a role an admin role, adding all permissions", type=str)
@click.argument("role", nargs=-1, required=True, type=str)
def role(create, delete, set_admin, role):
"""Manage roles"""
ctx = click.get_current_context()
if (create and delete) or (set_admin and delete):
ctx.fail("Do not mix --delete with --create or --set-admin")
for role_name in role:
if create:
r = roleController.create_role(role_name)
else:
r = roleController.get(role_name)
if delete:
roleController.delete(r)
if set_admin:
r.permissions = roleController.get_permissions()
db.session.commit()
@click.command()
@click.option("--add-role", help="Add a role to an user", type=str)
@click.option("--create", help="Create new user interactivly", callback=user, is_flag=True, expose_value=False)
@click.option("--delete", help="Delete a user", is_flag=True)
@click.argument("user", nargs=-1, type=str)
@with_appcontext
def user(add_role, delete, user):
"""Manage users"""
from flaschengeist.database import db
ctx = click.get_current_context()
try:
if USER_KEY in ctx.meta:
userController.register(ctx.meta[USER_KEY], ctx.meta[USER_KEY]["password"])
else:
if not isinstance(user, list) or not isinstance(user, tuple):
user = [user]
for uid in user:
logger.debug(f"Userid: {uid}")
user = userController.get_user(uid)
logger.debug(f"User: {user}")
if delete:
logger.debug(f"Deleting user {user}")
userController.delete_user(user)
elif add_role:
logger.debug(f"Adding role {add_role} to user {user}")
role = roleController.get(add_role)
logger.debug(f"Role: {role}")
user.roles_.append(role)
userController.modify_user(user, None)
db.session.commit()
except NotFound:
ctx.fail(f"User not found {uid}")

View File

@ -0,0 +1,13 @@
EDIT = "users_edit_other"
"""Can edit other users"""
SET_ROLES = "users_set_roles"
"""Can assign roles to users"""
DELETE = "users_delete"
"""Can delete users"""
REGISTER = "users_register"
"""Can register new users"""
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -0,0 +1,29 @@
from http.client import NO_CONTENT, CREATED
from flask import make_response, jsonify
from flaschengeist.utils.datetime import from_iso_format
def get_filter_args():
"""
Get filter parameter from request
returns: FROM, TO, LIMIT, OFFSET, DESCENDING
"""
from flask import request
return (
request.args.get("from", type=from_iso_format),
request.args.get("to", type=from_iso_format),
request.args.get("limit", type=int),
request.args.get("offset", type=int),
"descending" in request.args,
)
def no_content():
return make_response(jsonify(""), NO_CONTENT)
def created(obj=None):
return make_response(jsonify(obj if obj is not None else ""), CREATED)

View File

@ -0,0 +1 @@
"""Common utilities"""

View File

@ -0,0 +1,11 @@
import datetime
def from_iso_format(date_str):
"""Z-suffix aware version of `datetime.datetime.fromisoformat`"""
if not date_str:
return None
time = datetime.datetime.fromisoformat(date_str.replace("Z", "+00:00"))
if time.tzinfo:
return time.astimezone(datetime.timezone.utc)
return time.replace(tzinfo=datetime.timezone.utc)

View File

@ -0,0 +1,68 @@
from functools import wraps
from werkzeug.exceptions import Unauthorized
from flaschengeist import logger
from flaschengeist.controller import sessionController
def extract_session(permission=None):
from flask import request
try:
token = list(filter(None, request.headers.get("Authorization").split(" ")))[-1]
except AttributeError:
logger.debug("Missing Authorization header or ill-formed")
raise Unauthorized
session = sessionController.validate_token(token, request.headers, permission)
return session
def login_required(permission=None):
"""Decorator use to make a route only accessible by logged in users.
Sets ``current_session`` into kwargs of wrapped function with session identified by Authorization header.
Attributes:
permission: Optional permission needed for this route
Returns:
Wrapped function with login (and permission) guard
"""
def wrap(func):
@wraps(func)
def wrapped_f(*args, **kwargs):
session = extract_session(permission)
kwargs["current_session"] = session
logger.debug("token {{ {} }} is valid".format(session.token))
return func(*args, **kwargs)
return wrapped_f
return wrap
def headers(headers={}, **headers_kwargs):
"""
Wrap a Flask route to add HTTP headers.
Either pass a dictionary of headers to be set as the headerDict keyword
argument, or pass header values as keyword arguments. Or both.
The key and value of items in a dictionary will be converted to strings using
the `str` method, ensure both keys and values are serializable thusly.
Args:
headers: A dictionary of headers to be injected into the response headers.
Note, the supplied dictionary is first copied then mutated.
headers_kwargs: The headers to be injected into the response headers.
"""
_headerDict = headers.copy()
_headerDict.update(headers_kwargs)
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(*args, **kwargs), _headerDict
return decorated_function
return decorator

View File

@ -0,0 +1,81 @@
from functools import wraps
_hooks_before = {}
_hooks_after = {}
def Hook(function=None, id=None):
"""Hook decorator
Use to decorate functions as hooks, so plugins can hook up their custom functions.
"""
# `id` passed as `arg` not `kwarg`
if isinstance(function, str):
return Hook(id=function)
def decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
_id = id if id is not None else function.__qualname__
# Hooks before
for f in _hooks_before.get(_id, []):
f(*args, **kwargs)
# Main function
result = function(*args, **kwargs)
# Hooks after
for f in _hooks_after.get(_id, []):
f(*args, hook_result=result, **kwargs)
return result
return wrapped
# Called @Hook or we are in the second step
if callable(function):
return decorator(function)
else:
return decorator
def HookBefore(id: str):
"""Decorator for functions to be called before a Hook-Function is called
The hooked up function must accept the same arguments as the function hooked onto,
as the functions are called with the same arguments.
Hint: This enables you to modify the arguments!
"""
if not id or not isinstance(id, str):
raise TypeError("HookBefore requires the ID of the function to hook up")
def wrapped(function):
_hooks_before.setdefault(id, []).append(function)
return function
return wrapped
def HookAfter(id: str):
"""Decorator for functions to be called after a Hook-Function is called
As with the HookBefore, the hooked up function must accept the same
arguments as the function hooked onto, but also receives a
`hook_result` kwarg containing the result of the function.
Example:
```py
@HookAfter("some.id")
def my_func(hook_result):
# This function is executed after the function registered with "some.id"
print(hook_result) # This is the result of the function
```
"""
if not id or not isinstance(id, str):
raise TypeError("HookAfter requires the ID of the function to hook up")
def wrapped(function):
_hooks_after.setdefault(id, []).append(function)
return function
return wrapped

View File

@ -1,59 +0,0 @@
""" Server-package
Initialize app, cors, database and bcrypt (for passwordhashing) and added it to the application.
Initialize also a singelton for the AccesTokenControler and start the Thread.
"""
from .logger import getDebugLogger
from geruecht.controller import dbConfig, ldapConfig
from flask_mysqldb import MySQL
from flask_ldapconn import LDAPConn
import ssl
DEBUG = getDebugLogger()
DEBUG.info("Initialize App")
from flask import Flask
from flask_cors import CORS
DEBUG.info("Build APP")
app = Flask(__name__)
CORS(app)
app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29'
app.config['MYSQL_HOST'] = dbConfig['URL']
app.config['MYSQL_USER'] = dbConfig['user']
app.config['MYSQL_PASSWORD'] = dbConfig['passwd']
app.config['MYSQL_DB'] = dbConfig['database']
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
app.config['LDAP_SERVER'] = ldapConfig['URL']
app.config['LDAP_PORT'] = ldapConfig['PORT']
if ldapConfig['BIND_DN']:
app.config['LDAP_BINDDN'] = ldapConfig['BIND_DN']
else:
app.config['LDAP_BINDDN'] = ldapConfig['DN']
if ldapConfig['BIND_SECRET']:
app.config['LDAP_SECRET'] = ldapConfig['BIND_SECRET']
app.config['LDAP_USE_TLS'] = False
app.config['LDAP_USE_SSL'] = ldapConfig['SSL']
app.config['LDAP_TLS_VERSION'] = ssl.PROTOCOL_TLSv1_2
app.config['LDAP_REQUIRE_CERT'] = ssl.CERT_NONE
app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] = True
ldap = LDAPConn(app)
db = MySQL(app)
from geruecht import routes
from geruecht.baruser.routes import baruser
from geruecht.finanzer.routes import finanzer
from geruecht.user.routes import user
from geruecht.vorstand.routes import vorstand
from geruecht.gastro.routes import gastrouser
from geruecht.registration_route import registration
DEBUG.info("Registrate bluebrints")
app.register_blueprint(baruser)
app.register_blueprint(finanzer)
app.register_blueprint(user)
app.register_blueprint(vorstand)
app.register_blueprint(gastrouser)
app.register_blueprint(registration)

View File

@ -1,224 +0,0 @@
from flask import Blueprint, request, jsonify
import geruecht.controller.ldapController as lc
import geruecht.controller.mainController as mc
import geruecht.controller.accesTokenController as ac
from datetime import datetime
from geruecht.model import BAR, MONEY, USER, VORSTAND, EXTERN
from geruecht.decorator import login_required
from geruecht.logger import getDebugLogger, getCreditLogger
debug = getDebugLogger()
creditL = getCreditLogger()
baruser = Blueprint("baruser", __name__)
ldap = lc.LDAPController()
mainController = mc.MainController()
accesTokenController = ac.AccesTokenController()
@baruser.route("/bar")
@login_required(groups=[BAR], bar=True)
def _bar(**kwargs):
""" Main function for Baruser
Returns JSON-file with all Users, who hast amounts in this month.
Returns:
JSON-File with Users, who has amounts in this month
or ERROR 401 Permission Denied
"""
debug.info("/bar")
try:
dic = {}
users = mainController.getAllUsersfromDB()
for user in users:
geruecht = None
geruecht = user.getGeruecht(datetime.now().year)
if geruecht is not None:
all = geruecht.getSchulden()
if all != 0:
if all >= 0:
type = 'credit'
else:
type = 'amount'
dic[user.uid] = {"username": user.uid,
"firstname": user.firstname,
"lastname": user.lastname,
"amount": all,
"locked": user.locked,
"type": type,
"limit": user.limit,
"autoLock": user.autoLock
}
dic[user.uid]['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, "hour": user.last_seen.hour, "minute": user.last_seen.minute, "second": user.last_seen.second} if user.last_seen else None
debug.debug("return {{ {} }}".format(dic))
return jsonify(dic)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/baradd", methods=['POST'])
@login_required(groups=[BAR], bar=True)
def _baradd(**kwargs):
""" Function for Baruser to add amount
This function added to the user with the posted userID the posted amount.
Returns:
JSON-File with userID and the amount
or ERROR 401 Permission Denied
"""
debug.info("/baradd")
try:
data = request.get_json()
userID = data['userId']
amount = int(data['amount'])
amountl = amount
date = datetime.now()
mainController.addAmount(
userID, amount, year=date.year, month=date.month, bar=True)
user = mainController.getUser(userID)
geruecht = user.getGeruecht(year=date.year)
month = geruecht.getMonth(month=date.month)
amount = abs(month[0] - month[1])
all = geruecht.getSchulden()
if all >= 0:
type = 'credit'
else:
type = 'amount'
dic = user.toJSON()
dic['amount'] = all
dic['type'] = type
dic['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, "hour": user.last_seen.hour, "minute": user.last_seen.minute, "second": user.last_seen.second} if user.last_seen else None
debug.debug("return {{ {} }}".format(dic))
creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format(
date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amountl/100))
return jsonify(dic)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/barGetUsers")
@login_required(groups=[BAR, MONEY], bar=True)
def _getUsers(**kwargs):
""" Get Users without amount
This Function returns all Users, who hasn't an amount in this month.
Returns:
JSON-File with Users
or ERROR 401 Permission Denied
"""
debug.info("/barGetUsers")
try:
retVal = {}
retVal = ldap.getAllUser()
debug.debug("return {{ {} }}".format(retVal))
return jsonify(retVal)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/bar/storno", methods=['POST'])
@login_required(groups=[BAR], bar=True)
def _storno(**kwargs):
""" Function for Baruser to storno amount
This function added to the user with the posted userID the posted amount.
Returns:
JSON-File with userID and the amount
or ERROR 401 Permission Denied
"""
debug.info("/bar/storno")
try:
data = request.get_json()
userID = data['userId']
amount = int(data['amount'])
amountl = amount
date = datetime.now()
mainController.addCredit(
userID, amount, year=date.year, month=date.month)
user = mainController.getUser(userID)
geruecht = user.getGeruecht(year=date.year)
month = geruecht.getMonth(month=date.month)
amount = abs(month[0] - month[1])
all = geruecht.getSchulden()
if all >= 0:
type = 'credit'
else:
type = 'amount'
dic = user.toJSON()
dic['amount'] = all
dic['type'] = type
dic['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day,
"hour": user.last_seen.hour, "minute": user.last_seen.minute,
"second": user.last_seen.second} if user.last_seen else None
debug.debug("return {{ {} }}".format(dic))
creditL.info("{} Baruser {} {} storniert {} € von {} {}".format(
date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amountl/100, user.firstname, user.lastname))
return jsonify(dic)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/barGetUser", methods=['POST'])
@login_required(groups=[BAR], bar=True)
def _getUser(**kwargs):
debug.info("/barGetUser")
try:
data = request.get_json()
username = data['userId']
user = mainController.getUser(username)
amount = user.getGeruecht(datetime.now().year).getSchulden()
if amount >= 0:
type = 'credit'
else:
type = 'amount'
retVal = user.toJSON()
retVal['amount'] = amount
retVal['type'] = type
debug.debug("return {{ {} }}".format(retVal))
return jsonify(retVal)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/search", methods=['GET'])
@login_required(groups=[BAR, MONEY, USER, VORSTAND], bar=True)
def _search(**kwargs):
debug.info("/search")
try:
retVal = ldap.getAllUser()
for user in retVal:
if user['username'] == 'extern':
retVal.remove(user)
break
debug.debug("return {{ {} }}".format(retVal))
return jsonify(retVal)
except Exception as err:
debug.debug("exception", exc_info=True)
return jsonify({"error": str(err)}), 500
@baruser.route("/bar/lock", methods=['GET', 'POST'])
@login_required(groups=[BAR], bar=True)
def _lockbar(**kwargs):
debug.info('/bar/lock')
accToken = kwargs['accToken']
if request.method == "POST":
data = request.get_json()
accToken.lock_bar = data['value']
accToken = accesTokenController.updateAccessToken(accToken)
accToken = accesTokenController.validateAccessToken(accToken.token, [USER, EXTERN])
debug.debug('return {{ "value": {} }}'.format(accToken.lock_bar))
return jsonify({'value': accToken.lock_bar})

View File

@ -1,22 +0,0 @@
AccessTokenLifeTime: 1800
Database:
URL:
user:
passwd:
database:
LDAP:
URL:
DN:
BIND_DN:
BIND_SECRET:
SSL:
USER_DN:
ADMIN_DN:
ADMIN_SECRET:
Mail:
URL:
port:
user:
passwd:
email:
crypt: SSL/STARTLS

View File

@ -1,134 +0,0 @@
import yaml
import sys
from .logger import getDebugLogger
DEBUG = getDebugLogger()
default = {
'AccessTokenLifeTime': 1800,
'Mail': {
'URL': '',
'port': 0,
'user': '',
'passwd': '',
'email': '',
'crypt': 'STARTTLS'
}
}
class ConifgParser():
def __init__(self, file='config.yml'):
self.file = file
with open(file, 'r') as f:
self.config = yaml.safe_load(f)
if 'Database' not in self.config:
self.__error__(
'Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"')
if 'URL' not in self.config['Database'] or 'user' not in self.config['Database'] or 'passwd' not in self.config['Database'] or 'database' not in self.config['Database']:
self.__error__(
'Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"')
self.db = self.config['Database']
DEBUG.debug("Set Databaseconfig: {}".format(self.db))
if 'LDAP' not in self.config:
self.__error__(
'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "BIND_DN"')
if 'URL' not in self.config['LDAP'] or 'DN' not in self.config['LDAP']:
self.__error__(
'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "BIND_DN"')
if 'PORT' not in self.config['LDAP']:
DEBUG.info(
'No Config for port in LDAP found. Set it to default: {}'.format(389))
self.config['LDAP']['PORT'] = 389
if 'ADMIN_DN' not in self.config['LDAP']:
DEBUG.info(
'No Config for ADMIN_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None)
)
self.config['LDAP']['ADMIN_DN'] = None
if 'ADMIN_SECRET' not in self.config['LDAP']:
DEBUG.info(
'No Config for ADMIN_SECRET in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None)
)
self.config['LDAP']['ADMIN_SECRET'] = None
if 'USER_DN' not in self.config['LDAP']:
DEBUG.info(
'No Config for USER_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None)
)
self.config['LDAP']['USER_DN'] = None
if 'BIND_DN' not in self.config['LDAP']:
DEBUG.info(
'No Config for BIND_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None)
)
self.config['LDAP']['BIND_DN'] = None
if 'BIND_SECRET' not in self.config['LDAP']:
DEBUG.info(
'No Config for BIND_SECRET in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None)
)
self.config['LDAP']['BIND_SECRET'] = None
if 'SSL' not in self.config['LDAP']:
DEBUG.info(
'No Config for SSL in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(False)
)
self.config['LDAP']['SSL'] = False
else:
self.config['LDAP']['SSL'] = bool(self.config['LDAP']['SSL'])
self.ldap = self.config['LDAP']
DEBUG.info("Set LDAPconfig: {}".format(self.ldap))
if 'AccessTokenLifeTime' in self.config:
self.accessTokenLifeTime = int(self.config['AccessTokenLifeTime'])
DEBUG.info("Set AccessTokenLifeTime: {}".format(
self.accessTokenLifeTime))
else:
self.accessTokenLifeTime = default['AccessTokenLifeTime']
DEBUG.info("No Config for AccessTokenLifetime found. Set it to default: {}".format(
self.accessTokenLifeTime))
if 'Mail' not in self.config:
self.config['Mail'] = default['Mail']
DEBUG.info('No Conifg for Mail found. Set it to defaul: {}'.format(
self.config['Mail']))
if 'URL' not in self.config['Mail']:
self.config['Mail']['URL'] = default['Mail']['URL']
DEBUG.info("No Config for URL in Mail found. Set it to default")
if 'port' not in self.config['Mail']:
self.config['Mail']['port'] = default['Mail']['port']
DEBUG.info("No Config for port in Mail found. Set it to default")
else:
self.config['Mail']['port'] = int(self.config['Mail']['port'])
DEBUG.info("No Conifg for port in Mail found. Set it to default")
if 'user' not in self.config['Mail']:
self.config['Mail']['user'] = default['Mail']['user']
DEBUG.info("No Config for user in Mail found. Set it to default")
if 'passwd' not in self.config['Mail']:
self.config['Mail']['passwd'] = default['Mail']['passwd']
DEBUG.info("No Config for passwd in Mail found. Set it to default")
if 'email' not in self.config['Mail']:
self.config['Mail']['email'] = default['Mail']['email']
DEBUG.info("No Config for email in Mail found. Set it to default")
if 'crypt' not in self.config['Mail']:
self.config['Mail']['crypt'] = default['Mail']['crypt']
DEBUG.info("No Config for crypt in Mail found. Set it to default")
self.mail = self.config['Mail']
DEBUG.info('Set Mailconfig: {}'.format(self.mail))
def getLDAP(self):
return self.ldap
def getDatabase(self):
return self.db
def getAccessToken(self):
return self.accessTokenLifeTime
def getMail(self):
return self.mail
def __error__(self, msg):
DEBUG.error(msg, exc_info=True)
sys.exit(-1)
if __name__ == '__main__':
ConifgParser()

View File

@ -1,21 +0,0 @@
from geruecht.logger import getDebugLogger
from geruecht.configparser import ConifgParser
import os
print(os.getcwd())
config = ConifgParser('geruecht/config.yml')
LOGGER = getDebugLogger()
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
dbConfig = config.getDatabase()
ldapConfig = config.getLDAP()
accConfig = config.getAccessToken()
mailConfig = config.getMail()

View File

@ -1,129 +0,0 @@
from geruecht.model.accessToken import AccessToken
import geruecht.controller as gc
import geruecht.controller.mainController as mc
import geruecht.controller.databaseController as dc
from geruecht.model import BAR
from datetime import datetime, timedelta
import hashlib
from . import Singleton
from geruecht.logger import getDebugLogger
debug = getDebugLogger()
mainController = mc.MainController()
db = dc.DatabaseController()
class AccesTokenController(metaclass=Singleton):
""" Control all createt AccesToken
This Class create, delete, find and manage AccesToken.
Attributes:
tokenList: List of currents AccessToken
lifetime: Variable for the Lifetime of one AccessToken in seconds.
"""
instance = None
tokenList = None
def __init__(self, lifetime=1800):
""" Initialize AccessTokenController
Initialize Thread and set tokenList empty.
"""
debug.info("init accesstoken controller")
self.lifetime = gc.accConfig
def checkBar(self, user):
debug.info("check if user {{ {} }} is baruser".format(user))
if (mainController.checkBarUser(user)):
if BAR not in user.group:
debug.debug("append bar to user {{ {} }}".format(user))
user.group.append(BAR)
return True
else:
while BAR in user.group:
debug.debug("delete bar from user {{ {} }}".format(user))
user.group.remove(BAR)
return False
debug.debug("user {{ {} }} groups are {{ {} }}".format(user, user.group))
def validateAccessToken(self, token, group):
""" Verify Accestoken
Verify an Accestoken and Group so if the User has permission or not.
Retrieves the accestoken if valid else retrieves False
Args:
token: Token to verify.
group: Group like 'moneymaster', 'gastro', 'user' or 'bar'
Returns:
An the AccesToken for this given Token or False.
"""
debug.info("check token {{ {} }} is valid")
for accToken in db.getAccessTokens():
debug.debug("accesstoken is {}".format(accToken))
endTime = accToken.timestamp + timedelta(seconds=accToken.lifetime)
now = datetime.now()
debug.debug("now is {{ {} }}, endtime is {{ {} }}".format(now, endTime))
if now <= endTime:
debug.debug("check if token {{ {} }} is same as {{ {} }}".format(token, accToken))
if accToken == token:
if not self.checkBar(accToken.user):
accToken.lock_bar = False
debug.debug("check if accestoken {{ {} }} has group {{ {} }}".format(accToken, group))
if self.isSameGroup(accToken, group):
accToken.updateTimestamp()
db.updateAccessToken(accToken)
debug.debug("found accesstoken {{ {} }} with token: {{ {} }} and group: {{ {} }}".format(accToken, token, group))
return accToken
else:
debug.debug("accesstoken is {{ {} }} out of date".format(accToken))
db.deleteAccessToken(accToken)
debug.debug("no valid accesstoken with token: {{ {} }} and group: {{ {} }}".format(token, group))
return False
def createAccesToken(self, user, user_agent=None):
""" Create an AccessToken
Create an AccessToken for an User and add it to the tokenList.
Args:
user: For wich User is to create an AccessToken
Returns:
A created Token for User
"""
debug.info("creat accesstoken")
now = datetime.ctime(datetime.now())
token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest()
self.checkBar(user)
accToken = db.createAccessToken(user, token, self.lifetime, datetime.now(), lock_bar=False, user_agent=user_agent)
debug.debug("accesstoken is {{ {} }}".format(accToken))
return token
def isSameGroup(self, accToken, groups):
""" Verify group in AccessToken
Verify if the User in the AccesToken has the right group.
Args:
accToken: AccessToken to verify.
groups: Group to verify.
Returns:
A Bool. If the same then True else False
"""
debug.info("check accesstoken {{ {} }} has group {{ {} }}".format(accToken, groups))
for group in groups:
if group in accToken.user.group: return True
return False
def getAccessTokensFromUser(self, user):
return db.getAccessTokensFromUser(user)
def deleteAccessToken(self, accToken):
db.deleteAccessToken(accToken)
def updateAccessToken(self, accToken):
accToken.updateTimestamp()
return db.updateAccessToken(accToken)

View File

@ -1,71 +0,0 @@
from ..mainController import Singleton
from geruecht import db
from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController, dbAccessTokenController, dbRegistrationController
from geruecht.exceptions import DatabaseExecption
import traceback
from MySQLdb._exceptions import IntegrityError
class DatabaseController(dbUserController.Base,
dbCreditListController.Base,
dbWorkerController.Base,
dbWorkgroupController.Base,
dbPricelistController.Base,
dbJobKindController.Base,
dbJobInviteController.Base,
dbJobRequesController.Base,
dbAccessTokenController.Base,
dbRegistrationController.Base,
metaclass=Singleton):
'''
DatabaesController
Connect to the Database and execute sql-executions
'''
def __init__(self):
self.db = db
def getLockedDay(self, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from locked_days where daydate='{}'".format(date))
data = cursor.fetchone()
return data
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def setLockedDay(self, date, locked, hard=False):
try:
cursor = self.db.connection.cursor()
sql = "insert into locked_days (daydate, locked) VALUES ('{}', {})".format(date, locked)
cursor.execute(sql)
self.db.connection.commit()
return self.getLockedDay(date)
except IntegrityError as err:
self.db.connection.rollback()
try:
exists = self.getLockedDay(date)
if hard:
sql = "update locked_days set locked={} where id={}".format(locked, exists['id'])
else:
sql = False
if sql:
cursor.execute(sql)
self.db.connection.commit()
return self.getLockedDay(date)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
if __name__ == '__main__':
db = DatabaseController()
user = db.getUser('jhille')
db.getCreditListFromUser(user, year=2018)

View File

@ -1,82 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
from geruecht.model.accessToken import AccessToken
class Base:
def getAccessToken(self, item):
try:
cursor = self.db.connection.cursor()
if type(item) == str:
sql = "select * from session where token='{}'".format(item)
elif type(item) == int:
sql = 'select * from session where id={}'.format(item)
else:
raise DatabaseExecption("item as no type int or str. name={}, type={}".format(item, type(item)))
cursor.execute(sql)
session = cursor.fetchone()
retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], lock_bar=bool(session['lock_bar']),timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) if session != None else None
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getAccessTokensFromUser(self, user):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from session where user={}".format(user.id))
sessions = cursor.fetchall()
retVal = [
AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'],
lock_bar=bool(session['lock_bar']), timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions]
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getAccessTokens(self):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from session")
sessions = cursor.fetchall()
retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], lock_bar=bool(session['lock_bar']),timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions]
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def createAccessToken(self, user, token, lifetime, timestamp, lock_bar, user_agent=None):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into session (user, timestamp, lock_bar, token, lifetime, browser, platform) VALUES ({}, '{}', {}, '{}', {}, '{}', '{}')".format(user.id, timestamp, lock_bar, token, lifetime, user_agent.browser if user_agent else 'NULL', user_agent.platform if user_agent else 'NULL'))
self.db.connection.commit()
return self.getAccessToken(token)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateAccessToken(self, accToken):
try:
cursor = self.db.connection.cursor()
cursor.execute("update session set timestamp='{}', lock_bar={}, lifetime={} where id={}".format(accToken.timestamp, accToken.lock_bar, accToken.lifetime, accToken.id))
self.db.connection.commit()
return self.getAccessToken(accToken.id)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def deleteAccessToken(self, accToken):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from session where id={}".format(accToken.id))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))

View File

@ -1,73 +0,0 @@
import traceback
from datetime import datetime
from geruecht.exceptions import DatabaseExecption
from geruecht.model.creditList import CreditList
from geruecht.model.user import User
class Base:
def getCreditListFromUser(self, user, **kwargs):
try:
if type(user) is User:
if user.uid == 'extern':
return []
cursor = self.db.connection.cursor()
if 'year' in kwargs:
sql = "select * from creditList where user_id={} and year_date={}".format(user.id if type(user) is User else user, kwargs['year'])
else:
sql = "select * from creditList where user_id={}".format(user.id if type(user) is User else user)
cursor.execute(sql)
data = cursor.fetchall()
if len(data) == 0:
return self.createCreditList(user_id=user.id, year=datetime.now().year)
elif len(data) == 1:
return [CreditList(data[0])]
else:
return [CreditList(value) for value in data]
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def createCreditList(self, user_id, year=datetime.now().year):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id))
self.db.connection.commit()
return self.getCreditListFromUser(user_id)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateCreditList(self, creditlist):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year))
data = cursor.fetchall()
if len(data) == 0:
self.createCreditList(creditlist.user_id, creditlist.year)
sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden,
creditlist.feb_guthaben, creditlist.feb_schulden,
creditlist.maer_guthaben, creditlist.maer_schulden,
creditlist.apr_guthaben, creditlist.apr_schulden,
creditlist.mai_guthaben, creditlist.mai_schulden,
creditlist.jun_guthaben, creditlist.jun_schulden,
creditlist.jul_guthaben, creditlist.jul_schulden,
creditlist.aug_guthaben, creditlist.aug_schulden,
creditlist.sep_guthaben, creditlist.sep_schulden,
creditlist.okt_guthaben, creditlist.okt_schulden,
creditlist.nov_guthaben, creditlist.nov_schulden,
creditlist.dez_guthaben, creditlist.dez_schulden,
creditlist.last_schulden, creditlist.year, creditlist.user_id)
print(sql)
cursor = self.db.connection.cursor()
cursor.execute(sql)
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))

View File

@ -1,84 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def getJobInvite(self, from_user, to_user, date, id=None):
try:
cursor = self.db.connection.cursor()
if id:
cursor.execute("select * from job_invites where id={}".format(id))
else:
cursor.execute("select * from job_invites where from_user={} and to_user={} and on_date='{}'".format(from_user['id'], to_user['id'], date))
retVal = cursor.fetchone()
retVal['to_user'] = self.getUserById(retVal['to_user']).toJSON()
retVal['from_user'] = self.getUserById(retVal['from_user']).toJSON()
retVal['on_date'] = {'year': retVal['on_date'].year, 'month': retVal['on_date'].month, 'day': retVal['on_date'].day}
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getJobInvitesFromUser(self, from_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_invites where from_user={} and on_date>='{}'".format(from_user['id'], date))
retVal = cursor.fetchall()
for item in retVal:
item['from_user'] = from_user
item['to_user'] = self.getUserById(item['to_user']).toJSON()
item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day}
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getJobInvitesToUser(self, to_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_invites where to_user={} and on_date>='{}'".format(to_user['id'], date))
retVal = cursor.fetchall()
for item in retVal:
item['from_user'] = self.getUserById(item['from_user']).toJSON()
item['to_user'] = to_user
item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day}
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def setJobInvite(self, from_user, to_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into job_invites (from_user, to_user, on_date) values ({}, {}, '{}')".format(from_user['id'], to_user['id'], date))
self.db.connection.commit()
return self.getJobInvite(from_user, to_user, date)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateJobInvite(self, jobinvite):
try:
cursor = self.db.connection.cursor()
cursor.execute("update job_invites set watched={} where id={}".format(jobinvite['watched'], jobinvite['id']))
self.db.connection.commit()
return self.getJobInvite(None, None, None, jobinvite['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def deleteJobInvite(self, jobinvite):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from job_invites where id={}".format(jobinvite['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))

View File

@ -1,132 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def getAllJobKinds(self):
try:
cursor = self.db.connection.cursor()
cursor.execute('select * from job_kind')
list = cursor.fetchall()
for item in list:
item['workgroup'] = self.getWorkgroup(item['workgroup']) if item['workgroup'] != None else None
return list
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getJobKind(self, name):
try:
cursor = self.db.connection.cursor()
if type(name) == str:
sql = "select * from job_kind where name='{}'".format(name)
elif type(name) == int:
sql = 'select * from job_kind where id={}'.format(name)
else:
raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name)))
cursor.execute(sql)
retVal = cursor.fetchone()
retVal['workgroup'] = self.getWorkgroup(retVal['workgroup']) if retVal['workgroup'] != None else None
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def setJobKind(self, name, workgroup_id):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into job_kind (name, workgroup) values ('{}', {})".format(name, workgroup_id if workgroup_id != None else 'NULL'))
self.db.connection.commit()
return self.getJobKind(name)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateJobKind(self, jobkind):
try:
cursor = self.db.connection.cursor()
cursor.execute("update job_kind set name='{}', workgroup={} where id={}".format(jobkind['name'], jobkind['workgroup']['id'] if jobkind['workgroup'] != None else 'NULL', jobkind['id']))
self.db.connection.commit()
return self.getJobKind(jobkind['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def deleteJobKind(self, jobkind):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from job_kind where id={}".format(jobkind['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def setJobKindDates(self, date, jobkind, maxpersons):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into job_kind_dates (daydate, job_kind, maxpersons) values ('{}', {}, {})".format(date, jobkind['id'] if jobkind != None else 'NULL', maxpersons))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateJobKindDates(self, jobkindDate):
try:
cursor = self.db.connection.cursor()
cursor.execute("update job_kind_dates set job_kind={}, maxpersons='{}' where id={}".format(jobkindDate['job_kind']['id'] if jobkindDate['job_kind'] != None else 'NULL', jobkindDate['maxpersons'], jobkindDate['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def getJobKindDates(self, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_kind_dates where daydate='{}'".format(date))
list = cursor.fetchall()
for item in list:
item['job_kind'] = self.getJobKind(item['job_kind']) if item['job_kind'] != None else None
item['daydate'] = {'year': item['daydate'].year, 'month': item['daydate'].month, 'day': item['daydate'].day}
return list
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getJobKindDate(self, date, job_kind):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_kind_dates where daydate='{}' and job_kind={}".format(date, job_kind['id']))
item = cursor.fetchone()
if item:
item['job_kind'] = self.getJobKind(item['job_kind']) if item['job_kind'] != None else None
item['daydate'] = {'year': item['daydate'].year, 'month': item['daydate'].month, 'day': item['daydate'].day}
else:
item = {
'job_kind': self.getJobKind(1),
'daydate': {'year': date.year, 'month': date.month, 'day': date.day},
'maxpersons': 2
}
return item
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def deleteJobKindDates(self, jobkinddates):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from job_kind_dates where id={}".format(jobkinddates['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))

View File

@ -1,97 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def getJobRequest(self, from_user, to_user, date, id=None):
try:
cursor = self.db.connection.cursor()
if id:
cursor.execute("select * from job_request where id={}".format(id))
else:
cursor.execute("select * from job_request where from_user={} and to_user={} and on_date='{}'".format(from_user['id'], to_user['id'], date))
retVal = cursor.fetchone()
retVal['to_user'] = self.getUserById(retVal['to_user']).toJSON()
retVal['from_user'] = self.getUserById(retVal['from_user']).toJSON()
retVal['on_date'] = {'year': retVal['on_date'].year, 'month': retVal['on_date'].month, 'day': retVal['on_date'].day}
retVal['job_kind'] = self.getJobKind(retVal['job_kind'])
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getJobRequestsFromUser(self, from_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_request where from_user={} and on_date>='{}'".format(from_user['id'], date))
retVal = cursor.fetchall()
for item in retVal:
item['from_user'] = from_user
item['to_user'] = self.getUserById(item['to_user']).toJSON()
item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day}
item['job_kind'] = self.getJobKind(item['job_kind'])
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getJobRequestsToUser(self, to_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from job_request where to_user={} and on_date>='{}'".format(to_user['id'], date))
retVal = cursor.fetchall()
for item in retVal:
item['from_user'] = self.getUserById(item['from_user']).toJSON()
item['to_user'] = to_user
item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day}
item['job_kind'] = self.getJobKind(item['job_kind'])
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def setJobRequest(self, from_user, to_user, date, job_kind):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into job_request (from_user, to_user, on_date, job_kind) values ({}, {}, '{}', {})".format(from_user['id'], to_user['id'], date, job_kind['id']))
self.db.connection.commit()
return self.getJobRequest(from_user, to_user, date)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateJobRequest(self, jobrequest):
try:
cursor = self.db.connection.cursor()
cursor.execute("update job_request set watched={}, answered={}, accepted={} where id={}".format(jobrequest['watched'], jobrequest['answered'], jobrequest['accepted'], jobrequest['id']))
self.db.connection.commit()
return self.getJobRequest(None, None, None, jobrequest['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateAllJobRequest(self, jobrequest):
try:
cursor = self.db.connection.cursor()
cursor.execute("update job_request set answered={} where from_user={} and on_date='{}'".format(jobrequest['answered'], jobrequest['from_user']['id'], jobrequest['on_date']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def deleteJobRequest(self, jobrequest):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from job_request where id={}".format(jobrequest['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))

View File

@ -1,128 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def getPriceList(self):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from pricelist")
return cursor.fetchall()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def getDrinkPrice(self, name):
try:
cursor = self.db.connection.cursor()
if type(name) == str:
sql = "select * from pricelist where name='{}'".format(name)
elif type(name) == int:
sql = 'select * from pricelist where id={}'.format(name)
else:
raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name)))
cursor.execute(sql)
return cursor.fetchone()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def setDrinkPrice(self, drink):
try:
cursor = self.db.connection.cursor()
cursor.execute(
"insert into pricelist (name, price, price_big, price_club, price_club_big, premium, premium_club, price_extern_club, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(
drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'],
drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type']))
self.db.connection.commit()
return self.getDrinkPrice(str(drink['name']))
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def updateDrinkPrice(self, drink):
try:
cursor = self.db.connection.cursor()
cursor.execute("update pricelist set name=%s, price=%s, price_big=%s, price_club=%s, price_club_big=%s, premium=%s, premium_club=%s, price_extern_club=%s, type=%s where id=%s",
(
drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'], drink['id']
))
self.db.connection.commit()
return self.getDrinkPrice(drink['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def deleteDrink(self, drink):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from pricelist where id={}".format(drink['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Database: {}".format(err))
def getDrinkType(self, name):
try:
cursor = self.db.connection.cursor()
if type(name) == str:
sql = "select * from drink_type where name='{}'".format(name)
elif type(name) == int:
sql = 'select * from drink_type where id={}'.format(name)
else:
raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name)))
cursor.execute(sql)
return cursor.fetchone()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def setDrinkType(self, name):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into drink_type (name) values ('{}')".format(name))
self.db.connection.commit()
return self.getDrinkType(name)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Database: {}".format(err))
def updateDrinkType(self, type):
try:
cursor = self.db.connection.cursor()
cursor.execute("update drink_type set name='{}' where id={}".format(type['name'], type['id']))
self.db.connection.commit()
return self.getDrinkType(type['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Database: {}".format(err))
def deleteDrinkType(self, type):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from drink_type where id={}".format(type['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def getAllDrinkTypes(self):
try:
cursor = self.db.connection.cursor()
cursor.execute('select * from drink_type')
return cursor.fetchall()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Database: {}".format(err))

View File

@ -1,32 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def setNewRegistration(self, data):
try:
cursor = self.db.connection.cursor()
if data['entryDate']:
sql = "insert into registration_list (firstname, lastname, clubname, email, keynumber, birthdate, entrydate) VALUES ('{}', '{}', '{}', '{}', {}, '{}', '{}')".format(
data['firstName'],
data['lastName'],
data['clubName'] if data['clubName'] else 'NULL',
data['mail'],
data['keynumber'] if data['keynumber'] else 'NULL',
data['birthDate'],
data['entryDate']
)
else:
sql = "insert into registration_list (firstname, lastname, clubname, email, keynumber, birthdate) VALUES ('{}', '{}', '{}', '{}', {}, '{}')".format(
data['firstName'],
data['lastName'],
data['clubName'] if data['clubName'] else 'NULL',
data['mail'],
data['keynumber'] if data['keynumber'] else 'NULL',
data['birthDate']
)
cursor.execute(sql)
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))

View File

@ -1,214 +0,0 @@
from geruecht.exceptions import DatabaseExecption, UsernameExistDB
from geruecht.model.user import User
import traceback
class Base:
def getAllUser(self, extern=False, workgroups=True):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from user")
data = cursor.fetchall()
if data:
retVal = []
for value in data:
if extern and value['uid'] == 'extern':
continue
user = User(value)
creditLists = self.getCreditListFromUser(user)
user.initGeruechte(creditLists)
if workgroups:
user.workgroups = self.getWorkgroupsOfUser(user.id)
retVal.append(user)
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getUser(self, username, workgroups=True):
try:
retVal = None
cursor = self.db.connection.cursor()
cursor.execute("select * from user where uid='{}'".format(username))
data = cursor.fetchone()
if data:
retVal = User(data)
creditLists = self.getCreditListFromUser(retVal)
retVal.initGeruechte(creditLists)
if workgroups:
retVal.workgroups = self.getWorkgroupsOfUser(retVal.id)
if retVal:
if retVal.uid == username:
return retVal
else:
return None
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getUserById(self, id, workgroups=True):
try:
retVal = None
cursor = self.db.connection.cursor()
cursor.execute("select * from user where id={}".format(id))
data = cursor.fetchone()
if data:
retVal = User(data)
creditLists = self.getCreditListFromUser(retVal)
retVal.initGeruechte(creditLists)
if workgroups:
retVal.workgroups = self.getWorkgroupsOfUser(retVal.id)
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def _convertGroupToString(self, groups):
retVal = ''
print('groups: {}'.format(groups))
if groups:
for group in groups:
if len(retVal) != 0:
retVal += ','
retVal += group
return retVal
def insertUser(self, user):
try:
cursor = self.db.connection.cursor()
groups = self._convertGroupToString(user.group)
cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format(
user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateUser(self, user):
try:
cursor = self.db.connection.cursor()
groups = self._convertGroupToString(user.group)
sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format(
user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid)
print(sql)
cursor.execute(sql)
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def updateLastSeen(self, user, time):
try:
cursor = self.db.connection.cursor()
sql = "update user set last_seen='{}' where uid='{}'".format(
time, user.uid)
print(sql)
cursor.execute(sql)
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def changeUsername(self, user, newUsername):
try:
cursor= self.db.connection.cursor()
cursor.execute("select * from user where uid='{}'".format(newUsername))
data = cursor.fetchall()
if data:
raise UsernameExistDB("Username already exists")
else:
cursor.execute("update user set uid='{}' where id={}".format(newUsername, user.id))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getAllStatus(self):
try:
cursor = self.db.connection.cursor()
cursor.execute('select * from statusgroup')
return cursor.fetchall()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getStatus(self, name):
try:
cursor = self.db.connection.cursor()
if type(name) == str:
sql = "select * from statusgroup where name='{}'".format(name)
elif type(name) == int:
sql = 'select * from statusgroup where id={}'.format(name)
else:
raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name)))
cursor.execute(sql)
return cursor.fetchone()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def setStatus(self, name):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into statusgroup (name) values ('{}')".format(name))
self.db.connection.commit()
return self.getStatus(name)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateStatus(self, status):
try:
cursor = self.db.connection.cursor()
cursor.execute("update statusgroup set name='{}' where id={}".format(status['name'], status['id']))
self.db.connection.commit()
return self.getStatus(status['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def deleteStatus(self, status):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from statusgroup where id={}".format(status['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateStatusOfUser(self, username, status):
try:
cursor = self.db.connection.cursor()
cursor.execute("update user set statusgroup={} where uid='{}'".format(status['id'], username))
self.db.connection.commit()
return self.getUser(username)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateVotingOfUser(self, username, voting):
try:
cursor = self.db.connection.cursor()
cursor.execute("update user set voting={} where uid='{}'".format(voting, username))
self.db.connection.commit()
return self.getUser(username)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))

View File

@ -1,81 +0,0 @@
import traceback
from datetime import timedelta
from geruecht.exceptions import DatabaseExecption
class Base:
def getWorker(self, user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date))
data = cursor.fetchone()
return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}, "job_kind": self.getJobKind(data['job_kind']) if data['job_kind'] != None else None} if data else None
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getWorkers(self, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from bardienste where startdatetime='{}'".format(date))
data = cursor.fetchall()
retVal = []
return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}, "job_kind": self.getJobKind(work['job_kind']) if work['job_kind'] != None else None} for work in data]
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def getWorkersWithJobKind(self, date, job_kind):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from bardienste where startdatetime='{}' and job_kind={} {}".format(date, job_kind['id'], "or job_kind='null'" if job_kind['id'] is 1 else ''))
data = cursor.fetchall()
retVal = []
return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}, "job_kind": self.getJobKind(work['job_kind']) if work['job_kind'] != None else None} for work in data]
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def setWorker(self, user, date, job_kind=None):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime, job_kind) values ({},'{}','{}', {})".format(user.id, date, date + timedelta(days=1), job_kind['id'] if job_kind != None else 'NULL'))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def changeWorker(self, from_user, to_user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("update bardienste set user_id={} where user_id={} and startdatetime='{}'".format(to_user['id'], from_user['id'], date))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))
def deleteAllWorkerWithJobKind(self, date, job_kind):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from bardienste where startdatetime='{}' and job_kind={}".format(date, job_kind['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def deleteWorker(self, user, date):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Datatabase: {}".format(err))

View File

@ -1,126 +0,0 @@
import traceback
from geruecht.exceptions import DatabaseExecption
class Base:
def getAllWorkgroups(self):
try:
cursor = self.db.connection.cursor()
cursor.execute('select * from workgroup')
list = cursor.fetchall()
for item in list:
if item['boss'] != None:
item['boss']=self.getUserById(item['boss'], workgroups=False).toJSON()
return list
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getWorkgroup(self, name):
try:
cursor = self.db.connection.cursor()
if type(name) == str:
sql = "select * from workgroup where name='{}'".format(name)
elif type(name) == int:
sql = 'select * from workgroup where id={}'.format(name)
else:
raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name)))
cursor.execute(sql)
retVal = cursor.fetchone()
retVal['boss'] = self.getUserById(retVal['boss'], workgroups=False).toJSON() if retVal['boss'] != None else None
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def setWorkgroup(self, name, boss):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into workgroup (name, boss) values ('{}', {})".format(name, boss['id']))
self.db.connection.commit()
return self.getWorkgroup(name)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def updateWorkgroup(self, workgroup):
try:
cursor = self.db.connection.cursor()
cursor.execute("update workgroup set name='{}', boss={} where id={}".format(workgroup['name'], workgroup['boss']['id'], workgroup['id']))
self.db.connection.commit()
return self.getWorkgroup(workgroup['id'])
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def deleteWorkgroup(self, workgroup):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from workgroup where id={}".format(workgroup['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went worng with Databes: {}".format(err))
def getWorkgroupsOfUser(self, userid):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from user_workgroup where user_id={} ".format(userid))
knots = cursor.fetchall()
retVal = [self.getWorkgroup(knot['workgroup_id']) for knot in knots]
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def getUsersOfWorkgroups(self, workgroupid):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from user_workgroup where workgroup_id={}".format(workgroupid))
knots = cursor.fetchall()
retVal = [self.getUserById(knot['user_id'], workgroups=False).toJSON() for knot in knots]
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def getUserWorkgroup(self, user, workgroup):
try:
cursor = self.db.connection.cursor()
cursor.execute("select * from user_workgroup where workgroup_id={} and user_id={}".format(workgroup['id'], user['id']))
knot = cursor.fetchone()
retVal = {"workgroup": self.getWorkgroup(workgroup['id']), "user": self.getUserById(user['id'], workgroups=False).toJSON()}
return retVal
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def setUserWorkgroup(self, user, workgroup):
try:
cursor = self.db.connection.cursor()
cursor.execute("insert into user_workgroup (user_id, workgroup_id) VALUES ({}, {})".format(user['id'], workgroup['id']))
self.db.connection.commit()
return self.getUserWorkgroup(user, workgroup)
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))
def deleteWorkgroupsOfUser(self, user):
try:
cursor = self.db.connection.cursor()
cursor.execute("delete from user_workgroup where user_id={}".format(user['id']))
self.db.connection.commit()
except Exception as err:
traceback.print_exc()
self.db.connection.rollback()
raise DatabaseExecption("Something went wrong with Database: {}".format(err))

View File

@ -1,119 +0,0 @@
import smtplib
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from geruecht.logger import getDebugLogger
from . import mailConfig
debug = getDebugLogger()
class EmailController():
def __init__(self):
debug.info("init email controller")
self.smtpServer = mailConfig['URL']
self.port = mailConfig['port']
self.user = mailConfig['user']
self.passwd = mailConfig['passwd']
self.crypt = mailConfig['crypt']
self.email = mailConfig['email']
debug.debug("smtpServer is {{ {} }}, port is {{ {} }}, user is {{ {} }}, crypt is {{ {} }}, email is {{ {} }}".format(self.smtpServer, self.port, self.user, self.crypt, self.email))
def __connect__(self):
debug.info('connect to email server')
if self.crypt == 'SSL':
self.smtp = smtplib.SMTP_SSL(self.smtpServer, self.port)
log = self.smtp.ehlo()
debug.debug("ehlo is {{ {} }}".format(log))
if self.crypt == 'STARTTLS':
self.smtp = smtplib.SMTP(self.smtpServer, self.port)
log = self.smtp.ehlo()
debug.debug("ehlo is {{ {} }}".format(log))
log = self.smtp.starttls()
debug.debug("starttles is {{ {} }}".format(log))
log = self.smtp.login(self.user, self.passwd)
debug.debug("login is {{ {} }}".format(log))
def jobTransact(self, user, jobtransact):
debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user))
date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year'])
from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname'])
job_kind = jobtransact['job_kind']
subject = 'Dienstanfrage am {}'.format(date)
text = MIMEText(
"Hallo {} {},\n"
"{} fragt, ob du am {} den Dienst {} übernehmen willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date, job_kind['name']), 'plain')
debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string()))
return (subject, text)
def jobInvite(self, user, jobtransact):
debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user))
date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year'])
from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname'])
subject = 'Diensteinladung am {}'.format(date)
text = MIMEText(
"Hallo {} {},\n"
"{} fragt, ob du am {} mit Dienst haben willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'plain')
debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string()))
return (subject, text)
def credit(self, user):
debug.info("create email credit for user {{ {} }}".format(user))
subject = Header('Gerücht, bezahle deine Schulden!', 'utf-8')
sum = user.getGeruecht(datetime.now().year).getSchulden()
if sum < 0:
type = 'Schulden'
add = 'Bezahle diese umgehend an den Finanzer.'
else:
type = 'Guthaben'
add = ''
text = MIMEText(
"Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format(
user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain')
debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string()))
return (subject, text)
def passwordReset(self, user, data):
debug.info("create email passwort reset for user {{ {} }}".format(user))
subject = Header("Password vergessen")
text = MIMEText(
"Hallo {} {},\nDu hast dein Password vergessen!\nDies wurde nun mit Flaschengeist zurückgesetzt.\nDein neues Passwort lautet:\n{}\n\nBitte ändere es sofort in deinem Flaschengeistprolif in https://flaschengeist.wu5.de.".format(
user.firstname, user.lastname, data['password']
), 'plain'
)
debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string()))
return (subject, text)
def sendMail(self, user, type='credit', jobtransact=None, **kwargs):
debug.info("send email to user {{ {} }}".format(user))
try:
if user.mail == 'None' or not user.mail:
debug.warning("user {{ {} }} has no email-address".format(user))
raise Exception("no valid Email")
msg = MIMEMultipart()
msg['From'] = self.email
msg['To'] = user.mail
if type == 'credit':
subject, text = self.credit(user)
elif type == 'jobtransact':
subject, text = self.jobTransact(user, jobtransact)
elif type == 'jobinvite':
subject, text = self.jobInvite(user, jobtransact)
elif type == 'passwordReset':
subject, text = self.passwordReset(user, kwargs)
else:
raise Exception("Fail to send Email. No type is set. user={}, type={} , jobtransact={}".format(user, type, jobtransact))
msg['Subject'] = subject
msg.attach(text)
debug.debug("send email {{ {} }} to user {{ {} }}".format(msg.as_string(), user))
self.__connect__()
self.smtp.sendmail(self.email, user.mail, msg.as_string())
return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}}
except Exception:
debug.warning("exception in send email", exc_info=True)
return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}}

View File

@ -1,204 +0,0 @@
from geruecht import ldap
from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5
from ldap3.utils.hashed import hashed
from geruecht.model import MONEY, USER, GASTRO, BAR, VORSTAND, EXTERN
from geruecht.exceptions import PermissionDenied
from . import Singleton
from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion
from geruecht import ldapConfig
from geruecht.logger import getDebugLogger
debug = getDebugLogger()
class LDAPController(metaclass=Singleton):
'''
Authentification over LDAP. Create Account on-the-fly
'''
def __init__(self):
debug.info("init ldap controller")
self.dn = ldapConfig['DN']
self.ldap = ldap
debug.debug("base dn is {{ {} }}".format(self.dn))
debug.debug("ldap is {{ {} }}".format(self.ldap))
def login(self, username, password):
debug.info("login user {{ {} }} in ldap")
try:
retVal = self.ldap.authenticate(username, password, 'uid', self.dn)
debug.debug("authentification to ldap is {{ {} }}".format(retVal))
if not retVal:
debug.debug("authenification is incorrect")
raise PermissionDenied("Invalid Password or Username")
except Exception as err:
debug.warning("exception while login into ldap", exc_info=True)
raise PermissionDenied("Invalid Password or Username. {}".format(err))
def bind(self, user, password):
debug.info("bind user {{ {} }} to ldap")
ldap_conn = self.ldap.connect(user.dn, password)
debug.debug("ldap_conn is {{ {} }}".format(ldap_conn))
return ldap_conn
def getUserData(self, username):
debug.info("get user data from ldap of user {{ {} }}".format(username))
try:
debug.debug("search user in ldap")
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail'])
user = self.ldap.connection.response[0]['attributes']
debug.debug("user is {{ {} }}".format(user))
retVal = {
'dn': self.ldap.connection.response[0]['dn'],
'firstname': user['givenName'][0],
'lastname': user['sn'][0],
'uid': user['uid'][0],
}
if user['mail']:
retVal['mail'] = user['mail'][0]
debug.debug("user is {{ {} }}".format(retVal))
if retVal['uid'] == username:
return retVal
else:
raise Exception()
except:
debug.warning("exception in get user data from ldap", exc_info=True)
raise PermissionDenied("No User exists with this uid.")
def getGroup(self, username):
debug.info("get group from user {{ {} }}".format(username))
try:
retVal = []
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['gidNumber'])
main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber']
debug.debug("main group number is {{ {} }}".format(main_group_number))
if main_group_number:
if type(main_group_number) is list:
main_group_number = main_group_number[0]
self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn'])
group_name = self.ldap.connection.response[0]['attributes']['cn'][0]
debug.debug("group name is {{ {} }}".format(group_name))
if group_name == 'ldap-user':
retVal.append(USER)
if group_name == 'extern':
retVal.append(EXTERN)
self.ldap.connection.search('ou=group,{}'.format(self.dn), '(memberUID={})'.format(username), SUBTREE, attributes=['cn'])
groups_data = self.ldap.connection.response
debug.debug("groups number is {{ {} }}".format(groups_data))
for data in groups_data:
group_name = data['attributes']['cn'][0]
debug.debug("group name is {{ {} }}".format(group_name))
if group_name == 'finanzer':
retVal.append(MONEY)
elif group_name == 'gastro':
retVal.append(GASTRO)
elif group_name == 'bar':
retVal.append(BAR)
elif group_name == 'vorstand':
retVal.append(VORSTAND)
elif group_name == 'ldap-user':
retVal.append(USER)
debug.debug("groups are {{ {} }}".format(retVal))
return retVal
except Exception as err:
debug.warning("exception in get groups from ldap", exc_info=True)
raise LDAPExcetpion(str(err))
def __isUserInList(self, list, username):
help_list = []
for user in list:
help_list.append(user['username'])
if username in help_list:
return True
return False
def getAllUser(self):
debug.info("get all users from ldap")
retVal = []
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail'])
data = self.ldap.connection.response
debug.debug("data is {{ {} }}".format(data))
for user in data:
if 'uid' in user['attributes']:
username = user['attributes']['uid'][0]
firstname = user['attributes']['givenName'][0]
lastname = user['attributes']['sn'][0]
retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname})
debug.debug("users are {{ {} }}".format(retVal))
return retVal
def searchUser(self, searchString):
name = searchString.split(" ")
for i in range(len(name)):
name[i] = "*"+name[i]+"*"
print(name)
name_result = []
if len(name) == 1:
if name[0] == "**":
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE,
attributes=['uid', 'givenName', 'sn'])
name_result.append(self.ldap.connection.response)
else:
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(givenName={})'.format(name[0]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail'])
name_result.append(self.ldap.connection.response)
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(sn={})'.format(name[0]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail'])
name_result.append(self.ldap.connection.response)
else:
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(givenName={})'.format(name[1]), SUBTREE, attributes=['uid', 'givenName', 'sn'])
name_result.append(self.ldap.connection.response)
self.ldap.connection.search('ou=user,{}'.format(self.dn), '(sn={})'.format(name[1]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail'])
name_result.append(self.ldap.connection.response)
retVal = []
for names in name_result:
for user in names:
if 'uid' in user['attributes']:
username = user['attributes']['uid'][0]
if not self.__isUserInList(retVal, username):
firstname = user['attributes']['givenName'][0]
lastname = user['attributes']['sn'][0]
retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname})
return retVal
def modifyUser(self, user, conn, attributes):
debug.info("modify ldap data from user {{ {} }} with attributes (can't show because here can be a password)".format(user))
try:
if 'username' in attributes:
debug.debug("change username")
conn.search('ou=user,{}'.format(self.dn), '(uid={})'.format(attributes['username']))
if conn.entries:
debug.warning("username already exists", exc_info=True)
raise UsernameExistLDAP("Username already exists in LDAP")
#create modifyer
mody = {}
if 'username' in attributes:
mody['uid'] = [(MODIFY_REPLACE, [attributes['username']])]
if 'firstname' in attributes:
mody['givenName'] = [(MODIFY_REPLACE, [attributes['firstname']])]
if 'lastname' in attributes:
mody['sn'] = [(MODIFY_REPLACE, [attributes['lastname']])]
if 'mail' in attributes:
mody['mail'] = [(MODIFY_REPLACE, [attributes['mail']])]
if 'password' in attributes:
salted_password = hashed(HASHED_SALTED_MD5, attributes['password'])
mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])]
debug.debug("modyfier are (can't show because here can be a password)")
conn.modify(user.dn, mody)
except Exception as err:
debug.warning("exception in modify user data from ldap", exc_info=True)
raise LDAPExcetpion("Something went wrong in LDAP: {}".format(err))
if __name__ == '__main__':
a = LDAPController()
a.getUserData('jhille')

View File

@ -1,152 +0,0 @@
from .. import Singleton, mailConfig
import geruecht.controller.databaseController as dc
import geruecht.controller.ldapController as lc
import geruecht.controller.emailController as ec
from geruecht.model.user import User
from datetime import datetime, timedelta
from geruecht.logger import getDebugLogger
from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController, mainRegistrationController, mainPasswordReset
db = dc.DatabaseController()
ldap = lc.LDAPController()
emailController = ec.EmailController()
debug = getDebugLogger()
class MainController(mainJobKindController.Base,
mainCreditListController.Base,
mainPricelistController.Base,
mainUserController.Base,
mainWorkerController.Base,
mainWorkgroupController.Base,
mainJobInviteController.Base,
mainJobRequestController.Base,
mainRegistrationController.Base,
mainPasswordReset.Base,
metaclass=Singleton):
def __init__(self):
debug.debug("init UserController")
pass
def setLockedDay(self, date, locked, hard=False):
debug.info(
"set day locked on {{ {} }} with state {{ {} }}".format(date, locked))
retVal = db.setLockedDay(date.date(), locked, hard)
debug.debug("seted day locked is {{ {} }}".format(retVal))
return retVal
def getLockedDays(self, from_date, to_date):
debug.info("get locked days from {{ {} }} to {{ {} }}".format(
from_date.date(), to_date.date()))
oneDay = timedelta(1)
delta = to_date.date() - from_date.date()
retVal = []
startdate = from_date - oneDay
for _ in range(delta.days + 1):
startdate += oneDay
lockday = self.getLockedDay(startdate)
retVal.append(lockday)
debug.debug("lock days are {{ {} }}".format(retVal))
return retVal
def getLockedDaysFromList(self, date_list):
debug.info("get locked days from list {{ {} }}".format(date_list))
retVal = []
for on_date in date_list:
day = datetime(on_date['on_date']['year'], on_date['on_date']['month'], on_date['on_date']['day'], 12)
retVal.append(self.getLockedDay(day))
return retVal
def getLockedDay(self, date):
debug.info("get locked day on {{ {} }}".format(date))
now = datetime.now()
debug.debug("now is {{ {} }}".format(now))
oldMonth = False
debug.debug("check if date old month or current month")
for i in range(1, 8):
if datetime(now.year, now.month, i).weekday() == 2:
if now.day < i:
oldMonth = True
break
debug.debug("oldMonth is {{ {} }}".format(oldMonth))
lockedYear = now.year
lockedMonth = now.month if now.month < now.month else now.month - \
1 if oldMonth else now.month
endDay = 1
debug.debug("calculate end day of month")
lockedYear = lockedYear if lockedMonth != 12 else (lockedYear + 1)
lockedMonth = (lockedMonth + 1) if lockedMonth != 12 else 1
for i in range(1, 8):
nextMonth = datetime(lockedYear, lockedMonth, i)
if nextMonth.weekday() == 2:
endDay = i
break
monthLockedEndDate = datetime(
lockedYear, lockedMonth, endDay) - timedelta(1)
debug.debug("get lock day from database")
retVal = db.getLockedDay(date.date())
if not retVal:
debug.debug(
"lock day not exists, retVal is {{ {} }}".format(retVal))
if date.date() <= monthLockedEndDate.date():
debug.debug("lock day {{ {} }}".format(date.date()))
self.setLockedDay(date, True)
retVal = db.getLockedDay(date.date())
else:
retVal = {"daydate": date.date(), "locked": False}
debug.debug("locked day is {{ {} }}".format(retVal))
return retVal
def __updateDataFromLDAP(self, user):
debug.info("update data from ldap for user {{ {} }}".format(user))
groups = ldap.getGroup(user.uid)
debug.debug("ldap gorups are {{ {} }}".format(groups))
user_data = ldap.getUserData(user.uid)
debug.debug("ldap data is {{ {} }}".format(user_data))
user_data['gruppe'] = groups
user_data['group'] = groups
user.updateData(user_data)
db.updateUser(user)
def checkBarUser(self, user):
debug.info("check if user {{ {} }} is baruser")
date = datetime.now()
zero = date.replace(hour=0, minute=0, second=0, microsecond=0)
end = zero + timedelta(hours=12)
startdatetime = date.replace(
hour=12, minute=0, second=0, microsecond=0)
if date > zero and end > date:
startdatetime = startdatetime - timedelta(days=1)
enddatetime = startdatetime + timedelta(days=1)
debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format(
startdatetime, end))
result = False
if date >= startdatetime and date < enddatetime:
result = db.getWorker(user, startdatetime)
debug.debug("worker is {{ {} }}".format(result))
return True if result else False
def sendMail(self, username):
debug.info("send mail to user {{ {} }}".format(username))
if type(username) == User:
user = username
if type(username) == str:
user = db.getUser(username)
retVal = emailController.sendMail(user)
debug.debug("send mail is {{ {} }}".format(retVal))
return retVal
def sendAllMail(self):
debug.info("send mail to all user")
retVal = []
users = db.getAllUser()
debug.debug("users are {{ {} }}".format(users))
for user in users:
retVal.append(self.sendMail(user))
debug.debug("send mails are {{ {} }}".format(retVal))
return retVal

View File

@ -1,86 +0,0 @@
from datetime import datetime
import geruecht.controller.databaseController as dc
import geruecht.controller.emailController as ec
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
emailController = ec.EmailController()
debug = getDebugLogger()
class Base:
def autoLock(self, user):
debug.info("start autolock of user {{ {} }}".format(user))
if user.autoLock:
debug.debug("autolock is active")
credit = user.getGeruecht(year=datetime.now().year).getSchulden()
limit = -1*user.limit
if credit <= limit:
debug.debug(
"credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit))
debug.debug("lock user")
user.updateData({'locked': True})
debug.debug("send mail to user")
emailController.sendMail(user)
else:
debug.debug(
"cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit))
debug.debug("unlock user")
user.updateData({'locked': False})
db.updateUser(user)
def addAmount(self, username, amount, year, month, finanzer=False, bar=False):
debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format(
amount, username, month, year))
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
if user.uid == 'extern':
debug.debug("user is extern user, so exit add amount")
return
if not user.locked or finanzer:
debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format(
user.locked, finanzer))
user.addAmount(amount, year=year, month=month)
if bar:
user.last_seen = datetime.now()
db.updateLastSeen(user, user.last_seen)
creditLists = user.updateGeruecht()
debug.debug("creditList is {{ {} }}".format(creditLists))
for creditList in creditLists:
debug.debug("update creditlist {{ {} }}".format(creditList))
db.updateCreditList(creditList)
debug.debug("do autolock")
self.autoLock(user)
retVal = user.getGeruecht(year)
debug.debug("updated creditlists is {{ {} }}".format(retVal))
return retVal
def addCredit(self, username, credit, year, month):
debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format(
credit, username, month, year))
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
if user.uid == 'extern':
debug.debug("user is extern user, so exit add credit")
return
user.addCredit(credit, year=year, month=month)
creditLists = user.updateGeruecht()
debug.debug("creditlists are {{ {} }}".format(creditLists))
for creditList in creditLists:
debug.debug("update creditlist {{ {} }}".format(creditList))
db.updateCreditList(creditList)
debug.debug("do autolock")
self.autoLock(user)
retVal = user.getGeruecht(year)
debug.debug("updated creditlists are {{ {} }}".format(retVal))
return retVal
def __updateGeruechte(self, user):
debug.debug("update creditlists")
user.getGeruecht(datetime.now().year)
creditLists = user.updateGeruecht()
debug.debug("creditlists are {{ {} }}".format(creditLists))
if user.getGeruecht(datetime.now().year).getSchulden() != 0:
for creditList in creditLists:
debug.debug("update creditlist {{ {} }}".format(creditList))
db.updateCreditList(creditList)

View File

@ -1,42 +0,0 @@
from datetime import date
import geruecht.controller.databaseController as dc
import geruecht.controller.emailController as ec
from geruecht import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
emailController = ec.EmailController()
class Base:
def getJobInvites(self, from_user, to_user, date):
debug.info("get JobInvites from_user {{ {} }} to_user {{ {} }} on date {{ {} }}".format(from_user, to_user, date))
if from_user is None:
retVal = db.getJobInvitesToUser(to_user, date)
elif to_user is None:
retVal = db.getJobInvitesFromUser(from_user, date)
else:
raise Exception("from_user {{ {} }} and to_user {{ {} }} are None".format(from_user, to_user))
return retVal
def setJobInvites(self, data):
debug.info("set new JobInvites data {{ {} }}".format(data))
retVal = []
for jobInvite in data:
from_user = jobInvite['from_user']
to_user = jobInvite['to_user']
on_date = date(jobInvite['date']['year'], jobInvite['date']['month'], jobInvite['date']['day'])
debug.info("set new JobInvite from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}")
setJobInvite = db.setJobInvite(from_user, to_user, on_date)
retVal.append(setJobInvite)
emailController.sendMail(db.getUserById(to_user['id'], False), type='jobinvite', jobtransact=setJobInvite)
debug.debug("seted JobInvites are {{ {} }}".format(retVal))
return retVal
def updateJobInvites(self, data):
debug.info("update JobInvites data {{ {} }}".format(data))
return db.updateJobInvite(data)
def deleteJobInvite(self, jobInvite):
debug.info("delete JobInvite {{ {} }}".format(jobInvite))
db.deleteJobInvite(jobInvite)

View File

@ -1,90 +0,0 @@
from datetime import date, timedelta, datetime, time
import geruecht.controller.databaseController as dc
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
class Base:
def getAllJobKinds(self):
debug.info("get all jobkinds")
retVal = db.getAllJobKinds()
debug.debug("jobkinds are {{ {} }}".format(retVal))
return retVal
def getJobKind(self, name):
debug.info("get jobkinds {{ {} }}".format(name))
retVal = db.getJobKind(name)
debug.debug("jobkind is {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def setJobKind(self, name, workgroup=None):
debug.info("set jobkind {{ {} }} ".format(name))
retVal = db.setJobKind(name, workgroup)
debug.debug(
"seted jobkind {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def deleteJobKind(self, jobkind):
debug.info("delete jobkind {{ {} }}".format(jobkind))
db.deleteJobKind(jobkind)
def updateJobKind(self, jobkind):
debug.info("update workgroup {{ {} }}".format(jobkind))
retVal = db.updateJobKind(jobkind)
debug.debug("updated jobkind is {{ {} }}".format(retVal))
return retVal
def getJobKindDates(self, date):
debug.info("get jobkinddates on {{ {} }}".format(date))
retVal = db.getJobKindDates(date)
debug.debug("jobkinddates are {{ {} }}".format(retVal))
return retVal
def updateJobKindDates(self, jobkindDate):
debug.info("update jobkinddate {{ {} }}".format(jobkindDate))
retVal = db.updateJobKindDates(jobkindDate)
debug.debug("updated jobkind is {{ {} }}".format(retVal))
return retVal
def deleteJobKindDates(self, jobkinddates):
debug.info("delete jobkinddates {{ {} }}".format(jobkinddates))
db.deleteJobKindDates(jobkinddates)
def setJobKindDates(self, datum, jobkind, maxpersons):
debug.info("set jobkinddates with {{ {}, {}, {}, }}".format(datum, jobkind, maxpersons))
retVal = db.setJobKindDates(datum, jobkind, maxpersons)
debug.debug("seted jobkinddates is {{ {} }}".format(retVal))
return retVal
def controllJobKindDates(self, jobkinddates):
debug.info("controll jobkinddates {{ {} }}".format(jobkinddates))
datum = None
for jobkinddate in jobkinddates:
datum = date(jobkinddate['daydate']['year'], jobkinddate['daydate']['month'], jobkinddate['daydate']['day'])
if jobkinddate['id'] == -1:
if jobkinddate['job_kind']:
self.setJobKindDates(datum, jobkinddate['job_kind'], jobkinddate['maxpersons'])
if jobkinddate['id'] == 0:
jobkinddate['id'] = jobkinddate['backupid']
db.deleteAllWorkerWithJobKind(datetime.combine(datum, time(12)), jobkinddate['job_kind'])
self.deleteJobKindDates(jobkinddate)
if jobkinddate['id'] >= 1:
self.updateJobKindDates(jobkinddate)
retVal = self.getJobKindDates(datum) if datum != None else []
debug.debug("controlled jobkinddates {{ {} }}".format(retVal))
return retVal
def getJobKindDatesFromTo(self, from_date, to_date):
debug.info("get locked days from {{ {} }} to {{ {} }}".format(
from_date.date(), to_date.date()))
oneDay = timedelta(1)
delta = to_date.date() - from_date.date()
retVal = []
startdate = from_date - oneDay
for _ in range(delta.days + 1):
startdate += oneDay
jobkinddate = self.getJobKindDates(startdate)
retVal.append(jobkinddate)
debug.debug("lock days are {{ {} }}".format(retVal))
return retVal

View File

@ -1,46 +0,0 @@
from datetime import date, time, datetime
import geruecht.controller.emailController as ec
import geruecht.controller.databaseController as dc
from geruecht import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
emailController = ec.EmailController()
class Base:
def getJobRequests(self, from_user, to_user, date):
debug.info("get JobRequests from_user {{ {} }} to_user {{ {} }} on date {{ {} }}".format(from_user, to_user, date))
if from_user is None:
retVal = db.getJobRequestsToUser(to_user, date)
elif to_user is None:
retVal = db.getJobRequestsFromUser(from_user, date)
else:
raise Exception("from_user {{ {} }} and to_user {{ {} }} are None".format(from_user, to_user))
return retVal
def setJobRequests(self, data):
debug.info("set new JobRequests data {{ {} }}".format(data))
retVal = []
for jobRequest in data:
from_user = jobRequest['from_user']
to_user = jobRequest['to_user']
on_date = date(jobRequest['date']['year'], jobRequest['date']['month'], jobRequest['date']['day'])
job_kind = jobRequest['job_kind']
debug.info("set new JobRequest from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}")
setJobRequest = db.setJobRequest(from_user, to_user, on_date, job_kind)
retVal.append(setJobRequest)
emailController.sendMail(db.getUserById(to_user['id']), type='jobtransact', jobtransact=setJobRequest)
debug.debug("seted JobRequests are {{ {} }}".format(retVal))
return retVal
def updateJobRequests(self, data):
debug.info("update JobRequest data {{ {} }}".format(data))
if data['accepted']:
self.changeWorker(data['from_user'], data['to_user'], datetime.combine(data['on_date'], time(12)))
db.updateAllJobRequest(data)
return db.updateJobRequest(data)
def deleteJobRequest(self, jobRequest):
debug.info("delete JobRequest {{ {} }}".format(jobRequest))
db.deleteJobRequest(jobRequest)

View File

@ -1,39 +0,0 @@
from geruecht import ldap, ldapConfig, getDebugLogger
import geruecht.controller.emailController as ec
from ldap3.utils.hashed import hashed
from ldap3 import HASHED_SALTED_MD5, MODIFY_REPLACE
import string
import random
emailController = ec.EmailController()
debug = getDebugLogger()
def randomString(stringLength=8):
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for i in range(stringLength))
class Base:
def resetPassword(self, data):
debug.info("forgot password {{ {} }}".format(data))
adminConn = ldap.connect(ldapConfig['ADMIN_DN'], ldapConfig['ADMIN_SECRET'])
if 'username' in data:
search = 'uid={}'.format(data['username'].lower())
elif 'mail' in data:
search = 'mail={}'.format(data['mail'].lower())
else:
debug.error("username or mail not set")
raise Exception('username or mail not set')
adminConn.search(ldapConfig['DN'], '(&(objectClass=person)({}))'.format(search),
attributes=['cn', 'sn', 'givenName', 'uid', 'mail'])
for user in adminConn.response:
user_dn = user['dn']
uid = user['attributes']['uid'][0]
mail = user['attributes']['mail'][0]
mody = {}
password = randomString()
salted_password = hashed(HASHED_SALTED_MD5, password)
mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])]
debug.info("reset password for {{ {} }}".format(user_dn))
adminConn.modify(user_dn, mody)
emailController.sendMail(self.getUser(uid), type='passwordReset', password=password)
return mail

View File

@ -1,50 +0,0 @@
import geruecht.controller.databaseController as dc
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
class Base:
def deleteDrinkType(self, type):
debug.info("delete drink type {{ {} }}".format(type))
db.deleteDrinkType(type)
def updateDrinkType(self, type):
debug.info("update drink type {{ {} }}".format(type))
retVal = db.updateDrinkType(type)
debug.debug("updated drink type is {{ {} }}".format(retVal))
return retVal
def setDrinkType(self, type):
debug.info("set drink type {{ {} }}".format(type))
retVal = db.setDrinkType(type)
debug.debug("seted drink type is {{ {} }}".format(retVal))
return retVal
def deletDrinkPrice(self, drink):
debug.info("delete drink {{ {} }}".format(drink))
db.deleteDrink(drink)
def setDrinkPrice(self, drink):
debug.info("set drink {{ {} }}".format(drink))
retVal = db.setDrinkPrice(drink)
debug.debug("seted drink is {{ {} }}".format(retVal))
return retVal
def updateDrinkPrice(self, drink):
debug.info("update drink {{ {} }}".format(drink))
retVal = db.updateDrinkPrice(drink)
debug.debug("updated drink is {{ {} }}".format(retVal))
return retVal
def getAllDrinkTypes(self):
debug.info("get all drink types")
retVal = db.getAllDrinkTypes()
debug.debug("all drink types are {{ {} }}".format(retVal))
return retVal
def getPricelist(self):
debug.info("get all drinks")
list = db.getPriceList()
debug.debug("all drinks are {{ {} }}".format(list))
return list

View File

@ -1,14 +0,0 @@
from datetime import date
import geruecht.controller.databaseController as dc
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
class Base:
def setNewRegistration(self, data):
debug.info("set new registration {{ {} }}".format(data))
data['birthDate'] = date(int(data['birthDate']['year']), int(data['birthDate']['month']), int(data['birthDate']['day']))
data['entryDate'] = date(int(data['entryDate']['year']), int(data['entryDate']['month']), int(data['entryDate']['day'])) if data['entryDate'] else None
db.setNewRegistration(data)

View File

@ -1,179 +0,0 @@
from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError
from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, PermissionDenied
import geruecht.controller.databaseController as dc
import geruecht.controller.ldapController as lc
from geruecht.logger import getDebugLogger
from geruecht.model.user import User
db = dc.DatabaseController()
ldap = lc.LDAPController()
debug = getDebugLogger()
class Base:
def getAllStatus(self):
debug.info("get all status for user")
retVal = db.getAllStatus()
debug.debug("status are {{ {} }}".format(retVal))
return retVal
def getStatus(self, name):
debug.info("get status of user {{ {} }}".format(name))
retVal = db.getStatus(name)
debug.debug("status of user {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def setStatus(self, name):
debug.info("set status of user {{ {} }}".format(name))
retVal = db.setStatus(name)
debug.debug(
"settet status of user {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def deleteStatus(self, status):
debug.info("delete status {{ {} }}".format(status))
db.deleteStatus(status)
def updateStatus(self, status):
debug.info("update status {{ {} }}".format(status))
retVal = db.updateStatus(status)
debug.debug("updated status is {{ {} }}".format(retVal))
return retVal
def updateStatusOfUser(self, username, status):
debug.info("update status {{ {} }} of user {{ {} }}".format(
status, username))
retVal = db.updateStatusOfUser(username, status)
debug.debug(
"updatet status of user {{ {} }} is {{ {} }}".format(username, retVal))
return retVal
def updateVotingOfUser(self, username, voting):
debug.info("update voting {{ {} }} of user {{ {} }}".format(
voting, username))
retVal = db.updateVotingOfUser(username, voting)
debug.debug(
"updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal))
return retVal
def lockUser(self, username, locked):
debug.info("lock user {{ {} }} for credit with status {{ {} }}".format(
username, locked))
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
user.updateData({'locked': locked})
db.updateUser(user)
retVal = self.getUser(username)
debug.debug("locked user is {{ {} }}".format(retVal))
return retVal
def updateConfig(self, username, data):
debug.info(
"update config of user {{ {} }} with config {{ {} }}".format(username, data))
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
user.updateData(data)
db.updateUser(user)
retVal = self.getUser(username)
debug.debug("updated config of user is {{ {} }}".format(retVal))
return retVal
def syncLdap(self):
debug.info('sync Users from Ldap')
ldap_users = ldap.getAllUser()
for user in ldap_users:
self.getUser(user['username'])
def getAllUsersfromDB(self, extern=True):
debug.info("get all users from database")
if (len(ldap.getAllUser()) != len(db.getAllUser())):
self.syncLdap()
users = db.getAllUser()
debug.debug("users are {{ {} }}".format(users))
for user in users:
try:
debug.debug("update data from ldap")
self.__updateDataFromLDAP(user)
except:
pass
debug.debug("update creditlists")
self.__updateGeruechte(user)
retVal = db.getAllUser(extern=extern)
debug.debug("all users are {{ {} }}".format(retVal))
return retVal
def getUser(self, username):
debug.info("get user {{ {} }}".format(username))
user = db.getUser(username)
debug.debug("user is {{ {} }}".format(user))
groups = ldap.getGroup(username)
debug.debug("groups are {{ {} }}".format(groups))
user_data = ldap.getUserData(username)
debug.debug("user data from ldap is {{ {} }}".format(user_data))
user_data['gruppe'] = groups
user_data['group'] = groups
if user is None:
debug.debug("user not exists in database -> insert into database")
user = User(user_data)
db.insertUser(user)
else:
debug.debug("update database with user")
user.updateData(user_data)
db.updateUser(user)
user = db.getUser(username)
self.__updateGeruechte(user)
debug.debug("user is {{ {} }}".format(user))
return user
def modifyUser(self, user, attributes, password):
debug.info("modify user {{ {} }} with attributes (can't show because here can be a password)".format(
user))
try:
ldap_conn = ldap.bind(user, password)
if attributes:
if 'username' in attributes:
debug.debug("change username, so change first in database")
db.changeUsername(user, attributes['username'])
ldap.modifyUser(user, ldap_conn, attributes)
if 'username' in attributes:
retVal = self.getUser(attributes['username'])
debug.debug("user is {{ {} }}".format(retVal))
return retVal
else:
retVal = self.getUser(user.uid)
debug.debug("user is {{ {} }}".format(retVal))
return retVal
return self.getUser(user.uid)
except UsernameExistLDAP as err:
debug.debug(
"username exists on ldap, rechange username on database", exc_info=True)
db.changeUsername(user, user.uid)
raise Exception(err)
except LDAPExcetpion as err:
if 'username' in attributes:
db.changeUsername(user, user.uid)
raise Exception(err)
except LDAPPasswordIsMandatoryError as err:
raise Exception('Password wurde nicht gesetzt!!')
except LDAPBindError as err:
raise Exception('Password ist falsch')
except Exception as err:
raise Exception(err)
def validateUser(self, username, password):
debug.info("validate user {{ {} }}".format(username))
ldap.login(username, password)
def loginUser(self, username, password):
debug.info("login user {{ {} }}".format(username))
try:
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
user.password = password
ldap.login(username, password)
return user
except PermissionDenied as err:
debug.debug("permission is denied", exc_info=True)
raise err

View File

@ -1,58 +0,0 @@
from datetime import time, datetime
import geruecht.controller.databaseController as dc
from geruecht.exceptions import DayLocked
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
class Base:
def getWorker(self, date, username=None):
debug.info("get worker {{ {} }} on {{ {} }}".format(username, date))
if (username):
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
retVal = [db.getWorker(user, date)]
debug.debug("worker is {{ {} }}".format(retVal))
return retVal
retVal = db.getWorkers(date)
debug.debug("workers are {{ {} }}".format(retVal))
return retVal
def addWorker(self, username, date, job_kind=None, userExc=False):
debug.info("add job user {{ {} }} on {{ {} }} with job_kind {{ {} }}".format(username, date, job_kind))
if (userExc):
debug.debug("this is a user execution, check if day is locked")
lockedDay = self.getLockedDay(date)
if lockedDay:
if lockedDay['locked']:
debug.debug("day is lockey. user cant get job")
raise DayLocked("Day is locked. You can't get the Job")
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
debug.debug("check if user has job on date")
if (not db.getWorker(user, date) and len(db.getWorkersWithJobKind(date, job_kind)) < db.getJobKindDate(date.date(), job_kind)['maxpersons']):
debug.debug("set job to user")
db.setWorker(user, date, job_kind=job_kind)
retVal = self.getWorker(date, username=username)
debug.debug("worker on date is {{ {} }}".format(retVal))
return retVal
def changeWorker(self, from_user, to_user, date):
debug.info("change worker from {{ {} }} to {{ {} }} on {{ {} }}".format(from_user, to_user, date))
db.changeWorker(from_user, to_user, date)
def deleteWorker(self, username, date, userExc=False):
debug.info(
"delete worker {{ {} }} on date {{ {} }}".format(username, date))
user = self.getUser(username)
debug.debug("user is {{ {} }}".format(user))
if userExc:
debug.debug("is user execution, check if day locked")
lockedDay = self.getLockedDay(date)
if lockedDay:
if lockedDay['locked']:
raise DayLocked(
"Day is locked. You can't delete the Job")
db.deleteWorker(user, date)

View File

@ -1,42 +0,0 @@
import geruecht.controller.databaseController as dc
from geruecht.logger import getDebugLogger
db = dc.DatabaseController()
debug = getDebugLogger()
class Base:
def updateWorkgroupsOfUser(self, user, workgroups):
debug.info("update workgroups {{ {} }} of user {{ {} }}".format(workgroups, user))
db.deleteWorkgroupsOfUser(user)
for workgroup in workgroups:
db.setUserWorkgroup(user, workgroup)
return db.getWorkgroupsOfUser(user['id'])
def getAllWorkgroups(self):
debug.info("get all workgroups")
retVal = db.getAllWorkgroups()
debug.debug("workgroups are {{ {} }}".format(retVal))
return retVal
def getWorkgroups(self, name):
debug.info("get Workgroup {{ {} }}".format(name))
retVal = db.getWorkgroup(name)
debug.debug("workgroup is {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def setWorkgroup(self, name, boss):
debug.info("set workgroup {{ {} }} with boss {{ {} }}".format(name, boss))
retVal = db.setWorkgroup(name, boss)
debug.debug(
"seted workgroup {{ {} }} is {{ {} }}".format(name, retVal))
return retVal
def deleteWorkgroup(self, workgroup):
debug.info("delete workgroup {{ {} }}".format(workgroup))
db.deleteWorkgroup(workgroup)
def updateWorkgroup(self, workgroup):
debug.info("update workgroup {{ {} }}".format(workgroup))
retVal = db.updateWorkgroup(workgroup)
debug.debug("updated workgroup is {{ {} }}".format(retVal))
return retVal

Some files were not shown because too many files have changed in this diff Show More