Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • acides/hiboo
  • frju365/hiboo
  • pascoual/hiboo
  • thedarky/hiboo
  • jeremy/hiboo
  • cyrinux/hiboo
  • a.f/hiboo
  • mickge/hiboo
  • llaq/hiboo
  • vaguelysalaried/hiboo
  • felinn-glotte/hiboo
  • AntoninDelFabbro/hiboo
  • docemmetbrown/hiboo
13 results
Show changes
Showing
with 760 additions and 254 deletions
{% extends "form.html" %}
{% block title %}{% trans %}Create a group{% endtrans %}{% endblock %}
{% extends "base.html" %}
{% block title %}{{ group.groupname }}{% endblock %}
{% block subtitle %}{% trans %}group details{% endtrans %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>{% trans %}Attributes{% endtrans %}</h3>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-lg-3">{% trans %}groupname{% endtrans %}</dt>
<dd class="col-lg-9">{{ group.groupname }}</dd>
<dt class="col-lg-3">{% trans %}UUID{% endtrans %}</dt>
<dd class="col-lg-9">
<code>{{ group.uuid }}</code>
</dd>
<dt class="col-lg-3">{% trans %}Commentaire{% endtrans %}</dt>
<dd class="col-lg-9">{{ group.comment }}</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h4>User list</h4>
</div>
<div class="card-body table-responsive">
<table class="table table-striped table-head-fixed text-nowrap">
<thead>
<tr>
<th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Groups{% endtrans %}</th>
<th>{% trans %}Auth. methods{% endtrans %}</th>
<th>{% trans %}Created on{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for user in group.users %}
<tr>
<td><a href="{{ url_for("user.details", user_uuid=user.uuid) }}">{{ user.name }}</a></td>
<td>{{ macros.groups_badges(user.groups) }}</td>
<td>{{ macros.auths_badges(user.auths) }}</td>
<td>{{ user.created_at.date() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".membership", group_uuid=group.uuid) }}" class="btn btn-outline-primary">{% trans %}Manage members{% endtrans %}</a>
<a href="{{ url_for(".edit", group_uuid=group.uuid) }}" class="btn btn-outline-warning">{% trans %}Edit group{% endtrans %}</a>
<a href="{{ url_for(".delete", group_uuid=group.uuid) }}" class="btn btn-outline-danger">{% trans %}Delete group{% endtrans %}</a>
{% endblock %}
{% extends "form.html" %}
{% block title %}{% trans %}New password{% endtrans %}{% endblock %}
{% block title %}{% trans %}Edit a group{% endtrans %}{% endblock %}
{% extends "base.html" %}
{% block title %}{% trans %}Group list{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}all available groups{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead>
<tr>
<th>{% trans %}Group{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Members{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td><a href="{{ url_for("group.details", group_uuid=group.uuid) }}">{{ group.groupname }}</a></td>
<td>{{ group.comment }}</td>
<td>
{% set user_list = [] %}
{% for user in group.users %}
{{ user_list.append( user ) or "" }}
{% endfor %}
{{ user_list|count }}
</td>
<td>
<a href="{{ url_for(".membership", group_uuid=group.uuid)}}">{% trans %}Members{% endtrans %}</a>&nbsp;
<a href="{{ url_for(".edit", group_uuid=group.uuid)}}">{% trans %}Edit{% endtrans %}</a>&nbsp;
<a href="{{ url_for(".delete", group_uuid=group.uuid)}}">{% trans %}Delete{% endtrans %}</a>&nbsp;
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".create") }}" class="btn btn-outline-success">{% trans %}Create a group{% endtrans %}</a>
{% endblock %}
{% extends "form.html" %}
{% from 'bootstrap5/form.html' import render_field %}
{% block title %}{% trans %}Manage members of group{% endtrans %} {{ group.groupname }}{% endblock %}
{% block content %}
<form method="post">
{{ form.csrf_token() }}
<div class="row">
{{ render_field(form.users, class="overflow-y-scroll", style="max-height: 100vh", form_group_classes="mb-3 col-sm-6 col-12") }}
{{ render_field(form.pre_populate, class="overflow-y-scroll", style="max-height: 100vh", form_group_classes="mb-3 col-sm-6 col-12") }}
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}
from hiboo.group import blueprint, forms
from hiboo import models, utils, security
from sqlalchemy import not_
from flask_babel import lazy_gettext as _
import flask
import flask_login
@blueprint.route("/create", methods=["GET", "POST"])
@security.admin_required()
def create():
form = forms.GroupCreateEdit()
form.submit.label.text = (_("Create"))
if form.validate_on_submit():
conflict = models.Group.query.filter_by(groupname=form.groupname.data).first()
if conflict:
flask.flash(_("A group with the same name exists already"), "danger")
else:
group = models.Group()
group.groupname = form.groupname.data
group.comment = form.comment.data
models.db.session.add(group)
models.db.session.commit()
flask.flash(_("Group created successfully"), "success")
return flask.redirect(utils.url_or_intent(".list"))
return flask.render_template("group_create.html", form=form)
@blueprint.route("/delete/<group_uuid>", methods=["GET", "POST"])
@security.admin_required()
@security.confirmation_required(_("Delete the group"))
def delete(group_uuid):
group = models.Group.query.get(group_uuid) or flask.abort(404)
#TODO log implementation needs a way to keep group infos after deletion
models.db.session.delete(group)
models.db.session.commit()
return flask.redirect(flask.url_for(".list"))
@blueprint.route("/details/<group_uuid>")
@security.admin_required()
def details(group_uuid):
group = models.Group.query.get(group_uuid) or flask.abort(404)
return flask.render_template("group_details.html", group=group)
@blueprint.route("/edit/<group_uuid>", methods=["GET", "POST"])
@security.admin_required()
def edit(group_uuid):
group = models.Group.query.get(group_uuid) or flask.abort(404)
form = forms.GroupCreateEdit()
form.submit.label.text = (_("Edit"))
if form.validate_on_submit():
if form.groupname.data != group.groupname:
models.log(
models.History.GROUP,
comment=str(_("Group {} was renamed to {}").format(group.groupname, form.groupname.data)),
actor=flask_login.current_user, group=group
)
group.groupname = form.groupname.data
group.comment = form.comment.data
models.db.session.add(group)
models.db.session.commit()
flask.flash(_("Group successfully updated"), "success")
return flask.redirect(flask.url_for(".list"))
form.process(obj=group)
return flask.render_template("group_edit.html", group=group, form=form)
@blueprint.route("/membership/<group_uuid>", methods=["GET", "POST"])
@security.admin_required()
def membership(group_uuid):
group = models.Group.query.get(group_uuid) or flask.abort(404)
form = forms.GroupUserForm(pre_populate=[user.name for user in group.users])
form.pre_populate.choices = sorted([user.name for user in group.users])
legacy_users = form.pre_populate.choices[:]
available_users = (models.User.query
.filter(not_(models.User.groups.any(models.Group.uuid.in_([group_uuid]))))
.order_by('name')
)
form.users.choices = [user.name for user in available_users]
if form.validate_on_submit():
if form.users.data:
for username in form.users.data:
user = models.User.query.filter_by(name=username).first() or flask.abort(404)
models.log(
models.History.GROUP,
comment=str(_("User {} was added to the group {}").format(user.name, group.groupname)),
user=user, actor=flask_login.current_user, group=group
)
exit_list = (user for user in legacy_users if user not in form.pre_populate.data)
if exit_list:
for username in exit_list:
user = models.User.query.filter_by(name=username).first() or flask.abort(404)
models.log(
models.History.GROUP,
comment=str(_("User {} was removed from the group {}").format(user.name, group.groupname)),
user=user, actor=flask_login.current_user, group=group
)
selected_users = []
for username in [*form.users.data, *form.pre_populate.data]:
user = models.User.query.filter_by(name=username).first() or flask.abort(404)
selected_users.append(user)
group.users = selected_users
models.db.session.add(group)
models.db.session.commit()
flask.flash(_("Group memberships successfully updated"), "success")
return flask.redirect(flask.url_for(".membership", group_uuid=group.uuid))
return flask.render_template("group_membership.html", group=group, form=form)
@blueprint.route("/list")
@security.admin_required()
def list():
groups = models.Group.query.all()
return flask.render_template("group_list.html", groups=groups)
from passlib import context, hash
from flask import current_app as app
from sqlalchemy.ext import declarative, mutable
from hiboo.security import username_blocklist
from sqlalchemy import orm, types, schema, sql
from sqlalchemy.ext import mutable
from flask_babel import lazy_gettext as _
from hiboo import actions
import flask_sqlalchemy
import flask_babel
import sqlalchemy
import datetime
import json
import uuid
import pyotp
def log(category, value=None, comment=None, user=None, profile=None,
service=None, actor=None, public=True):
def log(category, value=None, comment=None, user=None, group=None,
profile=None, service=None, actor=None, public=True):
""" Log a history event
"""
event = History()
......@@ -20,6 +22,7 @@ def log(category, value=None, comment=None, user=None, profile=None,
event.value = value
event.comment = comment
event.user = user
event.group = group
event.profile = profile
event.service = service
event.actor = actor
......@@ -27,32 +30,32 @@ def log(category, value=None, comment=None, user=None, profile=None,
db.session.add(event)
class Base(flask_sqlalchemy.Model):
class Base(orm.DeclarativeBase):
""" Base class for all models
"""
metadata = sqlalchemy.schema.MetaData(
metadata = schema.MetaData(
naming_convention={
"fk": "%(table_name)s_%(column_0_name)s_fkey",
"pk": "%(table_name)s_pkey"
}
)
@declarative.declared_attr
@orm.declared_attr
def uuid(cls):
return sqlalchemy.Column(sqlalchemy.String(36), primary_key=True,
return schema.Column(types.String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
@declarative.declared_attr
@orm.declared_attr
def created_at(cls):
return sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.now)
return schema.Column(types.DateTime, nullable=False, default=datetime.datetime.now)
@declarative.declared_attr
@orm.declared_attr
def updated_at(cls):
return sqlalchemy.Column(sqlalchemy.DateTime, nullable=True, onupdate=datetime.datetime.now)
return schema.Column(types.DateTime, nullable=True, onupdate=datetime.datetime.now)
@declarative.declared_attr
@orm.declared_attr
def comment(cls):
return sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
return schema.Column(types.String(255), nullable=True)
db = flask_sqlalchemy.SQLAlchemy(model_class=Base)
......@@ -71,14 +74,21 @@ class JSONEncoded(db.TypeDecorator):
return json.loads(value) if value else None
membership = db.Table('membership', db.Model.metadata,
db.Column('user_uuid', db.ForeignKey('user.uuid'), primary_key=True),
db.Column('group_uuid', db.ForeignKey('group.uuid'), primary_key=True)
)
class User(db.Model):
""" A user is the local representation of an authenticated person.
"""
__tablename__ = "user"
username = db.Column(db.String(255), nullable=False, unique=True)
name = db.Column(db.String(255), nullable=False, unique=True)
is_admin = db.Column(db.Boolean(), nullable=False, default=False)
contact = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
groups = db.relationship("Group", secondary=membership, backref="users")
# Flask-login attributes
is_authenticated = True
......@@ -91,13 +101,13 @@ class User(db.Model):
@classmethod
def login(cls, username, password):
user = cls.query.filter_by(username=username).first()
user = cls.query.filter_by(name=username).first()
if not user:
return False
auths = user.auths
if not auths:
if not auths[Auth.PASSWORD]:
return False
if not auths[0].check_password(password):
if not auths[Auth.PASSWORD].check_password(password):
return False
return user
......@@ -108,22 +118,92 @@ class User(db.Model):
profile = Profile()
profile.service = service
profile.user = self
profile.username = self.username
profile.name = self.name
profile.uuid = self.uuid
profile.status = Profile.ACTIVE
return profile
@classmethod
def get_timeout(cls):
""" Return the configured timeout delta as a timedelta
"""
return (
datetime.datetime.now() -
datetime.timedelta(seconds=int(app.config.get("USER_TIMEOUT")))
)
def time_to_deletion(self):
""" Return timeout for the current account or None
"""
if self.profiles.filter(Profile.status != Profile.PURGED).count():
return None
if User.get_timeout() < self.created_at:
return None
return User.get_timeout() - (self.updated_at or self.created_at)
@classmethod
def delete_unused(cls):
""" Delete unused accounts
"""
unused = (cls.query
.join(cls.profiles.and_(Profile.status != Profile.PURGED), isouter=True)
.filter(Profile.uuid == None)
.filter(
cls.get_timeout() >
sql.func.coalesce(cls.updated_at, cls.created_at)
))
for user in unused.all():
print("Deleting user {}".format(user.name))
db.session.delete(user)
db.session.commit()
@classmethod
def exist_username(cls, username):
return cls.query.filter(cls.name.ilike(username)).first()
def reserved_username(self, username):
return self.name == username.lower()
def available_username(self, username, service, spoof_allowed=False):
return not Profile.exist_profilename(username, service=service) and (
not User.exist_username(username)
or self.reserved_username(username)
or spoof_allowed
)
class Group(db.Model):
""" A group is an instance that a user can be attached to.
"""
__tablename__ = "group"
groupname = db.Column(db.String(255), nullable=False, unique=True)
class Auth(db.Model):
""" An authenticator is a method to authenticate a user.
"""
__tablename__ = "auth"
PASSWORD = "password"
TOTP = "totp"
BADGES = {
PASSWORD: "primary",
TOTP: "info"
}
def __init__(self, realm, enabled=False):
self.realm = realm
self.enabled = enabled
realm = db.Column(db.String(25), server_default=PASSWORD)
enabled = db.Column(db.Boolean(), nullable=False, default=1)
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
user = db.relationship(User,
backref=db.backref('auths', cascade='all, delete-orphan'))
# TODO: support multiple authentication realms, therefore more than
# passwords
backref=db.backref('auths',
collection_class=orm.attribute_mapped_collection('realm'),
cascade='all, delete-orphan'))
value = db.Column(db.String)
extra = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
......@@ -133,6 +213,14 @@ class Auth(db.Model):
def check_password(self, password):
return hash.pbkdf2_sha256.verify(password, self.value)
def set_otp_key(self):
self.value = pyotp.random_base32()
def check_totp(self, totp):
Totp = pyotp.TOTP(self.value)
return Totp.verify(totp)
class Service(db.Model):
""" A service is a client application (SP or RP typically).
......@@ -159,13 +247,10 @@ class Service(db.Model):
description = db.Column(db.String())
policy = db.Column(db.String(255))
max_profiles = db.Column(db.Integer(), nullable=False, default=1)
profile_format = db.Column(db.String(255))
profile_format = db.Column(db.String(255), nullable=False, default='lowercase')
single_profile = db.Column(db.Boolean(), nullable=False, default=False)
config = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
def check_username(self, username):
return Profile.query.filter_by(service_uuid=self.uuid, username=username).first()
class Profile(db.Model):
""" A profile is a per-service custom identity.
......@@ -187,36 +272,61 @@ class Profile(db.Model):
DONE = "done"
STATUSES = {
UNCLAIMED: ("gray", _("unclaimed")),
REQUEST: ("blue", _("requested")),
ACTIVE: ("green", _("active")),
BLOCKED: ("orange", _("blocked")),
DELETED: ("red", _("deleted")),
PURGED: ("red", _("purged"))
# Translators: this qualifier refers to a profile.
UNCLAIMED: ("secondary", _("unclaimed")),
# Translators: this qualifier refers to a profile.
REQUEST: ("info", _("requested")),
# Translators: this qualifier refers to a profile.
ACTIVE: ("success", _("active")),
# Translators: this qualifier refers to a profile.
BLOCKED: ("warning", _("blocked")),
# Translators: this qualifier refers to a profile.
DELETED: ("danger", _("deleted")),
# Translators: this qualifier refers to a profile.
PURGED: ("dark", _("purged"))
}
TRANSITIONS = {
# Assigning or claiming is not a generic transition
# so it is not listed here (it requires chosing a user)
"activate": (REQUEST, ACTIVE, 30, True, _("activate")),
"reject": (REQUEST, DELETED, 3600 * 24 * 3, True, _("reject")),
"block": (ACTIVE, BLOCKED, 0, True, _("block")),
"unblock": (BLOCKED, ACTIVE, 30, True, _("unblock")),
"delete": (ACTIVE, DELETED, 3600 * 24, False, _("delete")),
"delete-blocked": (BLOCKED, DELETED, 3600, True, _("delete")),
"purge": (ACTIVE, PURGED, 3600 * 24 * 3, True, _("purge")),
"purge-blocked": (BLOCKED, PURGED, 0*3600, True, _("purge")),
"purge-deleted": (DELETED, PURGED, 0*3600, True, _("purge"))
"assign": actions.Transition("assign",
label=(_("assign")), label_alt=(_("assigned")), description=(_("assign this profile to a user")),
icon="person-fill-up", from_=(UNCLAIMED,), to=ACTIVE
),
"activate": actions.Transition("activate",
label=(_("activate")), label_alt=(_("activated")), description=(_("activate this profile")),
icon="check", from_=(REQUEST,), to=ACTIVE
),
"reject": actions.Transition("reject",
label=(_("reject")), label_alt=(_("rejected")), description=(_("reject this request")),
icon="user-times", from_=(REQUEST,), to=PURGED
),
"block": actions.Transition("block",
label=(_("block")), label_alt=(_("blocked")), description=(_("block this profile")),
icon="slash-circle-fill", from_=(ACTIVE,), to=BLOCKED
),
"unblock": actions.Transition("unblock",
label=(_("unblock")), label_alt=(_("unblocked")), description=(_("unblock this blocked profile")),
icon="check-circle-fill", from_=(BLOCKED,), to=ACTIVE
),
"delete": actions.Transition("delete",
label=(_("delete")), label_alt=(_("deleted")), description=(_("delete this profile")),
icon="x-circle-fill", from_=(ACTIVE, BLOCKED), to=DELETED,
admin_only=False, delay=120
),
"purge": actions.Transition("purge",
label=(_("purge")), label_alt=(_("purged")), description=(_("delete and purge this profile")),
icon="trash-fill", from_=(UNCLAIMED, ACTIVE, BLOCKED, DELETED), to=PURGED,
delay=120
)
}
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
service_uuid = db.Column(db.String(36), db.ForeignKey(Service.uuid))
user = db.relationship(User,
backref=db.backref('profiles', cascade='all, delete-orphan', lazy='dynamic'))
backref=db.backref('profiles', lazy='dynamic'))
service = db.relationship(Service,
backref=db.backref('profiles', cascade='all, delete-orphan', lazy='dynamic'))
username = db.Column(db.String(255), nullable=False)
name = db.Column(db.String(255), nullable=False)
status = db.Column(db.String(25), nullable=False)
server_status = db.Column(db.String(25))
transition = db.Column(db.String(25))
......@@ -228,6 +338,14 @@ class Profile(db.Model):
def email(self):
return "{}@{}".format(self.uuid, app.config.get("MAIL_DOMAIN"))
@property
def actions(self):
result = [
action for action in Profile.TRANSITIONS.values()
if action.authorized(self)
]
return result
@classmethod
def filter(cls, service, user):
return cls.query.filter_by(
......@@ -237,41 +355,17 @@ class Profile(db.Model):
@classmethod
def transition_ready(cls):
return cls.query.filter(
cls.transition_step.in_ ([cls.START, cls.DONE]) or
(
cls.transition_step == cls.INIT and
return cls.query.filter(sql.expression.or_(
cls.transition_step.in_ ([cls.START, cls.DONE]),
sql.expression.and_(
cls.transition_step == cls.INIT,
datetime.datetime.now() > cls.transition_time
)
)
def transitions(self, actor):
return {
name: transition for name, transition in Profile.TRANSITIONS.items()
if transition[0] == self.status and not self.transition_step
and (actor.is_admin or (self.uuid == actor.uuid and not transition[3]))
}
))
def transition_delta(self, formatted=False):
delta = self.transition_time - datetime.datetime.now() if self.transition_time else 0
return flask_babel.format_timedelta(delta) if formatted else delta
def set_transition(self, transition, actor):
""" Prepare the profile for transition
"""
_, _, delta, _, _ = Profile.TRANSITIONS[transition]
self.transition = transition
self.transition_step = Profile.INIT
self.transition_time = datetime.datetime.now() + datetime.timedelta(seconds=delta)
log(
category=History.TRANSITION,
value=transition,
user=self.user,
service=self.service,
profile=self,
actor=actor,
public=True
)
@classmethod
def exist_profilename(cls, profilename, service):
return cls.query.filter_by(service_uuid=service.uuid).filter(cls.name.ilike(profilename)).first()
class ClaimName(db.Model):
......@@ -283,18 +377,8 @@ class ClaimName(db.Model):
service_uuid = db.Column(db.String(36), db.ForeignKey(Service.uuid))
profile = db.relationship(Profile,
backref=db.backref('claimnames', cascade='all, delete-orphan', lazy='dynamic'))
username = db.Column(db.String(255), nullable=False)
class ResetToken(db.Model):
""" A reset token is used to reset authentication for a given user.
"""
__tablename__ = "resettoken"
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
user = db.relationship(User)
expired_at = db.Column(sqlalchemy.DateTime, nullable=False)
name = db.Column(db.String(255), nullable=False)
class History(db.Model):
......@@ -304,32 +388,41 @@ class History(db.Model):
SIGNUP = "signup"
CREATE = "create"
EDIT = "edit"
DELETE = "delete"
NOTE = "note"
STATUS = "status"
TRANSITION = "transition"
PASSWORD = "password"
MFA = "mfa"
GROUP = "group"
DESCRIPTION = {
SIGNUP: _("signed up for this account"),
CREATE: _("created the profile {this.profile.username} on {this.service.name}"),
CREATE: _("created the profile {this.profile.name} on {this.service.name}"),
EDIT: _("edited the profile {this.profile.name} on {this.service.name}"),
PASSWORD: _("changed this account password"),
STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}"),
TRANSITION: _("did {this.transition[4]} the profile {this.profile.username} on {this.service.name}")
MFA: _("modified this account multi-factor authentication (MFA) setting"),
STATUS: _("set the {this.service.name} profile {this.profile.name} as {this.value}"),
TRANSITION: _("{this.transition.label_alt} the profile {this.profile.name} on {this.service.name}"),
GROUP: _("modified the group {this.group.groupname}")
}
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
profile_uuid = db.Column(db.String(36), db.ForeignKey(Profile.uuid))
service_uuid = db.Column(db.String(36), db.ForeignKey(Service.uuid))
actor_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
group_uuid = db.Column(db.String(36), db.ForeignKey(Group.uuid))
user = db.relationship(User, foreign_keys=[user_uuid],
backref=db.backref('history', cascade='all, delete-orphan', lazy='dynamic'))
backref=db.backref('history', lazy='dynamic'))
profile = db.relationship(Profile,
backref=db.backref('history', cascade='all, delete-orphan', lazy='dynamic'))
backref=db.backref('history', lazy='dynamic'))
service = db.relationship(Service,
backref=db.backref('history', cascade='all, delete-orphan', lazy='dynamic'))
backref=db.backref('history', lazy='dynamic'))
actor = db.relationship(User, foreign_keys=[actor_uuid],
backref=db.backref('actions', cascade='all, delete-orphan', lazy='dynamic'))
backref=db.backref('actions', lazy='dynamic'))
group = db.relationship(Group, foreign_keys=[group_uuid],
backref=db.backref('history', lazy='dynamic'))
public = db.Column(db.Boolean(), default=True)
category = db.Column(db.String(25))
......
import flask
blueprint = flask.Blueprint("moderation", __name__, template_folder="templates")
from hiboo.moderation import views
{% extends "base.html" %}
{% from "bootstrap5/pagination.html" import render_pagination %}
{% block title %}{% trans %}Moderation{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}Activity logs{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead>
<tr>
<th>{% trans %}Category{% endtrans %}</th>
<th>{% trans %}Timestamp{% endtrans %}</th>
<th>{% trans %}Scope{% endtrans %}</th>
<th>{% trans %}Message{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ macros.history_category_icons(event) }} {{ event.category|upper }}</td>
<td>{{ event.created_at.isoformat(sep=" ", timespec="seconds") }}</td>
<td>
{% if event.user.name %}
<span class="badge border rounded-pill
bg-secondary-subtle border-secondary-subtle
text-secondary-emphasis">
{{ render_icon("person-fill") }}{{ event.user.name }}
</span>
{% endif %}
{% if event.group.groupname %}
<span class="badge border rounded-pill
bg-secondary-subtle border-secondary-subtle
text-secondary-emphasis">{{ render_icon("diagram-3-fill") }}
{{ event.group.groupname }}
</span>
{% endif %}
</td>
<td>
<b>{% if event.actor.is_admin %}{{ render_icon("shield-shaded") }}{% endif %}
{{ event.actor.name or event.user.name }}
</b>
{{ event.description }}
{% if event.comment %}
<br><span class="text-secondary-emphasis">{{ event.comment }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if (events.page) and (events.has_next or events.has_prev) %}
{{ render_pagination(events, align="center") }}
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".pending_profiles") }}" class="btn btn-outline-primary">
{% trans %}Pending profiles{% endtrans %}</a>
{% endblock %}
This diff is collapsed.
from hiboo.moderation import blueprint
from hiboo import security, models
import flask
@blueprint.route("/")
@security.admin_required()
def pending_profiles():
page = flask.request.args.get('page', 1, type=int)
profiles = models.Profile.query.filter(
models.db.or_(
models.Profile.status==models.Profile.REQUEST,
models.Profile.status==models.Profile.BLOCKED,
models.Profile.status==models.Profile.UNCLAIMED,
models.Profile.transition.is_not(None),
)).order_by(models.Profile.status.desc()).paginate(page=page, per_page=25)
return flask.render_template("moderation_profiles.html", profiles=profiles)
@blueprint.route("/activity")
@security.admin_required()
def activity():
page = flask.request.args.get('page', 1, type=int)
events = models.History.query.order_by(
models.History.created_at.desc()).paginate(page=page, per_page=25)
return flask.render_template("moderation_activity.html", events=events)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.