Compare commits

...
8 Commits
Author SHA1 Message Date
wiktr 7acb426fea IZBI!!!1!!11 2026-09-10 00:38:16 +02:00
wiktr c3d61ada91 hotfix 2026-08-28 14:57:00 +02:00
wiktr 37442c9864 add CSRF protection 2026-08-28 14:48:58 +02:00
wiktr 8b94f36db5 fix Token db interface 2026-08-28 14:47:52 +02:00
wiktr 0bb7ca21cf finish rewrite 2026-08-25 16:20:27 +02:00
wiktr cfa862d4e5 partial frontend rewrite 2026-08-25 15:20:19 +02:00
wiktr 8e143acb1e rewrite backend 2026-08-25 15:16:31 +02:00
wiktr 1b5ea7be3a Add comments 2026-08-25 01:48:03 +02:00
9 changed files with 799 additions and 586 deletions

No files matched your search

+55 -14
View File
@@ -1,18 +1,59 @@
MIT License The izbi license 1.01
Copyright (c) 2026 wiktr Permission is hereby granted, free of charge, to any student and/or
graduate of Computer Science - Information Security Management
(pl. Informatyka - Zarządzanie Bezpieczeństwem Informacji "izbi")
from AGH University of Krakow ("AGH"), to freely use, modify and
redistribute the contents of this software, its source code
and any associated documentation ("full software").
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Every file that is written in human-readable format, meant to be read
associated documentation files (the "Software"), to deal in the Software without restriction, including by humans and not used by the software itself (documentation, readme, etc.),
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell has to end with the word "izbi", separated from the rest of the
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the aforementioned file by a newline character.
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial Only other students and/or staff of AGH's Faculty of Computer Science
portions of the Software. (pl. Wydział Informatyki Akademii Górniczo-Hutniczej "WI AGH"),
after being given the verbal (which may be revoked at any time by original authors) or written
permission from the author(s) of the full software may acquire the same rights
to the full software as the students of izbi, free of charge.
The use of this license by people affiliated with WI AGH is permitted only if they
know someone that attends and/or attended izbi.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT Any other person, who is unrelated to WI AGH and wishes to use this software is allowed to,
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO but cannot publish for profit or modify any version of the full software
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER or parts of its source code, including (but not limited to) merging and/or sublicensing,
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE except where such modification is private and cannot be discovered or
USE OR OTHER DEALINGS IN THE SOFTWARE. traced through the Clearnet or Deep web, in which case it is explicitly permitted.
Students of AGH's Faculty of Computer Science, Electronics and Telecommunications
(pl. Wydział Informatyki, Elektroniki i Telekomunikacji "WIET")
are not permitted to use and/or modify the full software.
However if they want to use the full software or its parts
they need to give 1 (one) full can of beer from the brand "Romper",
or any other beer stated in the beer.md file in which (if created),
every author needs to name the brand and type of beer they want to be given.
This beer must be given to every credited author of the full software,
even if parts of the software that the students of WIET used were not made by that person.
After supplying the author(s) with beer, WIET students are acquiring the
same rights as students of WI AGH.
The only people that are not allowed to use the full software in
any way, shape or form, including reading this license or any other file
are any past, present and future deans of WIET.
THE TEXT ABOVE HAS TO APPEAR IN ITS UNCHANGED FORM IN ANY COPIES
AND/OR FORKS OF FULL SOFTWARE THAT WERE NOT CREATED BY THE ORIGINAL
AUTHOR OF THE FULL SOFTWARE. THE ONLY PEOPLE WITH THE RIGHT TO
CHANGE THE LICENSE OF THE FULL SOFTWARE ARE THE AUTHORS
CREDITED IN THE HISTORY OF THE ORIGINAL REPOSITORY (OR SEPARATE
AUTHORS.md file).
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
izbi
+2
View File
@@ -1,3 +1,5 @@
# IZBI DNS # IZBI DNS
Dynamic DNS for `.izbi` domains. Dynamic DNS for `.izbi` domains.
izbi
+20 -398
View File
@@ -1,413 +1,35 @@
import sqlite3 import db
from os import urandom
from hashlib import sha256 from hashlib import sha256
def init_db(path: str) -> None: # Check if access token can update domain's ip
with sqlite3.connect(path) as connection:
cursor = connection.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS Tokens (
TKHASH TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL,
NAME TEXT NOT NULL
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS Domains (
DOMAIN TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS Users (
USERID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
USERNAME TEXT NOT NULL UNIQUE,
PWHASH TEXT NOT NULL,
PWSALT TEXT NOT NULL,
EMAIL TEXT,
IS_ADMIN INTEGER NOT NULL DEFAULT 0
)
''')
cursor.execute('''
SELECT COUNT(*) FROM Users
''')
if cursor.fetchone()[0] == 0:
username = "root"
password = urandom(8).hex()
create_user(path, username, password, is_admin=True)
print(f"No users detected, created new admin\nUser: {username}\nPass: {password}", flush=True)
def check_token_domain(db_path: str, token: str, domain: str) -> bool: def check_token_domain(db_path: str, token: str, domain: str) -> bool:
with sqlite3.connect(db_path) as connection: userid = db.Token.get_user(db_path, token)
cursor = connection.cursor()
token_hash = sha256(token.encode('utf-8')).hexdigest() domain = domain.strip()
cursor.execute(''' return domain in db.User.get_domains(db_path, userid)
SELECT COUNT(*) FROM Tokens
JOIN Domains ON Tokens.userid = Domains.userid
WHERE tkhash = ?
AND domain = ?
''', (token_hash, domain))
return cursor.fetchone()[0] == 1
# Verify user's passowrd for login. Return UserID on success, None on failure
def user_login(db_path: str, username: str, password: str) -> int | None: def user_login(db_path: str, username: str, password: str) -> int | None:
with sqlite3.connect(db_path) as connection: try:
cursor = connection.cursor() userid = db.User.get_id(db_path, username)
except ValueError:
cursor.execute('''
SELECT pwsalt FROM Users
WHERE username = ?
''', (username,))
password_salt = cursor.fetchone()
if password_salt is None:
return None
password_salt = password_salt[0]
password_hash = sha256( (password_salt + password).encode('utf-8') ).hexdigest()
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
AND pwhash = ?
''', (username, password_hash))
result = cursor.fetchone()
if result is None:
return None
return result[0]
def get_username(db_path: str, userid: int) -> str | None:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT username FROM Users
WHERE userid = ?
''', (userid, ))
username = cursor.fetchone()
if username is None:
return None
return username[0]
def get_user_domains(db_path: str, userid: int) -> list[str]:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (userid, ))
domains = cursor.fetchall()
domains = map(lambda x: x[0], domains)
return domains
def get_user_tokens(db_path: str, userid: int) -> list[str]:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT name FROM Tokens
WHERE userid = ?
''', (userid, ))
tokens = cursor.fetchall()
tokens = map(lambda x: x[0], tokens)
return tokens
def generate_user_token(db_path: str, userid: int, token_name: str) -> str | None:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
''', (userid, ))
if cursor.fetchone()[0] != 1:
return None
cursor.execute('''
SELECT COUNT(*) FROM Tokens
WHERE userid = ?
''', (userid, ))
if cursor.fetchone()[0] >= 5:
return None
cursor.execute('''
SELECT COUNT(*) FROM Tokens
WHERE userid = ?
AND name = ?
''', (userid, token_name))
if cursor.fetchone()[0] != 0:
return None
if not token_name.isalnum():
return None
if len(token_name) >= 16:
return None
for _ in range(10):
try:
token = urandom(32).hex()
token_hash = sha256(token.encode("utf-8")).hexdigest()
cursor.execute('''
INSERT INTO Tokens (tkhash, userid, name) VALUES (?, ?, ?)
''', (token_hash, userid, token_name))
return token
except sqlite3.IntegrityError:
pass
return None return None
pwsalt, pwhash = db.User.get_password_hash(db_path, userid)
def revoke_user_token(db_path: str, userid: int, token_name: str) -> None: password_hash = sha256( (pwsalt + password).encode('utf-8')).hexdigest()
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute(''' if pwhash == password_hash:
DELETE FROM Tokens return userid
WHERE userid = ?
AND name = ?
''', (userid, token_name))
def change_user_password(db_path: str, userid: int, oldpass: str, newpass: str) -> bool: return None
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute(''' # Change password of user (by user)
SELECT COUNT(*) FROM Users def change_user_password(db_path: str, userid: int, oldpass: str, newpass: str) -> None:
WHERE userid = ? username = db.User.get_username(db_path, userid)
''', (userid, ))
if cursor.fetchone()[0] != 1: if user_login(db_path, username, oldpass) is None:
return False raise ValueError("Incorrect password")
cursor.execute(''' db.User.set_password(db_path, userid, newpass)
SELECT pwsalt FROM Users
WHERE userid = ?
''', (userid, ))
salt = cursor.fetchone()
if salt is None:
return False
salt = salt[0]
oldpass_hash = sha256( (salt + oldpass).encode('utf-8')).hexdigest()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
AND pwhash = ?
''', (userid, oldpass_hash))
if cursor.fetchone()[0] != 1:
return False
return set_user_password(db_path, userid, newpass)
def set_user_password(db_path: str, userid: int, newpass: str) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
''', (userid, ))
if cursor.fetchone()[0] != 1:
return False
salt = urandom(16).hex()
newpass_hash = sha256( (salt + newpass).encode('utf-8')).hexdigest()
cursor.execute('''
UPDATE Users
SET
pwsalt = ?,
pwhash = ?
WHERE userid = ?
''', (salt, newpass_hash, userid))
return True
def is_admin(db_path: str, userid: int) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
AND IS_ADMIN = 1
''', (userid, ))
return cursor.fetchone()[0] == 1
def get_users(db_path: str) -> dict:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT userid, username, is_admin, email FROM Users
''')
users = cursor.fetchall()
users = list(map(lambda x: {"userid": x[0], "username": x[1], "domains": [], "email": x[3], "is_admin": bool(x[2])}, users))
for user in users:
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (user["userid"], ))
domains = cursor.fetchall()
domains = list(map(lambda x: x[0], domains))
user['domains'] = domains
return users
def create_user(db_path: str, username: str, password: str, domains: list = [], is_admin: bool = False, email: str = None):
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
username.strip()
if email is not None:
email.strip()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE username = ?
''', (username, ))
if cursor.fetchone()[0] != 0:
return "Username exists"
if username == "":
return "Username is empty"
if not username.isalnum():
return "Username is not alphanumeric"
if password == "":
return "Password empty"
if len(password) < 8:
return "Password is shorter than 8 characters"
salt = urandom(16).hex()
password_hash = sha256( (salt + password).encode('utf-8')).hexdigest()
cursor.execute('''
INSERT INTO Users (username, pwsalt, pwhash, email, is_admin) VALUES (?, ?, ?, ?, ?)
''', (username, salt, password_hash, email, is_admin))
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
''', (username, ))
userid = cursor.fetchone()[0]
for domain in domains:
domain = domain.strip()
if domain == "":
continue
cursor.execute('''
SELECT COUNT(*) FROM Domains
WHERE domain = ?
''', (domain, ))
if cursor.fetchone()[0] != 0:
continue
cursor.execute('''
INSERT INTO Domains (userid, domain) VALUES (?, ?)
''', (userid, domain))
def update_user(db_path: str, userid: int, domains: list[str] = None, password: str = None, is_admin: bool = None, email: str = None) -> tuple[bool, str]:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
''', (userid, ))
if cursor.fetchone()[0] != 1:
return False, "Uerid not found"
if domains is not None:
# crude check if domains is iterable
for domain in domains:
break
cursor.execute('''
DELETE FROM Domains
WHERE userid = ?
''', (userid, ))
for domain in domains:
cursor.execute('''
INSERT INTO Domains (userid, domain) VALUES (?, ?)
''', (userid, domain))
if password is not None:
set_user_password(db_path, userid, password)
if is_admin is not None:
cursor.execute('''
UPDATE Users
SET
is_admin = ?
WHERE userid = ?
''', (is_admin, userid))
if email is not None:
if email == "":
email = None
cursor.execute('''
UPDATE Users
SET
email = ?
WHERE userid = ?
''', (email, userid))
def delete_user(db_path: str, userid: int):
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('DELETE FROM Domains WHERE userid = ?', (userid,))
cursor.execute('DELETE FROM Tokens WHERE userid = ?', (userid,))
cursor.execute('DELETE FROM Users WHERE userid = ?', (userid,))
+432
View File
@@ -0,0 +1,432 @@
import sqlite3
from os import urandom
from hashlib import sha256
# Initialize database
def init(db_path: str) -> None:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
# Auth token table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Tokens (
TKHASH TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL,
NAME TEXT NOT NULL
)
''')
# Registered domains table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Domains (
DOMAIN TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL
)
''')
# Users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Users (
USERID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
USERNAME TEXT NOT NULL UNIQUE,
PWHASH TEXT NOT NULL,
PWSALT TEXT NOT NULL,
EMAIL TEXT,
IS_ADMIN INTEGER NOT NULL DEFAULT 0
)
''')
# Create a new admin if there are no accounts
cursor.execute('''
SELECT COUNT(*) FROM Users
''')
if cursor.fetchone()[0] == 0:
username = "root"
password = urandom(8).hex()
User.new(db_path, username, password, is_admin=True)
print(f"No users detected, created new admin\nUser: {username}\nPass: {password}", flush=True)
def list_users(db_path: str) -> list[dict]:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT userid, username, is_admin, email FROM Users
''')
users = cursor.fetchall()
users = list(map(lambda x: {"userid": x[0], "username": x[1], "domains": [], "email": x[3], "is_admin": x[2]}, users))
for user in users:
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (user["userid"], ))
domains = cursor.fetchall()
domains = list(map(lambda x: x[0], domains))
user['domains'] = domains
return users
# Collection of getters and setters
class User:
def exists(db_path: str, userid: int) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0] == 1
def new(db_path: str, username: str, password: str, domains: list = [], is_admin: bool = False, email: str = None) -> int:
username = username.strip()
if not username.isalnum():
raise ValueError("Username has to be alphanumeric")
if len(username) < 1:
raise ValueError("Username has to be at least 1 character long")
if len(password) > 16:
raise ValueError("Username has to be at most 16 characters long")
if len(password) < 8:
raise ValueError("Password has to be at least 8 characters long")
if len(password) > 128:
raise ValueError("Password has to be at most 128 characters long")
if email is not None:
email = email.strip()
if len(email) > 128:
raise ValueError("Email has to be at most 128 characters long, be reasonable")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
pwsalt = urandom(16).hex()
pwhash = sha256( (pwsalt + password).encode('utf-8') ).hexdigest()
cursor.execute('''
INSERT INTO Users (username, pwsalt, pwhash, email, is_admin) VALUES (?, ?, ?, ?, ?)
''', (username, pwsalt, pwhash, email, is_admin))
# Get new user's ID
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
''', (username, ))
userid = cursor.fetchone()[0]
User.set_domains(db_path, userid, domains)
return userid
def delete(db_path: str, userid: int):
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('DELETE FROM Domains WHERE userid = ?', (userid, ))
cursor.execute('DELETE FROM Tokens WHERE userid = ?', (userid, ))
cursor.execute('DELETE FROM Users WHERE userid = ?', (userid, ))
def is_admin(db_path: str, userid: int) -> bool:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT is_admin FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def set_admin(db_path: str, userid: int, is_admin: bool):
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
UPDATE Users
SET is_admin = ?
WHERE userid = ?
''', (is_admin, userid))
def get_username(db_path: str, userid: int) -> str:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT username FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def get_id(db_path: str, username: str) -> int:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
''', (username, ))
userid = cursor.fetchone()
if userid is None:
raise ValueError(f"User {username} does not exist")
return userid[0]
def get_domains(db_path: str, userid: int) -> list[str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (userid, ))
domains = cursor.fetchall()
domains = list(map(lambda x: x[0], domains))
return domains
def set_domains(db_path: str, userid: int, domains: list[str]) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
#TODO domain regex
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
DELETE FROM Domains
WHERE userid = ?
''', (userid, ))
for domain in domains:
domain = domain.strip()
if len(domain) < 1:
continue
cursor.execute('''
INSERT INTO Domains (domain, userid) VALUES (?, ?)
''', (domain, userid))
def get_token_names(db_path: str, userid: int) -> list[str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT name FROM Tokens
WHERE userid = ?
''', (userid, ))
token_names = cursor.fetchall()
token_names = list(map(lambda x: x[0], token_names))
return token_names
def generate_token(db_path: str, userid: int, token_name: str) -> str | None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if len(token_name) < 1:
raise ValueError("Token name has to be at least 1 character long")
if len(token_name) > 16:
raise ValueError("Token name has to be at most 16 characters long")
if not token_name.isalnum():
raise ValueError("Token name has to be alphanumeric")
token_name_list = User.get_token_names(db_path, userid)
if len(token_name_list) >= 5:
raise ValueError("One user can have at most 5 tokens")
if token_name in token_name_list:
raise ValueError("Token name is not unique")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
# Try 10 times in case of collisions
for _ in range(10):
token = urandom(32).hex()
token_hash = sha256(token.encode("utf-8")).hexdigest()
try:
cursor.execute('''
INSERT INTO Tokens (tkhash, userid, name) VALUES (?, ?, ?)
''', (token_hash, userid, token_name))
except sqlite3.IntegrityError:
continue
return token
return None
def revoke_token(db_path: str, userid: int, token_name: str) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if token_name not in User.get_token_names(db_path, userid):
raise ValueError("Token name not found")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
DELETE FROM Tokens
WHERE userid = ?
AND name = ?
''', (userid, token_name))
def get_password_hash(db_path: str, userid: int) -> tuple[str, str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT pwsalt, pwhash FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()
def set_password(db_path: str, userid: int, password: str) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if len(password) < 8:
raise ValueError("Password has to be at least 8 characters long")
if len(password) > 128:
raise ValueError("Password has to be at most 128 characters long")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
pwsalt = urandom(16).hex()
pwhash = sha256( (pwsalt + password).encode('utf-8') ).hexdigest()
cursor.execute('''
UPdATE Users
SET pwsalt = ?,
pwhash = ?
WHERE userid = ?
''', (pwsalt, pwhash, userid))
def get_email(db_path: str, userid: int) -> str:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT email FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def set_email(db_path: str, userid: int, email: str | None) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if email is not None:
email = email.strip()
if len(email) > 128:
raise ValueError("Email has to be at most 128 characters long, be reasonable")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
UPDATE Users
SET email = ?
WHERE userid = ?
''', (email, userid, ))
# Collection of getters
class Token:
def exists(db_path: str, token: str) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
tkhash = sha256(token.encode('utf-8')).hexdigest()
cursor.execute('''
SELECT COUNT(*) FROM Tokens
WHERE tkhash = ?
''', (tkhash, ))
return cursor.fetchone()[0] == 1
def get_user(db_path: str, token: str) -> int:
if not Token.exists(db_path, token):
raise ValueError("Token does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
tkhash = sha256(token.encode('utf-8')).hexdigest()
cursor.execute('''
SELECT userid FROM Tokens
WHERE tkhash = ?
''', (tkhash, ))
return cursor.fetchone()[0]
+248 -155
View File
@@ -1,25 +1,27 @@
from flask import Flask, jsonify, request, render_template, redirect, session, flash from flask import Flask, jsonify, request, render_template, redirect, session, flash
import auth import auth
import db
import re import re
from os import environ, urandom from os import environ, urandom
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, EmailField, HiddenField
from wtforms.validators import DataRequired
from flask_wtf.csrf import CSRFProtect
HOSTS_PATH = ( environ["HOSTS"] if "HOSTS" in environ else"/app/data/hosts.d/" ) + "/izbi" HOSTS_PATH = ( environ["HOSTS"] if "HOSTS" in environ else"/app/data/hosts.d/" ) + "/izbi"
DB_PATH = environ["DB_PATH"] if "DB_PATH" in environ else "/app/data/sqlite.db" DB_PATH = environ["DB_PATH"] if "DB_PATH" in environ else "/app/data/sqlite.db"
app = Flask(__name__) app = Flask(__name__)
app.config["SESSION_PERMANENT"] = True app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "memcached" app.config["SESSION_TYPE"] = "memcached"
app.config["PERMANENT_SESSION_LIFETIME"] = 3600 app.config["PERMANENT_SESSION_LIFETIME"] = 3600
app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex() app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex()
csrf = CSRFProtect(app)
hosts = {} hosts = {}
# Update hosts file
def update_hosts(path: str, hosts: dict = {}) -> bool: def update_hosts(path: str, hosts: dict = {}) -> bool:
try: try:
hosts_str = "" hosts_str = ""
@@ -34,213 +36,304 @@ def update_hosts(path: str, hosts: dict = {}) -> bool:
return False return False
# check if string is a valid IP
def validate_ip(ip: str) -> bool: def validate_ip(ip: str) -> bool:
exp = r"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$" exp = r"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"
return re.search(exp, ip) return re.search(exp, ip)
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
@app.route('/', methods=["GET"]) # Serve homepage
def homepage(): @app.route('/', methods=["GET", "POST"])
def index():
if session.get("USERID") is not None: if session.get("USERID") is not None:
return redirect("/dashboard") return redirect("/dashboard")
return render_template("index.html")
login_form = LoginForm()
if login_form.validate_on_submit():
username = login_form.username.data
password = login_form.password.data
userid = auth.user_login(DB_PATH, username, password)
if userid is None:
flash("Invalid user or password")
return redirect("/")
session["USERID"] = userid
return redirect("/dashboard")
return render_template("index.html", login_form=login_form)
class LogoutForm(FlaskForm):
pass
@app.route('/login', methods=["POST"]) class GenerateTokenForm(FlaskForm):
def login(): token_name = StringField('token-name', validators=[DataRequired()])
if "user" not in request.form or "pass" not in request.form:
return redirect("/"), 400
if request.form["user"].strip() == "" or request.form["pass"].strip() == "": class RevokeTokenForm(FlaskForm):
return redirect("/"), 400 token_name = StringField('token-name', validators=[DataRequired()])
userid = auth.user_login(DB_PATH, request.form["user"], request.form["pass"]) class ChangePasswordForm(FlaskForm):
if userid is None: password = PasswordField('password', validators=[DataRequired()])
return redirect("/") new_password = PasswordField('new-password', validators=[DataRequired()])
repeat_password = PasswordField('repeat-password', validators=[DataRequired()])
session["USERID"] = userid class CreateUserForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
return redirect("/dashboard", code=302) domains = StringField('domains')
password = PasswordField('password', validators=[DataRequired()])
@app.route('/logout', methods=["GET"]) is_admin = BooleanField('is-admin')
def logout(): email = EmailField('email')
session.clear()
return redirect("/")
class UpdateUserForm(FlaskForm): #TODO
pass
class DeleteUserForm(FlaskForm):
userid = HiddenField('userid', validators=[DataRequired()])
# Display dashboard for logged in users
@app.route('/dashboard', methods=["GET"]) @app.route('/dashboard', methods=["GET"])
def dashboard(): def dashboard():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
logout_form = LogoutForm()
generate_token_form = GenerateTokenForm()
revoke_token_form = RevokeTokenForm()
change_password_form = ChangePasswordForm()
userid = session.get("USERID") userid = session.get("USERID")
username = auth.get_username(DB_PATH, userid)
domains = auth.get_user_domains(DB_PATH, userid)
tokens = auth.get_user_tokens(DB_PATH, userid)
if auth.is_admin(DB_PATH, userid): username = db.User.get_username(DB_PATH, userid)
users = auth.get_users(DB_PATH) domains = db.User.get_domains(DB_PATH, userid)
return render_template("dashboard.html", username=username, domains=domains, tokens=tokens, is_admin=True, users=users) tokens = db.User.get_token_names(DB_PATH, userid)
return render_template("dashboard.html", username=username, domains=domains, tokens=tokens, is_admin=False)
# If user is admin, display additional options
if db.User.is_admin(DB_PATH, userid):
create_user_form = CreateUserForm()
update_user_form = UpdateUserForm()
delete_user_form = DeleteUserForm()
users = db.list_users(DB_PATH)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form, create_user_form=create_user_form, update_user_form=update_user_form, delete_user_form=delete_user_form,
username=username, domains=domains, tokens=tokens, is_admin=True, users=users
)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form,
username=username, domains=domains, tokens=tokens, is_admin=False
)
# Log out by clearing session data
@app.route('/logout', methods=["POST"])
def logout():
logout_form = LogoutForm()
if logout_form.validate_on_submit():
session.clear()
return redirect("/")
# Generate new access token with a name
@app.route('/generate-token', methods=["POST"]) @app.route('/generate-token', methods=["POST"])
def generate_token(): def generate_token():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
if "token" not in request.form: generate_token_form = GenerateTokenForm()
return redirect("/dashboard")
if request.form["token"].strip() == "":
return redirect("/dashboard")
token = auth.generate_user_token(DB_PATH, session.get("USERID"), request.form["token"]) if generate_token_form.validate_on_submit():
token_name = generate_token_form.token_name.data
try:
token = db.User.generate_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
token = None
if token is not None: if token is not None:
flash(token) flash(token)
return redirect("/dashboard") return redirect("/dashboard")
# Revoke token by name
@app.route('/revoke-token', methods=["POST"]) @app.route('/revoke-token', methods=["POST"])
def revoke_token(): def revoke_token():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
if "token" not in request.form: revoke_token_form = RevokeTokenForm()
return redirect("/dashboard")
if request.form["token"].strip() == "":
return redirect("/dashboard")
auth.revoke_user_token(DB_PATH, session.get("USERID"), request.form["token"]) if revoke_token_form.validate_on_submit():
token_name = revoke_token_form.token_name.data
try:
db.User.revoke_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
return redirect("/dashboard") return redirect("/dashboard")
@app.route('/create-user', methods=["POST"]) # Change password (by user)
def create_user():
if session.get("USERID") is None:
return redirect("/")
if not auth.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
if 'username' not in request.form or 'password' not in request.form:
return redirect("/dashboard")
username = request.form.get("username").strip()
password = request.form.get("password")
domains = request.form.get('domains')
if domains is None:
domains = []
else:
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
is_admin = request.form.get("is_admin") is not None
flash(auth.create_user(DB_PATH, username, password, domains, is_admin, request.form.get('email')))
return redirect("/dashboard")
@app.route('/update-user', methods=["POST"])
def update_user():
if session.get("USERID") is None:
return redirect("/")
if not auth.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
return redirect("/dashboard")
try:
userid = int(userid)
except ValueError:
return redirect("/dashboard")
domains = request.form.get('domains')
if domains is not None:
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
password = request.form.get("password")
is_admin = request.form.get("is_admin")
print(is_admin, flush=True)
if is_admin is not None:
is_admin = is_admin == "true"
print(is_admin, flush=True)
email = request.form.get("email")
auth.update_user(DB_PATH, userid, domains, password, is_admin, email)
return redirect("/dashboard")
@app.route('/delete-user', methods=["POST"])
def delete_user():
if session.get("USERID") is None:
return redirect("/")
if not auth.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
return redirect("/dashboard")
try:
userid = int(userid)
except ValueError:
return redirect("/dashboard")
auth.delete_user(DB_PATH, userid)
return redirect("/dashboard")
@app.route('/change-password', methods=["POST"]) @app.route('/change-password', methods=["POST"])
def change_password(): def change_password():
if session.get("USERID") is None: if session.get("USERID") is None:
return redirect("/") return redirect("/")
if "pass" not in request.form or "pass-new" not in request.form or "pass-rep" not in request.form: change_password_form = ChangePasswordForm()
flash("password change failed")
return redirect("/dashboard")
if request.form["pass"] == "" or request.form["pass-new"] == "" or request.form["pass-rep"] == "":
flash("password change failed")
return redirect("/dashboard")
oldpass = request.form["pass"] if change_password_form.validate_on_submit():
newpass = request.form["pass-new"] password = change_password_form.password.data
new_password = change_password_form.new_password.data
repeat_password = change_password_form.repeat_password.data
if newpass != request.form["pass-rep"]: if new_password != repeat_password:
flash("passwords do not match") flash("Passwords do not match")
return redirect("/dashboard") return redirect("/dashboard")
if not auth.change_user_password(DB_PATH, session.get("USERID"), oldpass, newpass): try:
flash("password change failed") auth.change_user_password(DB_PATH, session.get("USERID"), password, new_password)
return redirect("/dashboard") except ValueError as e:
flash(str(e))
return redirect("/dashboard")
flash("Password changed successfully")
flash("password changed successfully")
return redirect("/dashboard") return redirect("/dashboard")
# Create new user (by admin)
@app.route('/create-user', methods=["POST"])
def create_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
create_user_form = CreateUserForm()
if create_user_form.validate_on_submit():
username = create_user_form.username.data.strip()
password = create_user_form.password.data
domains = create_user_form.domains.data
if domains is None:
domains = []
else:
# Collapse whitespaces
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
is_admin = create_user_form.is_admin.data
email = create_user_form.email.data
if email is not None:
if email == "":
email = None
try:
db.User.new(DB_PATH, username, password, domains, is_admin, email)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
# Update user's field (by admin)
@app.route('/update-user', methods=["POST"])
def update_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
update_user_form = UpdateUserForm()
if not update_user_form.validate_on_submit():
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
flash("UserID not supplied")
return redirect("/dashboard")
if not db.User.exists(DB_PATH, userid):
flash("User does not exist")
return redirect("/dashboard")
domains = request.form.get('domains')
if domains is not None:
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
try:
db.User.set_domains(DB_PATH, userid, domains)
except ValueError as e:
flash(str(e))
password = request.form.get("password")
if password is not None:
try:
db.User.set_password(DB_PATH, userid, password)
except ValueError as e:
flash(str(e))
is_admin = request.form.get("is_admin")
if is_admin is not None:
is_admin = is_admin == "true"
db.User.set_admin(DB_PATH, userid, is_admin)
email = request.form.get("email")
if email is not None:
if email == "":
email = None
db.User.set_email(DB_PATH, userid, email)
return redirect("/dashboard")
# Delete user (by admin)
@app.route('/delete-user', methods=["POST"])
def delete_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
delete_user_form = DeleteUserForm()
if delete_user_form.validate_on_submit():
userid = delete_user_form.userid.data
if not db.User.exists(DB_PATH, userid):
flash("User does not exst")
return redirect("/dashboard")
db.User.delete(DB_PATH, userid)
return redirect("/dashboard")
# API health endpoint ???
@app.route('/health', methods=["GET"]) @app.route('/health', methods=["GET"])
def health(): def health():
return jsonify({"status": "healthy", "comment": ""}) return jsonify({"status": "healthy", "comment": ""})
# API for updating domain's IP
@app.route('/update', methods=["GET"]) @app.route('/update', methods=["GET"])
def update_addr(): def update_addr():
try: try:
@@ -250,10 +343,10 @@ def update_addr():
if 'ip' not in request.args: if 'ip' not in request.args:
ip = request.remote_addr ip = request.remote_addr
elif not validate_ip(request.args['ip']): elif validate_ip(request.args['ip']):
return jsonify({"status": "400", "code": "ip-error", "comment": "Invalid IP"}), 400
else:
ip = request.args['ip'] ip = request.args['ip']
else:
return jsonify({"status": "400", "code": "ip-error", "comment": "Invalid IP"}), 400
if 'token' not in request.args: if 'token' not in request.args:
return jsonify({"status": "400", "code": "token-error", "comment": "Missing or invalid token"}), 400 return jsonify({"status": "400", "code": "token-error", "comment": "Missing or invalid token"}), 400
@@ -265,6 +358,7 @@ def update_addr():
success = True success = True
# Update hosts file if something changed
if domain not in hosts or ip != hosts[domain]: if domain not in hosts or ip != hosts[domain]:
hosts[domain] = ip hosts[domain] = ip
success = update_hosts(HOSTS_PATH, hosts) success = update_hosts(HOSTS_PATH, hosts)
@@ -278,7 +372,6 @@ def update_addr():
return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500 return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500
if __name__ == '__main__': if __name__ == '__main__':
auth.init_db(DB_PATH) db.init(DB_PATH)
app.run(host="0.0.0.0", port=8080, debug=False) app.run(host="0.0.0.0", port=8080, debug=False)
+13 -7
View File
@@ -7,11 +7,12 @@
<tr><th>Username</th><th>Domains</th><th>Password</th><th>Admin</th><th>Email</th><th>Delete</th></tr> <tr><th>Username</th><th>Domains</th><th>Password</th><th>Admin</th><th>Email</th><th>Delete</th></tr>
<form method="POST" action="/create-user"><tr> <form method="POST" action="/create-user"><tr>
<td><input type="text" name="username" placeholder="*user name" required /></td> {{ create_user_form.hidden_tag() }}
<td><input type="text" name="domains" placeholder="space separated domains" /></td> <td>{{ create_user_form.username(placeholder="*Username") }}</td>
<td><input type="password" name="password" placeholder="*password" required /></td> <td>{{ create_user_form.domains(placeholder="Space separated domains") }}</td>
<td><input type="checkbox" name="is_admin" /></td> <td>{{ create_user_form.password(placeholder="*Password") }}</td>
<td><input type="email" name="email" placeholder="email" /></td> <td>{{ create_user_form.is_admin() }}</td>
<td>{{ create_user_form.email(placeholder="Email") }}</td>
<td><button type="submit">Add</button></td> <td><button type="submit">Add</button></td>
</tr></form> </tr></form>
@@ -19,28 +20,33 @@
<tr> <tr>
<td>{{ user["username"] }}</td> <td>{{ user["username"] }}</td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" /> <input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" />
<button type="submit">update domains</button> <button type="submit">update domains</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="password" name="password" placeholder="*password" required /> <input type="password" name="password" placeholder="*password" required />
<button type="submit">update password</button> <button type="submit">update password</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="hidden" name="is_admin" value="{% if user["is_admin"] %}false{% else %}true{% endif %}" /> <input type="hidden" name="is_admin" value="{% if user["is_admin"] %}false{% else %}true{% endif %}" />
<input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} /> <input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} />
<button type="submit">toggle admin</button> <button type="submit">toggle admin</button>
</form></td> </form></td>
<td><form method="POST" action="/update-user"> <td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> <input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="email" name="email" placeholder="email" value="{{ user["email"] }}"/> <input type="email" name="email" placeholder="email" value="{{ user["email"] if user["email"] }}"/>
<button type="submit">update email</button> <button type="submit">update email</button>
</form></td> </form></td>
<td><form method="POST" action="/delete-user"> <td><form method="POST" action="/delete-user">
<input type="hidden" name="userid" value="{{ user["userid"] }}" /> {{ delete_user_form.csrf_token }}
{{ delete_user_form.userid(value=user["userid"]) }}
<button type="submit">Delete</button> <button type="submit">Delete</button>
</form></td> </form></td>
</tr> </tr>
+16 -9
View File
@@ -4,7 +4,11 @@
<title>Dashboard - IZBI DNS</title> <title>Dashboard - IZBI DNS</title>
</head> </head>
<body> <body>
<h2>Hello {{ username }} <a href="/logout">log&nbsp;out</a></h2> <h2>Hello {{ username }}</h2>
<form method="POST" action="/logout">
{{ logout_form.hidden_tag() }}
<button type="submit">Log&nbsp;Out</button>
</form>
{% with messages = get_flashed_messages()%} {% with messages = get_flashed_messages()%}
{% if messages%} {% if messages%}
@@ -23,14 +27,16 @@
<p>Your tokens: <p>Your tokens:
<form method="POST" action="/generate-token"> <form method="POST" action="/generate-token">
<input type="text" name="token" placeholder="token name" /> {{ generate_token_form.hidden_tag() }}
<button type="submit">new&nbsp;token</button> {{ generate_token_form.token_name(placeholder="Token Name") }}
<button type="submit">New&nbsp;Token</button>
</form> </form>
<ul>{% for token in tokens %} <ul>{% for token in tokens %}
<li> <li>
<form method="POST" action="/revoke-token"> <form method="POST" action="/revoke-token">
<input type="text" name="token" readonly value="{{ token }}" /> {{ revoke_token_form.hidden_tag() }}
<button type="submit">revoke</button> {{ revoke_token_form.token_name(placeholder="Token Name", readonly=True, value=token) }}
<button type="submit">Revoke</button>
</form> </form>
</li> </li>
{% endfor %} {% endfor %}
@@ -39,10 +45,11 @@
<p>Password change <p>Password change
<form method="POST" action="/change-password"> <form method="POST" action="/change-password">
<input type="password" name="pass" placeholder="old password" /><br /> {{ change_password_form.hidden_tag() }}
<input type="password" name="pass-new" placeholder="new password" /><br /> {{ change_password_form.password(placeholder="Old Password") }}<br />
<input type="password" name="pass-rep" placeholder="repeat password" /><br /> {{ change_password_form.new_password(placeholder="New Password") }}<br />
<button type="submit">change password</button> {{ change_password_form.repeat_password(placeholder="Repeat Password") }}<br />
<button type="submit">Change Password</button>
</form> </form>
</p>{% if is_admin %}{% include 'admin.html' %}{% endif %} </p>{% if is_admin %}{% include 'admin.html' %}{% endif %}
</body> </body>
+12 -3
View File
@@ -6,9 +6,18 @@
<body> <body>
<p>Dynamic DNS service for <span style="font-family: monospace">.izbi</span> domains <span style="font-family: monospace">:3</span></p> <p>Dynamic DNS service for <span style="font-family: monospace">.izbi</span> domains <span style="font-family: monospace">:3</span></p>
<form method="POST" action="/login"> {% with messages = get_flashed_messages()%}
<input type="text" name="user" placeholder="username" required autofocus /><br /> {% if messages%}
<input type="password" name="pass" placeholder="password" required /><br /> {% for message in messages%}
<h3>{{message}}</h3>
{%endfor%}
{%endif%}
{%endwith%}
<form method="POST" action="/">
{{ login_form.hidden_tag() }}
{{ login_form.username(placeholder="Username", autofocus=True) }}<br />
{{ login_form.password(placeholder="Password") }}<br />
<button type="submit">log in</button> <button type="submit">log in</button>
</form> </form>
</body> </body>
+1
View File
@@ -1 +1,2 @@
flask flask
Flask-WTF