mirror of
https://github.com/Tygs/0bin.git
synced 2023-08-10 21:13:00 +03:00
404 css jolifygationnement merge
This commit is contained in:
commit
425a947051
@ -3,3 +3,4 @@ clize==4.1.1
|
|||||||
lockfile==0.12.2
|
lockfile==0.12.2
|
||||||
sigtools==2.0.2
|
sigtools==2.0.2
|
||||||
bottle==0.12.18
|
bottle==0.12.18
|
||||||
|
Beaker==1.11.0
|
||||||
|
@ -7,10 +7,16 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import secrets
|
import hashlib
|
||||||
import _thread as thread
|
import _thread as thread
|
||||||
|
|
||||||
from zerobin.utils import settings, SettingsValidationError, drop_privileges
|
from zerobin.utils import (
|
||||||
|
settings,
|
||||||
|
SettingsValidationError,
|
||||||
|
drop_privileges,
|
||||||
|
ensure_var_env,
|
||||||
|
hash_password,
|
||||||
|
)
|
||||||
from zerobin.routes import get_app
|
from zerobin.routes import get_app
|
||||||
from zerobin.paste import Paste
|
from zerobin.paste import Paste
|
||||||
|
|
||||||
@ -43,13 +49,7 @@ def runserver(
|
|||||||
settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH
|
settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH
|
||||||
settings.DEBUG = bool(debug) if debug is not None else settings.DEBUG
|
settings.DEBUG = bool(debug) if debug is not None else settings.DEBUG
|
||||||
|
|
||||||
settings.VAR_DIR.mkdir(exist_ok=True, parents=True)
|
ensure_var_env()
|
||||||
settings.PASTE_FILES_ROOT.mkdir(exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
secret_key_file = settings.VAR_DIR / "secret_key"
|
|
||||||
if not secret_key_file.is_file():
|
|
||||||
secret_key_file.write_text(secrets.token_urlsafe(64))
|
|
||||||
settings.SECRET_KEY = secret_key_file.read_text()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, app = get_app(debug, settings_file, compressed_static, settings=settings)
|
_, app = get_app(debug, settings_file, compressed_static, settings=settings)
|
||||||
@ -60,6 +60,7 @@ def runserver(
|
|||||||
thread.start_new_thread(drop_privileges, (settings.USER, settings.GROUP))
|
thread.start_new_thread(drop_privileges, (settings.USER, settings.GROUP))
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
print(f"Admin URL: http://{settings.HOST}:{settings.PORT}{settings.ADMIN_URL}")
|
||||||
run(
|
run(
|
||||||
app, host=settings.HOST, port=settings.PORT, reloader=True, server=server,
|
app, host=settings.HOST, port=settings.PORT, reloader=True, server=server,
|
||||||
)
|
)
|
||||||
@ -104,13 +105,51 @@ def delete_paste(*pastes, quiet=False):
|
|||||||
print("Paste {} doesn't exist".format(paste_uuid))
|
print("Paste {} doesn't exist".format(paste_uuid))
|
||||||
|
|
||||||
|
|
||||||
|
def print_admin_url():
|
||||||
|
""" Print the route to the 0bin admin.
|
||||||
|
|
||||||
|
The admin route is generated by zerobin so that bots won't easily
|
||||||
|
bruteforce it. To get the full URL, simply preppend your website domain
|
||||||
|
name to it.
|
||||||
|
|
||||||
|
E.G:
|
||||||
|
|
||||||
|
If this command prints:
|
||||||
|
|
||||||
|
"/admin/f1cc3972a4b933c734b37906940cf69886161492ee4eb7c1faff5d7b5e92efb8"
|
||||||
|
|
||||||
|
Then the admin url is:
|
||||||
|
|
||||||
|
"http://yourdomain.com/admin/f1cc3972a4b933c734b37906940cf69886161492ee4eb7c1faff5d7b5e92efb8"
|
||||||
|
|
||||||
|
Adapt "http" and "yourdomain.com" to your configuration.
|
||||||
|
|
||||||
|
In debug mode, the dev server will print the url when starting.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ensure_var_env()
|
||||||
|
print(settings.ADMIN_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def set_admin_password(password):
|
||||||
|
""" Set the password for the admin
|
||||||
|
|
||||||
|
It will be stored as a scrypt hash in a file in the var dir.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ensure_var_env()
|
||||||
|
settings.ADMIN_PASSWORD_FILE.write_bytes(hash_password(password))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
subcommands = [runserver, delete_paste]
|
subcommands = [runserver, delete_paste, print_admin_url, set_admin_password]
|
||||||
subcommand_names = [
|
subcommand_names = [
|
||||||
clize.util.name_py2cli(name)
|
clize.util.name_py2cli(name)
|
||||||
for name in clize.util.dict_from_names(subcommands).keys()
|
for name in clize.util.dict_from_names(subcommands).keys()
|
||||||
]
|
]
|
||||||
if len(sys.argv) < 2 or sys.argv[1] not in subcommand_names:
|
if len(sys.argv) < 2 or sys.argv[1] not in subcommand_names:
|
||||||
sys.argv.insert(1, subcommand_names[0])
|
sys.argv.insert(1, subcommand_names[0])
|
||||||
clize.run(runserver, delete_paste)
|
clize.run(runserver, delete_paste, print_admin_url, set_admin_password)
|
||||||
|
|
||||||
|
@ -19,11 +19,6 @@ STATIC_FILES_ROOT = ROOT_DIR / "static"
|
|||||||
# otherwise, will use the ordinary files
|
# otherwise, will use the ordinary files
|
||||||
COMPRESSED_STATIC_FILES = False
|
COMPRESSED_STATIC_FILES = False
|
||||||
|
|
||||||
# absolute path where the paste files should be store
|
|
||||||
# default in projectdirectory/static/content/
|
|
||||||
# use "/" even under Windows
|
|
||||||
PASTE_FILES_ROOT = VAR_DIR / "content"
|
|
||||||
|
|
||||||
# A tuple of absolute paths of directory where to look the template for
|
# A tuple of absolute paths of directory where to look the template for
|
||||||
# the first one will be the first to be looked into
|
# the first one will be the first to be looked into
|
||||||
# if you want to override, it needs to be it a directory at the begining of
|
# if you want to override, it needs to be it a directory at the begining of
|
||||||
@ -48,7 +43,12 @@ GROUP = None
|
|||||||
# Be carreful if your site have to many pastes this can hurt your hard drive performances.
|
# Be carreful if your site have to many pastes this can hurt your hard drive performances.
|
||||||
# Refresh counter interval. Default to every minute after a paste.
|
# Refresh counter interval. Default to every minute after a paste.
|
||||||
DISPLAY_COUNTER = True
|
DISPLAY_COUNTER = True
|
||||||
REFRESH_COUNTER = 60 * 1
|
REFRESH_COUNTER = 60 * 1 # Fill this if you want to
|
||||||
|
ADMIN_CREDENTIALS = {
|
||||||
|
"username": None,
|
||||||
|
"password": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Names/links to insert in the menu bar.
|
# Names/links to insert in the menu bar.
|
||||||
# Any link with "mailto:" will be escaped to prevent spam
|
# Any link with "mailto:" will be escaped to prevent spam
|
||||||
@ -68,3 +68,4 @@ MAX_SIZE = 1024 * 500
|
|||||||
# total number of unique pastes can be calculated as 2^(6*PASTE_ID_LENGTH)
|
# total number of unique pastes can be calculated as 2^(6*PASTE_ID_LENGTH)
|
||||||
# for PASTE_ID_LENGTH=8, for example, it's 2^(6*8) = 281 474 976 710 656
|
# for PASTE_ID_LENGTH=8, for example, it's 2^(6*8) = 281 474 976 710 656
|
||||||
PASTE_ID_LENGTH = 8
|
PASTE_ID_LENGTH = 8
|
||||||
|
|
||||||
|
@ -7,20 +7,37 @@ import sys
|
|||||||
|
|
||||||
import _thread as thread
|
import _thread as thread
|
||||||
|
|
||||||
import urllib.parse as urlparse
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from zerobin import __version__
|
|
||||||
from zerobin.utils import settings, SettingsValidationError, dmerge
|
|
||||||
|
|
||||||
import bottle
|
import bottle
|
||||||
from bottle import Bottle, static_file, view, request, HTTPResponse
|
from bottle import (
|
||||||
|
Bottle,
|
||||||
|
debug,
|
||||||
|
static_file,
|
||||||
|
view,
|
||||||
|
request,
|
||||||
|
HTTPResponse,
|
||||||
|
redirect,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
|
||||||
|
from beaker.middleware import SessionMiddleware
|
||||||
|
|
||||||
|
from zerobin import __version__
|
||||||
|
from zerobin.utils import (
|
||||||
|
settings,
|
||||||
|
SettingsValidationError,
|
||||||
|
ensure_var_env,
|
||||||
|
check_password,
|
||||||
|
)
|
||||||
from zerobin.paste import Paste
|
from zerobin.paste import Paste
|
||||||
|
|
||||||
|
|
||||||
app = Bottle()
|
ensure_var_env()
|
||||||
|
|
||||||
|
|
||||||
GLOBAL_CONTEXT = {
|
GLOBAL_CONTEXT = {
|
||||||
"settings": settings,
|
"settings": settings,
|
||||||
"VERSION": __version__,
|
"VERSION": __version__,
|
||||||
@ -29,23 +46,79 @@ GLOBAL_CONTEXT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app = Bottle()
|
||||||
|
|
||||||
|
ADMIN_LOGIN_URL = settings.ADMIN_URL + "login/"
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@view("home")
|
@view("home")
|
||||||
def index():
|
def index():
|
||||||
return GLOBAL_CONTEXT
|
return GLOBAL_CONTEXT
|
||||||
|
|
||||||
|
|
||||||
@app.route("/faq/")
|
@app.get("/faq/")
|
||||||
@view("faq")
|
@view("faq")
|
||||||
def faq():
|
def faq():
|
||||||
return GLOBAL_CONTEXT
|
return GLOBAL_CONTEXT
|
||||||
|
|
||||||
|
|
||||||
@app.route("/paste/create", method="POST")
|
@app.get(settings.ADMIN_URL)
|
||||||
|
@app.post(settings.ADMIN_URL)
|
||||||
|
@view("admin")
|
||||||
|
def admin():
|
||||||
|
session = request.environ.get("beaker.session")
|
||||||
|
if not session or not session.get("is_authenticated"):
|
||||||
|
redirect(ADMIN_LOGIN_URL)
|
||||||
|
|
||||||
|
paste_id = request.forms.get("paste", "")
|
||||||
|
if paste_id:
|
||||||
|
try:
|
||||||
|
if "/paste/" in paste_id:
|
||||||
|
paste_id = urlparse(paste_id).path.split("/path/")[-1]
|
||||||
|
paste = Paste.load(paste_id)
|
||||||
|
paste.delete()
|
||||||
|
except (TypeError, ValueError, FileNotFoundError):
|
||||||
|
return {"status": "error", "message": f"Cannot find paste '{paste_id}'"}
|
||||||
|
|
||||||
|
return {"status": "ok", "message": "Paste deleted"}
|
||||||
|
|
||||||
|
return {"status": "ok", **GLOBAL_CONTEXT}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(ADMIN_LOGIN_URL)
|
||||||
|
@app.post(ADMIN_LOGIN_URL)
|
||||||
|
@view("login")
|
||||||
|
def login():
|
||||||
|
|
||||||
|
password = request.forms.get("password")
|
||||||
|
if password:
|
||||||
|
if not check_password(password):
|
||||||
|
return {"status": "error", "message": "Wrong password", **GLOBAL_CONTEXT}
|
||||||
|
|
||||||
|
session = request.environ.get("beaker.session")
|
||||||
|
session["is_authenticated"] = True
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
redirect(settings.ADMIN_URL)
|
||||||
|
|
||||||
|
return {"status": "ok", **GLOBAL_CONTEXT}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(settings.ADMIN_URL + "logout/")
|
||||||
|
@view("logout")
|
||||||
|
def logout():
|
||||||
|
session = request.environ.get("beaker.session")
|
||||||
|
session["is_authenticated"] = False
|
||||||
|
session.save()
|
||||||
|
redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/paste/create")
|
||||||
def create_paste():
|
def create_paste():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body = urlparse.parse_qs(request.body.read(int(settings.MAX_SIZE * 1.1)))
|
body = parse_qs(request.body.read(int(settings.MAX_SIZE * 1.1)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {"status": "error", "message": "Wrong data payload."}
|
return {"status": "error", "message": "Wrong data payload."}
|
||||||
|
|
||||||
@ -95,7 +168,7 @@ def create_paste():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/paste/:paste_id", method="GET")
|
@app.get("/paste/:paste_id")
|
||||||
@view("paste")
|
@view("paste")
|
||||||
def display_paste(paste_id):
|
def display_paste(paste_id):
|
||||||
|
|
||||||
@ -125,11 +198,10 @@ def display_paste(paste_id):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return error404(ValueError)
|
return error404(ValueError)
|
||||||
|
|
||||||
context = {"paste": paste, "keep_alive": keep_alive}
|
return {"paste": paste, "keep_alive": keep_alive, **GLOBAL_CONTEXT}
|
||||||
return dmerge(context, GLOBAL_CONTEXT)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/paste/:paste_id", method="DELETE")
|
@app.delete("/paste/:paste_id")
|
||||||
def delete_paste(paste_id):
|
def delete_paste(paste_id):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -154,7 +226,7 @@ def error404(code):
|
|||||||
return GLOBAL_CONTEXT
|
return GLOBAL_CONTEXT
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/<filename:path>")
|
@app.get("/static/<filename:path>")
|
||||||
def server_static(filename):
|
def server_static(filename):
|
||||||
return static_file(filename, root=settings.STATIC_FILES_ROOT)
|
return static_file(filename, root=settings.STATIC_FILES_ROOT)
|
||||||
|
|
||||||
@ -187,3 +259,14 @@ def get_app(debug=None, settings_file="", compressed_static=None, settings=setti
|
|||||||
bottle.debug(True)
|
bottle.debug(True)
|
||||||
|
|
||||||
return settings, app
|
return settings, app
|
||||||
|
|
||||||
|
|
||||||
|
app = SessionMiddleware(
|
||||||
|
app,
|
||||||
|
{
|
||||||
|
"session.type": "file",
|
||||||
|
"session.cookie_expires": 300,
|
||||||
|
"session.data_dir": settings.SESSIONS_DIR,
|
||||||
|
"session.auto": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -2,9 +2,10 @@ import time
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
import tempfile
|
import tempfile
|
||||||
import sys
|
|
||||||
import codecs
|
import codecs
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from zerobin import default_settings
|
from zerobin import default_settings
|
||||||
@ -12,16 +13,10 @@ from zerobin import default_settings
|
|||||||
try:
|
try:
|
||||||
from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group
|
from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group
|
||||||
except (AttributeError):
|
except (AttributeError):
|
||||||
pass # privilege does't work on several plateform
|
pass # privilege does't work on several plateforms
|
||||||
|
|
||||||
try:
|
|
||||||
from runpy import run_path
|
from runpy import run_path
|
||||||
except ImportError:
|
|
||||||
# python-2.6 or earlier - use simplier less-optimized execfile()
|
|
||||||
def run_path(file_path):
|
|
||||||
mod_globals = {"__file__": file_path}
|
|
||||||
execfile(file_path, mod_globals)
|
|
||||||
return mod_globals
|
|
||||||
|
|
||||||
|
|
||||||
def drop_privileges(user=None, group=None, wait=5):
|
def drop_privileges(user=None, group=None, wait=5):
|
||||||
@ -47,17 +42,6 @@ def drop_privileges(user=None, group=None, wait=5):
|
|||||||
print("Failed to drop privileges. Running with current user.")
|
print("Failed to drop privileges. Running with current user.")
|
||||||
|
|
||||||
|
|
||||||
def dmerge(*args):
|
|
||||||
"""
|
|
||||||
Return new directionay being the sum of all merged dictionaries passed
|
|
||||||
as arguments
|
|
||||||
"""
|
|
||||||
dictionary = {}
|
|
||||||
for arg in args:
|
|
||||||
dictionary.update(arg)
|
|
||||||
return dictionary
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsValidationError(Exception):
|
class SettingsValidationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -134,3 +118,55 @@ def as_unicode(obj):
|
|||||||
return unicode(obj)
|
return unicode(obj)
|
||||||
except NameError:
|
except NameError:
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_var_env():
|
||||||
|
""" Ensure all the variable things we generate are available.
|
||||||
|
|
||||||
|
This will make sure we have:
|
||||||
|
|
||||||
|
- a var dir
|
||||||
|
- a content dir
|
||||||
|
- a secret key
|
||||||
|
- an admin URL
|
||||||
|
"""
|
||||||
|
|
||||||
|
settings.VAR_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
settings.PASTE_FILES_ROOT = settings.VAR_DIR / "content"
|
||||||
|
settings.PASTE_FILES_ROOT.mkdir(exist_ok=True)
|
||||||
|
settings.SESSIONS_DIR = settings.VAR_DIR / "sessions"
|
||||||
|
settings.SESSIONS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
secret_key_file = settings.VAR_DIR / "secret_key"
|
||||||
|
if not secret_key_file.is_file():
|
||||||
|
secret_key_file.write_text(secrets.token_urlsafe(64))
|
||||||
|
settings.SECRET_KEY = secret_key_file.read_text()
|
||||||
|
|
||||||
|
admin_password_file = settings.VAR_DIR / "admin_password"
|
||||||
|
if not secret_key_file.is_file():
|
||||||
|
admin_password_file.write_text(
|
||||||
|
"No password set. Use the set_admin_passord command. Don't write this file by hand."
|
||||||
|
)
|
||||||
|
settings.ADMIN_PASSWORD_FILE = admin_password_file
|
||||||
|
|
||||||
|
payload = ("admin" + settings.SECRET_KEY).encode("ascii")
|
||||||
|
settings.ADMIN_URL = "/admin/" + hashlib.sha256(payload).hexdigest() + "/"
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password):
|
||||||
|
return hashlib.scrypt(
|
||||||
|
password.encode("utf8"),
|
||||||
|
salt=settings.SECRET_KEY.encode("ascii"),
|
||||||
|
n=16384,
|
||||||
|
r=8,
|
||||||
|
p=1,
|
||||||
|
dklen=32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_password(password):
|
||||||
|
try:
|
||||||
|
return settings.ADMIN_PASSWORD_FILE.read_bytes() == hash_password(password)
|
||||||
|
except (FileNotFoundError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
25
zerobin/views/admin.tpl
Normal file
25
zerobin/views/admin.tpl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<form action="./delete/" method="post">
|
||||||
|
%if status == "error":
|
||||||
|
<div class="alert alert-danger" role="alert alert-danger">
|
||||||
|
{{message}}
|
||||||
|
</div>
|
||||||
|
%end
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Paste to delete</label>
|
||||||
|
<input name="paste" type="text" class="form-control" placeholder="Paste URL or ID">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-black">Delete</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="./logout/" method="post">
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-black">Logout</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
% rebase('base', settings=settings, pastes_count=pastes_count)
|
@ -35,14 +35,14 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="submenu"><a href="#" @click.prevent="openPreviousPastesMenu = !openPreviousPastesMenu">Previous
|
<li class="submenu"><a href="#" @click.prevent="openPreviousPastesMenu = !openPreviousPastesMenu">Previous
|
||||||
pastes +</a>
|
pastes v</a>
|
||||||
<ul class="previous-pastes" id="topmenu" v-if="openPreviousPastesMenu"
|
<ul class="previous-pastes" id="topmenu" v-if="openPreviousPastesMenu"
|
||||||
@mouseleave="openPreviousPastesMenu =false">
|
@mouseleave="openPreviousPastesMenu =false">
|
||||||
<li class="item active" v-if="previousPastes.length === 0">
|
<li class="item active" v-if="previousPastes.length === 0">
|
||||||
<a href="#">No paste yet...</a>
|
<a href="#">No paste yet</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="item active" v-for="paste in previousPastes">
|
<li class="item active" v-for="paste in previousPastes">
|
||||||
<a :href="paste.link" @click="forceLoad(paste.link)">{% paste.displayDate %}.</a>
|
<a :href="paste.link" @click="forceLoad(paste.link)">{% paste.displayDate %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
18
zerobin/views/login.tpl
Normal file
18
zerobin/views/login.tpl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<form action="." method="post">
|
||||||
|
<div class="login-form">
|
||||||
|
<form>
|
||||||
|
<label>Password</label>
|
||||||
|
%if status == "error":
|
||||||
|
<div class="alert alert-danger" role="alert alert-danger">
|
||||||
|
{{message}}
|
||||||
|
</div>
|
||||||
|
%end
|
||||||
|
<input type="password" class="form-control" placeholder="Password" name="password">
|
||||||
|
<button type="submit" class="btn btn-black">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
% rebase('base', settings=settings, pastes_count=pastes_count)
|
Loading…
x
Reference in New Issue
Block a user