aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2022-02-12 12:07:05 -0500
committerB. Stack <bgstack15@gmail.com>2022-02-12 12:09:13 -0500
commitbd134ed333278f33c9b5596ef5df2501ee648bb1 (patch)
tree6c4dbcec2cd71f15c43c20b3ca505941041af928
parentuse app.config for salt (diff)
downloadstackbin-bd134ed333278f33c9b5596ef5df2501ee648bb1.tar.gz
stackbin-bd134ed333278f33c9b5596ef5df2501ee648bb1.tar.bz2
stackbin-bd134ed333278f33c9b5596ef5df2501ee648bb1.zip
add admin page, delete function, and APPNAME var
The admin can view the links to private pastes, and can delete pastes from the web console. For now, the admin page is not protected, so this is not production-ready.
-rw-r--r--README-bgstack15.md31
-rw-r--r--pastebin.py75
-rw-r--r--templates/admin.html21
-rw-r--r--templates/layout.html2
4 files changed, 122 insertions, 7 deletions
diff --git a/README-bgstack15.md b/README-bgstack15.md
index 10815e2..e0cd9bc 100644
--- a/README-bgstack15.md
+++ b/README-bgstack15.md
@@ -1,17 +1,42 @@
+<!--
+ -- Filename: README-bgstack15.md
+ -- Startdate: 2022-02-11
+ --
+ -->
+# Overview
+This is my proposed solution to my pastebin problem.
+
+# Features
+
+* Admin page which can list parents, children, and provide link to delete pastes.
+* Editable titles
+* "Reply to" pastes to make parent/children relationships
+* UUIDs instead of sequential integer ID numbers
+* Private pastes (accessible to admin, and to users with the whole link)
+
# Instructions
Generate new db.
python3 initdb.py
-Run server.
+Run server in development mode.
FLASK_APP=pastebin.py FLASK_DEBUG=True flask run --host='0.0.0.0'
# Improvements
-I still need to practice these:
+I still need to work on these tasks:
+
+## Development
+
+* Protect the /admin/ page
+* Add expiry of pastes? (use existing pubdate value, or just an extra column with desired expiration timestamp)
+
+## Release
-* Support deleting somehow: from an admin panel, or a link on the page?
+* Read any of my flask projects (fuss is the best one) to learn how to setup prod server
+* Document centos7 dependencies
+* Deploy to prod
# Alternatives
diff --git a/pastebin.py b/pastebin.py
index 37fda53..4c529f9 100644
--- a/pastebin.py
+++ b/pastebin.py
@@ -29,9 +29,14 @@ class UUID(types.TypeDecorator):
return False
id_column_name = "id"
def id_column():
- #import uuid
return Column(id_column_name,UUID(),primary_key=True,default=uuid.uuid4)
+def get_signed(string, salt="blank"):
+ return Signer(app.secret_key, salt=salt).sign(str(string))
+
+def get_unsigned(string, salt="blank"):
+ return Signer(app.secret_key, salt=salt).unsign(str(string)).decode("utf-8")
+
app = Flask(__name__)
app.config.from_pyfile('config.cfg')
db = SQLAlchemy(app)
@@ -41,6 +46,7 @@ def url_for_other_page(page):
args['page'] = page
return url_for(request.endpoint, **args)
app.jinja_env.globals['url_for_other_page'] = url_for_other_page
+app.jinja_env.globals['appname'] = app.config['APPNAME']
@app.before_request
def check_user_status():
@@ -89,7 +95,7 @@ def new_paste():
paste = Paste(g.user, request.form['code'], title, parent=parent, is_private=is_private)
db.session.add(paste)
db.session.commit()
- sign = Signer(app.secret_key, salt=app.config['SALT']).sign(str(paste.id)) \
+ sign = get_signed(paste.id, salt=app.config['SALT']) \
if is_private else None
return redirect(url_for('show_paste', paste_id=paste.id, s=sign))
return render_template('new_paste.html', parent=parent)
@@ -105,7 +111,7 @@ def show_paste(paste_id):
try:
sign = request.args.get('s', '')
assert str(paste.id) == \
- Signer(app.secret_key, salt=app.config['SALT']).unsign(sign).decode("utf-8")
+ get_unsigned(sign, salt=app.config['SALT'])
except:
abort(403)
parent = None
@@ -126,3 +132,66 @@ def show_paste(paste_id):
k = j.id, j.title
children.append(k)
return render_template('show_paste.html', paste=paste, parent=parent, children=children)
+
+@app.route('/<paste_id>/delete/', methods=['POST'])
+@app.route('/<paste_id>/delete', methods=['POST'])
+def delete_paste(paste_id):
+ try:
+ paste = Paste.query.options(db.eagerload('children')).get_or_404(paste_id)
+ except:
+ paste = Paste.query.options(db.eagerload('children')).get_or_404(uuid.UUID(paste_id))
+ sign = str(request.form['s'])
+ try:
+ assert str(paste.id) == get_unsigned(sign, salt=app.config['DELETESALT'])
+ except:
+ abort(403)
+ try:
+ Paste.query.filter(Paste.id == paste.id).delete()
+ db.session.commit()
+ return "OK",200 # WORKHERE: make this and the 500 redirect to admin/
+ except:
+ return "failure to delete object.",500
+
+def get_all_pastes():
+ """
+ Get custom arrangement of pastes for Admin view
+ """
+ all1 = Paste.query.all()
+ all2 = []
+ for p1 in all1:
+ parent_id = None
+ parent_title = None
+ children = []
+ if p1.parent_id:
+ parent_id = p1.parent_id
+ try:
+ parent_title = Paste.query.get(p1.parent_id).title
+ except:
+ parent_title = "" # works better than None for the parent column of the generated html
+ if p1.children:
+ for c1 in p1.children:
+ child = Paste.query.get(c1.id)
+ child_title = child.title
+ c2 = c1.id, child_title
+ children.append(c2)
+ private = None
+ if p1.is_private:
+ private = get_signed(p1.id, salt=app.config['SALT'])
+ p2 = {
+ "id": p1.id,
+ "title": p1.title,
+ "private": private,
+ "user_id": p1.user_id,
+ "is_private": p1.is_private,
+ "parent": (parent_id, parent_title),
+ "children": children,
+ "delete": get_signed(p1.id, salt=app.config['DELETESALT']).decode("utf-8")
+ }
+ all2.append(p2)
+ return all2
+
+@app.route('/admin/')
+@app.route('/admin')
+def admin():
+ all_pastes = get_all_pastes()
+ return render_template('admin.html', pastes = all_pastes)
diff --git a/templates/admin.html b/templates/admin.html
new file mode 100644
index 0000000..a815fb5
--- /dev/null
+++ b/templates/admin.html
@@ -0,0 +1,21 @@
+{% extends "layout.html" %}
+{% block title %}Administration{% endblock %}
+{% block body %}
+<h1>Administration for {{ appname }}</h1>
+{% if pastes %}
+<table>
+<tr><th>id</th><th>private</th><th>title</th><th>user</th><th>parent</th><th>children</th><th>Actions</th></tr>
+{% for p in pastes %}
+<tr>
+<td>{{ p.id }}</td>
+<td>{% if p.private %}&#10003;{% endif %}</td>{# magic string is from utf8icons.com #}
+<td><a href="{% if not p.private %}{{ url_for('show_paste', paste_id=p.id) }}{% else %}{{ url_for('show_paste', paste_id=p.id, s=p.private) }}{% endif %}">{{ p.title }}</a></td>
+<td>{% if p.user %}{{ p.user }}{% endif%}</td>
+<td>{% if p.parent[0] %}<a href="{{ url_for('show_paste', paste_id=p.parent[0]) }}">{{ p.parent[1] }}</a>{% endif %}</td>
+<td>{% if p.children %}{% for c in p.children %}{% if not loop.first %},{% endif %}
+<a href="{{ url_for('show_paste', paste_id=c[0]) }}">{{ c[1] }}</a>{% endfor %}{% endif %}
+</td>
+<td><form method="post" action="{{ url_for('delete_paste', paste_id=p.id) }}"><input type="submit" value="delete"/> <input type="hidden" id="s" name="s" value="{{ p.delete }}"/></form></a></td>
+{% endfor %}
+{% endif %}
+{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
index bfe8b05..fc2d8e5 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -1,5 +1,5 @@
<!doctype html>
-<title>{% block title %}{% endblock %} | Flask Pastebin</title>
+<title>{% block title %}{% endblock %}{% if appname %} | {{ appname }}{% endif %}</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
bgstack15