Merge pull request #15 from DarkflameUniverse/audit-log

Audit log
This commit is contained in:
Aaron Kimbrell 2022-02-11 23:06:45 -06:00 committed by GitHub
commit 9cc3dbb4c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 18 deletions

View File

@ -13,7 +13,7 @@ from flask_apscheduler import APScheduler
from app.luclient import query_cdclient, register_luclient_jinja_helpers from app.luclient import query_cdclient, register_luclient_jinja_helpers
from app.commands import init_db, init_accounts, load_property, gen_image_cache, gen_model_cache from app.commands import init_db, init_accounts, load_property, gen_image_cache, gen_model_cache
from app.models import Account, AccountInvitation from app.models import Account, AccountInvitation, AuditLog
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@ -245,3 +245,9 @@ def gm_level(gm_level):
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
return decorator return decorator
def log_audit(message):
AuditLog(
account_id=current_user.id,
action=message
).save()

View File

@ -6,7 +6,7 @@ import datetime
import time import time
from app.models import Account, AccountInvitation, db from app.models import Account, AccountInvitation, db
from app.schemas import AccountSchema from app.schemas import AccountSchema
from app import gm_level from app import gm_level, log_audit
from app.forms import EditGMLevelForm from app.forms import EditGMLevelForm
accounts_blueprint = Blueprint('accounts', __name__) accounts_blueprint = Blueprint('accounts', __name__)
@ -46,8 +46,10 @@ def edit_gm_level(id):
form = EditGMLevelForm() form = EditGMLevelForm()
if form.validate_on_submit(): if form.validate_on_submit():
log_audit(f"Changed ({account_data.id}){account_data.username}'s GM Level from {account_data.gm_level} to {form.gm_level.data}")
account_data.gm_level = form.gm_level.data account_data.gm_level = form.gm_level.data
account_data.save() account_data.save()
return redirect(url_for('accounts.view', id=account_data.id)) return redirect(url_for('accounts.view', id=account_data.id))
form.gm_level.data = account_data.gm_level form.gm_level.data = account_data.gm_level
@ -63,8 +65,10 @@ def lock(id):
account.locked = not account.locked account.locked = not account.locked
account.save() account.save()
if account.locked: if account.locked:
log_audit(f"Locked ({account.id}){account.username}")
flash("Locked Account", "danger") flash("Locked Account", "danger")
else: else:
log_audit(f"Unlocked ({account.id}){account.username}")
flash("Unlocked account", "success") flash("Unlocked account", "success")
return redirect(request.referrer if request.referrer else url_for("main.index")) return redirect(request.referrer if request.referrer else url_for("main.index"))
@ -77,8 +81,10 @@ def ban(id):
account.banned = not account.banned account.banned = not account.banned
account.save() account.save()
if account.banned: if account.banned:
log_audit(f"Banned ({account.id}){account.username}")
flash("Banned Account", "danger") flash("Banned Account", "danger")
else: else:
log_audit(f"Unbanned ({account.id}){account.username}")
flash("Unbanned account", "success") flash("Unbanned account", "success")
return redirect(request.referrer if request.referrer else url_for("main.index")) return redirect(request.referrer if request.referrer else url_for("main.index"))
@ -90,10 +96,12 @@ def mute(id, days=0):
account = Account.query.filter(Account.id == id).first() account = Account.query.filter(Account.id == id).first()
if days == "0": if days == "0":
account.mute_expire = 0 account.mute_expire = 0
log_audit(f"Unmuted ({account.id}){account.username}")
flash("Unmuted Account", "success") flash("Unmuted Account", "success")
else: else:
muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days)) muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days))
account.mute_expire = muted_intil.timestamp() account.mute_expire = muted_intil.timestamp()
log_audit(f"Muted ({account.id}){account.username} for {days} days")
flash(f"Muted account for {days} days", "danger") flash(f"Muted account for {days} days", "danger")
account.save() account.save()

View File

