diff options
author | B. Stack <bgstack15@gmail.com> | 2022-02-12 12:07:05 -0500 |
---|---|---|
committer | B. Stack <bgstack15@gmail.com> | 2022-02-12 12:09:13 -0500 |
commit | bd134ed333278f33c9b5596ef5df2501ee648bb1 (patch) | |
tree | 6c4dbcec2cd71f15c43c20b3ca505941041af928 | |
parent | use app.config for salt (diff) | |
download | stackbin-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.md | 31 | ||||
-rw-r--r-- | pastebin.py | 75 | ||||
-rw-r--r-- | templates/admin.html | 21 | ||||
-rw-r--r-- | templates/layout.html | 2 |
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 %}✓{% 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"> |