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 1140 additions and 0 deletions
import flask
blueprint = flask.Blueprint("group", __name__, template_folder="templates")
from hiboo.group import cli, views
from hiboo.group import blueprint
from hiboo import models
import click
import terminaltables
@blueprint.cli.command()
@click.argument("groupname")
@click.argument("comment", required=False, default="")
def create(groupname, comment):
""" Create a new group.
"""
assert not models.Group.query.filter_by(groupname=groupname).first()
group = models.Group(
groupname=groupname,
comment=comment
)
models.db.session.add(group)
models.db.session.commit()
@blueprint.cli.command()
def list():
""" List all groups.
"""
click.echo(terminaltables.SingleTable(
[('uuid', 'groupname', 'comment', 'users')] +
[
[
group.uuid, group.groupname, group.comment,
','.join([user.name for user in group.users])
]
for group in models.Group.query.all()
], title='Groups').table
)
@blueprint.cli.command()
@click.argument("groupname")
@click.argument("usernames", nargs=-1, required=True)
def add_user(groupname, usernames):
""" Add one or many users to a group
"""
group = models.Group.query.filter_by(groupname=groupname).first()
assert group
actual_users = group.users
selected_users = []
for username in usernames:
user = models.User.query.filter_by(name=username).first()
selected_users.append(user)
group.users = [*actual_users, *selected_users]
models.db.session.add(group)
models.db.session.commit()
from hiboo import utils
from hiboo.format import NameFormat
from wtforms import validators, fields
from flask_babel import lazy_gettext as _
import flask_wtf
class GroupCreateEdit(flask_wtf.FlaskForm):
formatter = NameFormat.registry["alnumSpace"]
groupname = fields.StringField(
_('Group name'),
formatter.validators(),
description = format(formatter.message)
)
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Edit group'))
class GroupUserForm(flask_wtf.FlaskForm):
users = utils.MultiCheckboxField(_('Available users', coerce=int))
pre_populate = utils.MultiCheckboxField(_('Users already in the group', coerce=int))
submit = fields.SubmitField(_('Update group memberships'))
{% 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 %}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 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 datetime
import json
import uuid
import pyotp
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()
event.category = category
event.value = value
event.comment = comment
event.user = user
event.group = group
event.profile = profile
event.service = service
event.actor = actor
event.public = public
db.session.add(event)
class Base(orm.DeclarativeBase):
""" Base class for all models
"""
metadata = schema.MetaData(
naming_convention={
"fk": "%(table_name)s_%(column_0_name)s_fkey",
"pk": "%(table_name)s_pkey"
}
)
@orm.declared_attr
def uuid(cls):
return schema.Column(types.String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
@orm.declared_attr
def created_at(cls):
return schema.Column(types.DateTime, nullable=False, default=datetime.datetime.now)
@orm.declared_attr
def updated_at(cls):
return schema.Column(types.DateTime, nullable=True, onupdate=datetime.datetime.now)
@orm.declared_attr
def comment(cls):
return schema.Column(types.String(255), nullable=True)
db = flask_sqlalchemy.SQLAlchemy(model_class=Base)
class JSONEncoded(db.TypeDecorator):
""" Represents an immutable structure as a json-encoded string.
"""
impl = db.String
def process_bind_param(self, value, dialect):
return json.dumps(value) if value else None
def process_result_value(self, value, dialect):
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"
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
is_active = True
is_anonymous = False
@classmethod
def get(cls, id):
return cls.query.get(id)
@classmethod
def login(cls, username, password):
user = cls.query.filter_by(name=username).first()
if not user:
return False
auths = user.auths
if not auths[Auth.PASSWORD]:
return False
if not auths[Auth.PASSWORD].check_password(password):
return False
return user
def get_id(self):
return self.uuid
def get_default_profile(self, service):
profile = Profile()
profile.service = service
profile.user = self
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',
collection_class=orm.attribute_mapped_collection('realm'),
cascade='all, delete-orphan'))
value = db.Column(db.String)
extra = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
def set_password(self, password):
self.value = hash.pbkdf2_sha256.hash(password)
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).
"""
__tablename__ = "service"
LOCKED = "locked"
RESERVED = "reserved"
MANAGED = "managed"
BURST = "burst"
OPEN = "open"
POLICIES = {
LOCKED: _("Profile creation is impossible"),
RESERVED: _("Profile creation is reserved to managers"),
MANAGED: _("Profile creation must be validated"),
BURST: _("Additional profiles must be validated"),
OPEN: _("No validation is required")
}
name = db.Column(db.String(255))
provider = db.Column(db.String(255))
application_id = db.Column(db.String(255))
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), nullable=False, default='lowercase')
single_profile = db.Column(db.Boolean(), nullable=False, default=False)
config = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
class Profile(db.Model):
""" A profile is a per-service custom identity.
"""
__tablename__ = "profile"
# Profile statuses
UNCLAIMED = "unclaimed"
REQUEST = "request"
ACTIVE = "active"
BLOCKED = "blocked"
DELETED = "deleted"
PURGED = "purged"
# Transitions steps
INIT = "init"
MANUAL = "manual"
START = "start"
DONE = "done"
STATUSES = {
# 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 = {
"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', lazy='dynamic'))
service = db.relationship(Service,
backref=db.backref('profiles', cascade='all, delete-orphan', lazy='dynamic'))
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))
transition_step = db.Column(db.String(25))
transition_time = db.Column(db.DateTime())
extra = db.Column(mutable.MutableDict.as_mutable(JSONEncoded))
@property
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(
service_uuid=service.uuid,
user_uuid=user.uuid,
).filter(cls.status.in_((cls.ACTIVE, cls.BLOCKED, cls.REQUEST)))
@classmethod
def transition_ready(cls):
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
)
))
@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):
""" A profile might have multiple claimable names.
"""
__tablename__ = "claimname"
profile_uuid = db.Column(db.String(36), db.ForeignKey(Profile.uuid))
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'))
name = db.Column(db.String(255), nullable=False)
class History(db.Model):
""" Records an even in an account's or profile's lifetime.
"""
__tablename__ = "history"
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.name} on {this.service.name}"),
EDIT: _("edited the profile {this.profile.name} on {this.service.name}"),
PASSWORD: _("changed this account password"),
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', lazy='dynamic'))
profile = db.relationship(Profile,
backref=db.backref('history', lazy='dynamic'))
service = db.relationship(Service,
backref=db.backref('history', lazy='dynamic'))
actor = db.relationship(User, foreign_keys=[actor_uuid],
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))
value = db.Column(db.String())
@property
def transition(self):
return Profile.TRANSITIONS[self.value]
@property
def description(self):
return History.DESCRIPTION.get(self.category, "").format(this=self)
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 %}
{% extends "base.html" %}
{% from 'bootstrap5/pagination.html' import render_pagination %}
{% block title %}{% trans %}Moderation{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}Pending profiles{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-head-fixed text-nowrap">
<thead>
<tr>
<th>{% trans %}Service{% endtrans %}</th>
<th>{% trans %}Profile username{% endtrans %}</th>
<th>{% trans %}Owned by{% endtrans %}</th>
<th>{% trans %}Created on{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for profile in profiles %}
<tr>
<td>{{ profile.service.name }}</td>
<td><a href="{{ url_for("profile.details", profile_uuid=profile.uuid, service_uuid=profile.service.uuid) }}">{{ profile.name }}</a></td>
{% if profile.user_uuid %}
<td><a href="{{ url_for("user.details", user_uuid=profile.user_uuid) }}">{{ profile.user.name }}</a></td>
{% else %}
<td>-</td>
{% endif %}
<td>{{ profile.created_at.date() }}</td>
<td>{{ macros.profile_status(profile) }}</td>
<td>
{% for action in profile.actions %}
<a href="{{ action.url(profile=profile) }}">{{ action.label | capitalize }}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if (profiles.page) and (profiles.has_next or profiles.has_prev) %}
{{ render_pagination(profiles, align="center") }}
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".activity") }}" class="btn btn-outline-primary">
{% trans %}Activity logs{% endtrans %}</a>
{% endblock %}
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)
import flask
blueprint = flask.Blueprint("profile", __name__, template_folder="templates")
import flask_login
from hiboo import models, utils
from hiboo.profile import forms, cli, views
def get_profile(service, **redirect_args):
profiles = models.Profile.filter(service, flask_login.current_user)
active = profiles.filter_by(status=models.Profile.ACTIVE)
form = forms.ProfilePickForm()
if form.validate_on_submit():
return active.filter_by(uuid=form.profile_uuid.data).first()
if active.count() == 1:
return active.one()
elif profiles.count() > 0:
utils.force_redirect(utils.url_for("profile.pick", **redirect_args))
else:
utils.force_redirect(utils.url_for("profile.create_quick", **redirect_args))
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{% extends "base.html" %}
{% block title %}{{ profile.name }}{% endblock %}
{% block subtitle %}{{ label.lower() }}{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">{{ result | safe }}</div>
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for("profile.details", profile_uuid=profile.uuid) }}"
class="btn btn-info">{% trans %}Show profile{% endtrans %}</a>
{% endblock %}
This diff is collapsed.