Add admin panel, arbitrary queries
This commit is contained in:
47
db/sqlite.py
47
db/sqlite.py
@@ -340,5 +340,52 @@ class DB:
|
||||
# User already exists, probably
|
||||
return False
|
||||
|
||||
def get_users(self):
|
||||
return self._db().execute('''
|
||||
select user_id, name, join_time, role, banned_until
|
||||
from users
|
||||
''',
|
||||
)
|
||||
|
||||
def set_forum_name(self, forum_id, name):
|
||||
return self.change_one('''
|
||||
update forums
|
||||
set name = ?
|
||||
where forum_id = ?
|
||||
''',
|
||||
(name, forum_id)
|
||||
)
|
||||
|
||||
def set_forum_description(self, forum_id, description):
|
||||
return self.change_one('''
|
||||
update forums
|
||||
set description = ?
|
||||
where forum_id = ?
|
||||
''',
|
||||
(description, forum_id)
|
||||
)
|
||||
|
||||
def add_forum(self, name, description):
|
||||
db = self._db()
|
||||
db.execute('''
|
||||
insert into forums(name, description)
|
||||
values (?, ?)
|
||||
''',
|
||||
(name, description)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
def change_one(self, query, values):
|
||||
db = self._db()
|
||||
c = db.cursor()
|
||||
c.execute(query, values)
|
||||
if c.rowcount > 0:
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def query(self, q):
|
||||
return self._db().execute(q)
|
||||
|
||||
def _db(self):
|
||||
return sqlite3.connect(self.conn)
|
||||
|
||||
65
main.py
65
main.py
@@ -308,6 +308,67 @@ def register():
|
||||
answer = answer,
|
||||
)
|
||||
|
||||
@app.route('/admin/')
|
||||
def admin():
|
||||
user = get_user()
|
||||
if user is None:
|
||||
return redirect(url_for('login'))
|
||||
if not user.is_admin():
|
||||
return '<h1>Forbidden</h1>', 403
|
||||
|
||||
return render_template(
|
||||
'admin/index.html',
|
||||
title = 'Admin panel',
|
||||
forums = db.get_forums(),
|
||||
users = db.get_users(),
|
||||
)
|
||||
|
||||
@app.route('/admin/query/', methods = ['GET', 'POST'])
|
||||
def admin_query():
|
||||
user = get_user()
|
||||
if user is None:
|
||||
return redirect(url_for('login'))
|
||||
if not user.is_admin():
|
||||
return '<h1>Forbidden</h1>', 403
|
||||
|
||||
try:
|
||||
rows = db.query(request.form['q']) if request.method == 'POST' else []
|
||||
except Exception as e:
|
||||
flash(e, 'error')
|
||||
rows = []
|
||||
return render_template(
|
||||
'admin/query.html',
|
||||
title = 'Query',
|
||||
rows = rows,
|
||||
)
|
||||
|
||||
@app.route('/admin/forum/<int:forum_id>/edit/<string:what>/', methods = ['POST'])
|
||||
def admin_edit_forum(forum_id, what):
|
||||
try:
|
||||
if what == 'description':
|
||||
res = db.set_forum_description(forum_id, request.form['description'].replace('\r', ''))
|
||||
elif what == 'name':
|
||||
res = db.set_forum_name(forum_id, request.form['name'])
|
||||
else:
|
||||
flash(f'Unknown property "{what}"', 'error')
|
||||
res = None
|
||||
if res is True:
|
||||
flash(f'Updated {what}', 'success')
|
||||
elif res is False:
|
||||
flash(f'Failed to update {what}', 'error')
|
||||
except Exception as e:
|
||||
flash(e, 'error')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
@app.route('/admin/forum/new/', methods = ['POST'])
|
||||
def admin_new_forum():
|
||||
try:
|
||||
db.add_forum(request.form['name'], request.form['description'].replace('\r', ''))
|
||||
flash('Added forum', 'success')
|
||||
except Exception as e:
|
||||
flash(str(e), 'error')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
|
||||
class Comment:
|
||||
def __init__(self, id, author_id, author, text, create_time, modify_time, parent_id):
|
||||
@@ -407,6 +468,9 @@ def utility_processor():
|
||||
# This shouldn't be reachable, but it's still better to return something
|
||||
return "incredibly long ago"
|
||||
|
||||
def format_time(t):
|
||||
return datetime.utcfromtimestamp(t / 10 ** 9)
|
||||
|
||||
def minimd(text):
|
||||
# Replace angle brackets to prevent XSS
|
||||
# Also replace ampersands to prevent surprises.
|
||||
@@ -420,6 +484,7 @@ def utility_processor():
|
||||
|
||||
return {
|
||||
'format_since': format_since,
|
||||
'format_time': format_time,
|
||||
'minimd': minimd,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ create table users (
|
||||
email varchar(254),
|
||||
about text not null default '',
|
||||
join_time integer not null,
|
||||
role integer not null default 0
|
||||
role integer not null default 0,
|
||||
banned_until integer
|
||||
);
|
||||
|
||||
create table threads (
|
||||
@@ -34,10 +35,9 @@ create table comments (
|
||||
);
|
||||
|
||||
create table forums (
|
||||
forum_id integer unique not null primary key autoincrement,
|
||||
forum_id integer unique not null primary key autoincrement,
|
||||
name varchar(64) not null,
|
||||
description text,
|
||||
allowed_roles_mask integer not null
|
||||
description text not null default ''
|
||||
);
|
||||
|
||||
-- Both of these speed up searches significantly if there are many threads or comments.
|
||||
|
||||
45
templates/admin/base.html
Normal file
45
templates/admin/base.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{# Don't use the default theme to emphasize this page is special -#}
|
||||
<!doctype html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.flash.success {
|
||||
background-color: lightgreen;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
}
|
||||
.flash.error {
|
||||
background-color: #ff4646;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 80%;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
textarea {
|
||||
width: 95%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>
|
||||
<a href="{{ url_for('admin') }}">Admin panel</a>
|
||||
<a href="{{ url_for('index') }}">Home page</a>
|
||||
</p>
|
||||
{%- for category, msg in get_flashed_messages(True) -%}
|
||||
<p class="flash {{ category }}">{{ msg }}</p>
|
||||
{%- endfor %}
|
||||
{%- block content %}{% endblock -%}
|
||||
</body>
|
||||
101
templates/admin/index.html
Normal file
101
templates/admin/index.html
Normal file
@@ -0,0 +1,101 @@
|
||||
{% extends 'admin/base.html' -%}
|
||||
{% block content -%}
|
||||
<h2>Query</h2>
|
||||
<p>⚠ Only use queries if you know what you're doing ⚠</p>
|
||||
<form action=query/ method=post>
|
||||
<input type=text name=q placeholder="SELECT * from users">
|
||||
<input type=submit value=Submit>
|
||||
</form>
|
||||
<h2>Configuration</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<input type=text value="Agrepy">
|
||||
<input type=submit value=Set>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Registrations enabled</td>
|
||||
<td>
|
||||
<input type=checkbox checked>
|
||||
<input type=submit value=Set>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2>Forums</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for id, name, description, _, _, _ in forums %}
|
||||
<tr>
|
||||
<td>{{ id }}</td>
|
||||
<td>
|
||||
<form method=post action="forum/{{ id }}/edit/name/">
|
||||
<input type=text name=name value="{{ name }}"</input>
|
||||
<input type=submit value="Set name">
|
||||
</form>
|
||||
<td>
|
||||
<form method=post action="forum/{{ id }}/edit/description/">
|
||||
<textarea name=description>{{ description }}</textarea>
|
||||
<input type=submit value="Set description">
|
||||
</form>
|
||||
</td>
|
||||
<td><a href="#">Remove</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<h3>Add forum</h3>
|
||||
<form method=post action="forum/new/">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td><input type=text name=name></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td><textarea name=description></textarea></td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type=submit value="Add forum">
|
||||
</form>
|
||||
<h2>Users</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Join date</th>
|
||||
<th>Role</th>
|
||||
<th>Banned until</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{%- for id, name, join_date, role, banned_until in users -%}
|
||||
<tr>
|
||||
<td>{{ id }}</td>
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ format_time(join_date) }}</td>
|
||||
<td>{{ ['user', 'moderator', 'admin'][role] if role >= 0 and role <= 2 else role }}</td>
|
||||
<td>{{ '' if banned_until is none else format_time(banned_until) }}</td>
|
||||
<td>
|
||||
<form method=post action="user/{{ id }}/edit/ban/">
|
||||
<input type=number name=days placeholder=days>
|
||||
<input type=time name=time>
|
||||
<input type=submit value=Ban>
|
||||
</form>
|
||||
<form method=post action="user/{{ id }}/edit/role/">
|
||||
<select name=role>
|
||||
<option value=0>user</option>
|
||||
<option value=1>moderator</option>
|
||||
<option value=2>admin</option>
|
||||
</select>
|
||||
<input type=submit value="Set role">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
{%- endblock %}
|
||||
17
templates/admin/query.html
Normal file
17
templates/admin/query.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'admin/base.html' -%}
|
||||
{% block content -%}
|
||||
<form method=post>
|
||||
<input type=text name=q placeholder="SELECT * from users">
|
||||
<input type=submit value=Submit>
|
||||
</form>
|
||||
<table style="font-family:monospace;white-space:pre">
|
||||
{%- for r in rows -%}
|
||||
<tr>
|
||||
{%- for c in r -%}
|
||||
<td>{{ c }}</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
{%- endblock -%}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
{% if user is not none %}
|
||||
<a href="{{ url_for('user_edit') }}">{{ user.name }}</a>
|
||||
<span>|</span>
|
||||
{% if user.is_admin() %}
|
||||
<a href="{{ url_for('admin') }}">Admin panel</a>
|
||||
<span>|</span>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout') }}">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<p><a href="{{ url_for('forum', forum_id = id) }}"><b>{{ name }}</b></a></p>
|
||||
<p>{{ description }}</p>
|
||||
<p>{{ minimd(description) | safe }}</p>
|
||||
</td>
|
||||
{% if t_id %}
|
||||
<td>
|
||||
@@ -21,6 +21,6 @@
|
||||
<td>No threads</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,15 +12,16 @@ insert into users (name, password, email, join_time) values (
|
||||
"bar@foo.baz",
|
||||
0
|
||||
);
|
||||
insert into users (name, password, join_time) values (
|
||||
insert into users (name, password, join_time, role) values (
|
||||
"bazzers",
|
||||
-- e
|
||||
"$argon2id$v=19$m=65536,t=3,p=4$9v5fS2ktxTinNEbIGUOoFQ$LMdEuAuuTCJ7utOE88+nXn7o6R/DEKY8ZA6wV+YkVGQ",
|
||||
0
|
||||
0,
|
||||
2
|
||||
);
|
||||
|
||||
insert into forums (name, description, allowed_roles_mask)
|
||||
values ("Earth", "The totality of all space and time; all that is, has been, and will be.", 1);
|
||||
insert into forums (name, description)
|
||||
values ("Earth", "The totality of all space and time; all that is, has been, and will be.");
|
||||
|
||||
insert into threads (author_id, forum_id, create_time, modify_time, update_time, title, text)
|
||||
values (1, 1, 0, 0, 0, "Hello, world!",
|
||||
|
||||
Reference in New Issue
Block a user