diff options
-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"> |