@ -5,7 +5,7 @@ from datatables import ColumnDT, DataTables
import datetime, time import datetime, time
from app.models import CharacterInfo, CharacterXML, Account, db from app.models import CharacterInfo, CharacterXML, Account, db
from app.schemas import CharacterInfoSchema from app.schemas import CharacterInfoSchema
from app import gm_level from app import gm_level, log_audit
import xmltodict import xmltodict
character_blueprint = Blueprint('characters', __name__) character_blueprint = Blueprint('characters', __name__)
@ -26,16 +26,19 @@ def approve_name(id, action):
character = CharacterInfo.query.filter(CharacterInfo.id == id).first() character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
if action == "approve": if action == "approve":
log_audit(f"Approved ({character.id}){character.pending_name} from {character.name}")
flash(
f"Approved ({character.id}){character.pending_name} from {character.name}",
"success"
)
if character.pending_name: if character.pending_name:
character.name = character.pending_name character.name = character.pending_name
character.pending_name = "" character.pending_name = ""
character.needs_rename = False character.needs_rename = False
flash(
f"Approved name {character.name}",
"success"
)
elif action == "rename": elif action == "rename":
character.needs_rename = True character.needs_rename = True
log_audit(f"Marked character ({character.id}){character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename")
flash( flash(
f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename", f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
"danger" "danger"
@ -146,6 +149,9 @@ def restrict(id, bit):
abort(404) abort(404)
return return
log_audit(f"Updated ({character_data.id}){character_data.name}'s permission map to \
{character_data.permission_map ^ (1 << int(bit))} from {character_data.permission_map}")
character_data.permission_map ^= (1 << int(bit)) character_data.permission_map ^= (1 << int(bit))
character_data.save() character_data.save()

View File

@ -3,7 +3,7 @@ from flask_user import login_required, current_user
from app.models import db, Mail, CharacterInfo from app.models import db, Mail, CharacterInfo
from datatables import ColumnDT, DataTables from datatables import ColumnDT, DataTables
from app.forms import SendMailForm from app.forms import SendMailForm
from app import gm_level from app import gm_level, log_audit
from app.luclient import translate_from_locale, query_cdclient from app.luclient import translate_from_locale, query_cdclient
import time import time
@ -29,6 +29,7 @@ def send():
if form.attachment.data != "0" and form.attachment_count.data == 0: if form.attachment.data != "0" and form.attachment_count.data == 0:
form.attachment_count.data = 1 form.attachment_count.data = 1
if form.recipient.data == "0": if form.recipient.data == "0":
log_audit(f"Sending {form.subject.data}: {form.body.data} to All Characters with {form.attachment_count.data} of item {form.attachment.data}")
for character in CharacterInfo.query.all(): for character in CharacterInfo.query.all():
Mail( Mail(
sender_id = 0, sender_id = 0,
@ -42,6 +43,7 @@ def send():
attachment_lot = form.attachment.data, attachment_lot = form.attachment.data,
attachment_count = form.attachment_count.data attachment_count = form.attachment_count.data
).save() ).save()
log_audit(f"Sent {form.subject.data}: {form.body.data} to ({character.id}){character.name} with {form.attachment_count.data} of item {form.attachment.data}")
else: else:
Mail( Mail(
sender_id = 0, sender_id = 0,
@ -55,6 +57,7 @@ def send():
attachment_lot = form.attachment.data, attachment_lot = form.attachment.data,
attachment_count = form.attachment_count.data attachment_count = form.attachment_count.data
).save() ).save()
log_audit(f"Sent {form.subject.data}: {form.body.data} to ({form.recipient.data}){CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name} with {form.attachment_count.data} of item {form.attachment.data}")
flash("Sent Mail", "success") flash("Sent Mail", "success")
return redirect(url_for('mail.send')) return redirect(url_for('mail.send'))

View File

@ -1,6 +1,6 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_user import UserMixin from flask_user import UserMixin, current_user
from wtforms import ValidationError from wtforms import ValidationError
import logging import logging
@ -1010,3 +1010,42 @@ class Reports(db.Model):
def delete(self): def delete(self):
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
class AuditLog(db.Model):
__tablename__ = 'audit_logs'
id = db.Column(
mysql.INTEGER,
primary_key=True
)
account_id = db.Column(
db.Integer(),
db.ForeignKey(Account.id, ondelete='CASCADE'),
nullable=False,
)
account = db.relationship(
'Account',
backref="audit_logs",
passive_deletes=True
)
action = db.Column(
mysql.TEXT,
nullable=False
)
date = db.Column(
mysql.TIMESTAMP,
nullable=False,
server_default=db.func.now()
)
def save(self):
db.session.add(self)
db.session.commit()
db.session.refresh(self)
def delete(self):
db.session.delete(self)
db.session.commit()

View File

@ -3,7 +3,7 @@ from flask_user import login_required
from app.models import PetNames, db from app.models import PetNames, db
from datatables import ColumnDT, DataTables from datatables import ColumnDT, DataTables
from app.forms import CreatePlayKeyForm, EditPlayKeyForm from app.forms import CreatePlayKeyForm, EditPlayKeyForm
from app import gm_level from app import gm_level, log_audit
moderation_blueprint = Blueprint('moderation', __name__) moderation_blueprint = Blueprint('moderation', __name__)
@ -23,6 +23,7 @@ def approve_pet(id):
pet_data = PetNames.query.filter(PetNames.id == id).first() pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data.approved = 2 pet_data.approved = 2
log_audit(f"Approved pet name {pet_data.pet_name}")
flash(f"Approved pet name {pet_data.pet_name}", "success") flash(f"Approved pet name {pet_data.pet_name}", "success")
pet_data.save() pet_data.save()
return redirect(request.referrer if request.referrer else url_for("main.index")) return redirect(request.referrer if request.referrer else url_for("main.index"))
@ -36,6 +37,7 @@ def reject_pet(id):
pet_data = PetNames.query.filter(PetNames.id == id).first() pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data.approved = 0 pet_data.approved = 0
log_audit(f"Rejected pet name {pet_data.pet_name}")
flash(f"Rejected pet name {pet_data.pet_name}", "danger") flash(f"Rejected pet name {pet_data.pet_name}", "danger")
pet_data.save() pet_data.save()
return redirect(request.referrer if request.referrer else url_for("main.index")) return redirect(request.referrer if request.referrer else url_for("main.index"))

View File

@ -3,7 +3,7 @@ from flask_user import login_required, current_user
from app.models import Account, AccountInvitation, PlayKey, db from app.models import Account, AccountInvitation, PlayKey, db
from datatables import ColumnDT, DataTables from datatables import ColumnDT, DataTables
from app.forms import CreatePlayKeyForm, EditPlayKeyForm from app.forms import CreatePlayKeyForm, EditPlayKeyForm
from app import gm_level from app import gm_level, log_audit
play_keys_blueprint = Blueprint('play_keys', __name__) play_keys_blueprint = Blueprint('play_keys', __name__)
@ -21,6 +21,7 @@ def index():
@gm_level(9) @gm_level(9)
def create(count=1, uses=1): def create(count=1, uses=1):
PlayKey.create(count=count, uses=uses) PlayKey.create(count=count, uses=uses)
log_audit(f"Created {count} Play Key(s) with {uses} uses!")
flash(f"Created {count} Play Key(s) with {uses} uses!", "success") flash(f"Created {count} Play Key(s) with {uses} uses!", "success")
return redirect(url_for('play_keys.index')) return redirect(url_for('play_keys.index'))
@ -32,6 +33,8 @@ def bulk_create():
form = CreatePlayKeyForm() form = CreatePlayKeyForm()
if form.validate_on_submit(): if form.validate_on_submit():
PlayKey.create(count=form.count.data, uses=form.uses.data) PlayKey.create(count=form.count.data, uses=form.uses.data)
log_audit(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!")
flash(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!", "success")
return redirect(url_for('play_keys.index')) return redirect(url_for('play_keys.index'))
return render_template('play_keys/bulk.html.j2', form=form) return render_template('play_keys/bulk.html.j2', form=form)
@ -43,6 +46,7 @@ def bulk_create():
def delete(id): def delete(id):
key = PlayKey.query.filter(PlayKey.id == id).first() key = PlayKey.query.filter(PlayKey.id == id).first()
associated_accounts = Account.query.filter(Account.play_key_id==id).all() associated_accounts = Account.query.filter(Account.play_key_id==id).all()
log_audit(f"Deleted Play Key {key.key_string}")
flash(f"Deleted Play Key {key.key_string}", "danger") flash(f"Deleted Play Key {key.key_string}", "danger")
key.delete() key.delete()
return redirect(url_for('play_keys.index')) return redirect(url_for('play_keys.index'))
@ -56,10 +60,16 @@ def edit(id):
form = EditPlayKeyForm() form = EditPlayKeyForm()
if form.validate_on_submit(): if form.validate_on_submit():
log_audit(f"Updated Play key {key.id} \
Uses: {key.key_uses}:{form.uses.data} \
Active: {key.active}:{form.active.data} \
Notes: {key.notes}:{form.notes.data} \
")
key.key_uses = form.uses.data key.key_uses = form.uses.data
key.active = form.active.data key.active = form.active.data
key.notes = form.notes.data key.notes = form.notes.data
key.save() key.save()
return redirect(url_for('play_keys.index')) return redirect(url_for('play_keys.index'))
form.uses.data = key.key_uses form.uses.data = key.key_uses

View File

@ -16,7 +16,7 @@ from datatables import ColumnDT, DataTables
import time import time
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account
from app.schemas import PropertySchema from app.schemas import PropertySchema
from app import gm_level from app import gm_level, log_audit
from app.luclient import query_cdclient from app.luclient import query_cdclient
import zlib import zlib
@ -50,25 +50,29 @@ def approve(id):
property_data.rejection_reason = "" property_data.rejection_reason = ""
if property_data.mod_approved: if property_data.mod_approved:
flash( message = f"""Approved Property
f"""Approved Property
{property_data.name if property_data.name else query_cdclient( {property_data.name if property_data.name else query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?', 'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data.zone_id], [property_data.zone_id],
one=True one=True
)[0]} )[0]}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""", from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
log_audit(message)
flash(
message,
"success" "success"
) )
else: else:
flash( message = f"""Unapproved Property
f"""Unapproved Property
{property_data.name if property_data.name else query_cdclient( {property_data.name if property_data.name else query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?', 'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data.zone_id], [property_data.zone_id],
one=True one=True
)[0]} )[0]}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""", from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
log_audit(message)
flash(
message,
"danger" "danger"
) )

View File

@ -0,0 +1,38 @@
"""fix nullables
Revision ID: 3132aaef7413
Revises: bd908969d8fe
Create Date: 2022-02-11 21:51:58.479066
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '3132aaef7413'
down_revision = 'bd908969d8fe'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('audit_logs', 'account_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.alter_column('audit_logs', 'action',
existing_type=mysql.TEXT(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('audit_logs', 'action',
existing_type=mysql.TEXT(),
nullable=True)
op.alter_column('audit_logs', 'account_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,35 @@
"""Add audit_log table
Revision ID: bd908969d8fe
Revises: aee4c6c24811
Create Date: 2022-02-11 21:48:03.798474
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'bd908969d8fe'
down_revision = 'aee4c6c24811'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('audit_logs',
sa.Column('id', mysql.INTEGER(), nullable=False),
sa.Column('account_id', sa.Integer(), nullable=True),
sa.Column('action', mysql.TEXT(), nullable=True),
sa.Column('date', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('audit_logs')
# ### end Alembic commands ###