From c084b2c820b3519f4557d2479673a26b5cc1cf95 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Sat, 2 Mar 2024 16:15:54 +0100 Subject: [PATCH] Working auth with dummy user --- Dockerfile | 13 +++++++++ app.py | 60 ++++++++++++++++++++++++++++++++++++++++++ forms.py | 8 ++++++ i18n/en_US.yaml | 5 ++++ lotosa.py | 42 +++++++++++++++++++++++++++++ requirements.txt | 5 ++++ static/favicon.ico | Bin 0 -> 15406 bytes static/main.css | 0 static/main.js | 0 static/simple.min.css | 1 + templates/admin.html | 18 +++++++++++++ templates/index.html | 32 ++++++++++++++++++++++ user.py | 48 +++++++++++++++++++++++++++++++++ 13 files changed, 232 insertions(+) create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 forms.py create mode 100644 i18n/en_US.yaml create mode 100644 lotosa.py create mode 100644 requirements.txt create mode 100644 static/favicon.ico create mode 100644 static/main.css create mode 100644 static/main.js create mode 100644 static/simple.min.css create mode 100644 templates/admin.html create mode 100644 templates/index.html create mode 100644 user.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..371ecec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM debian:12-slim + +RUN DEBIAN_FRONTEND=noninteractive apt update && apt install -y pipx +RUN useradd --add-subids-for-system --system --create-home --home-dir /app appuser +USER appuser +WORKDIR /app +ENV PATH /app/.local/bin:$PATH +COPY ./requirements.txt /app/requirements.txt +RUN pipx install gunicorn==21.2.0 && cat /app/requirements.txt | xargs pipx inject gunicorn +COPY ./ /app/ +EXPOSE 8080 +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "app:app"] + diff --git a/app.py b/app.py new file mode 100644 index 0000000..cc29f7e --- /dev/null +++ b/app.py @@ -0,0 +1,60 @@ +import os +from typing import Union + +from flask import (Flask, flash, redirect, render_template, request, + send_from_directory, url_for) +from flask_login import LoginManager, login_required, login_user, logout_user + +from forms import LoginForm +from lotosa import LoToSa +from user import User + +app = Flask(__name__) +app.config.update( + SECRET_KEY=os.urandom(32), + SESSION_COOKIE_HTTPONLY=True, + REMEMBER_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Strict", +) +login_manager = LoginManager() +login_manager.init_app(app) + +lotosa = LoToSa(app) + + +@login_manager.user_loader +def load_user(user_id) -> Union[User, None]: + for user in lotosa.get_users(): + if user.uid == user_id: + return user + return None + + +@app.route('/', methods=['GET', 'POST']) +def index(): + i18n = lotosa.get_i18n(request) + form = LoginForm() + if request.method == 'POST': + username = form.username.data + password = form.password.data + user = lotosa.login_user(username, password) + if user: + login_user(user) + flash('Logged in successfully.') + return redirect(url_for('admin')) + flash('Logged in faled, please try again.') + return render_template('index.html', i18n=i18n, form=form) + + +@app.route('/admin', methods=['GET']) +@login_required +def admin(): + i18n = lotosa.get_i18n(request) + return render_template('admin.html', i18n=i18n) + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', + mimetype='image/vnd.microsoft.icon') diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..ee5f4c9 --- /dev/null +++ b/forms.py @@ -0,0 +1,8 @@ +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField, SubmitField +from wtforms.validators import DataRequired + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml new file mode 100644 index 0000000..233cd9e --- /dev/null +++ b/i18n/en_US.yaml @@ -0,0 +1,5 @@ +head: + title: LoToSA + +body: + h1: LoToSA diff --git a/lotosa.py b/lotosa.py new file mode 100644 index 0000000..973b55a --- /dev/null +++ b/lotosa.py @@ -0,0 +1,42 @@ +import glob +import sys + +import yaml +from flask import Flask, request + +from user import User + + +class LoToSa: + + def __init__(self, app: Flask): + self.users = [ + User(app, 'micke', 'Micke Nordin', 'hej@mic.ke', 'S3cr3t!') + ] + + def get_users(self): + return self.users + + def login_user(self, username, password): + for user in self.users: + if user.get_id() == username and user.check_password(password): + user.set_authenticated(True) + user.set_active(True) + print(f'Logged in {user.get_id()}', file=sys.stderr) + return user + print(f'Login failed for {username}', file=sys.stderr) + return None + + def get_i18n(self, request: request): + language_files = glob.glob("i18n/*.yaml") + languages = {} + for lang in language_files: + filename = lang.split('/') + lang_code = filename[1].split('.')[0] + with open(lang, 'r', encoding='utf8') as file: + languages[lang_code] = yaml.safe_load(file.read()) + + supported_languages = list(languages.keys()) + user_language = request.accept_languages.best_match( + supported_languages) + return languages[user_language] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ea6ce4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==3.0.2 +flask-login==0.6.3 +flask-bcrypt==1.0.1 +flask-wtf==1.2.1 +pyyaml==6.0.1 diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..afbf5a560dc20158bd52a564f5ee320eb1746436 GIT binary patch literal 15406 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 lHdg(O<#*{grid-column:2}body>header{background-color:var(--accent-bg);border-bottom:1px solid var(--border);text-align:center;padding:0 .5rem 2rem .5rem;grid-column:1/-1}body>header>:only-child{margin-block-start:2rem}body>header h1{max-width:1200px;margin:1rem auto}body>header p{max-width:40rem;margin:1rem auto}main{padding-top:1.5rem}body>footer{margin-top:4rem;padding:2rem 1rem 1.5rem 1rem;color:var(--text-light);font-size:.9rem;text-align:center;border-top:1px solid var(--border)}h1{font-size:3rem}h2{font-size:2.6rem;margin-top:3rem}h3{font-size:2rem;margin-top:3rem}h4{font-size:1.44rem}h5{font-size:1.15rem}h6{font-size:.96rem}p{margin:1.5rem 0}h1,h2,h3,h4,h5,h6,p{overflow-wrap:break-word}h1,h2,h3{line-height:1.1}@media only screen and (max-width:720px){h1{font-size:2.5rem}h2{font-size:2.1rem}h3{font-size:1.75rem}h4{font-size:1.25rem}}a,a:visited{color:var(--accent)}a:hover{text-decoration:none}.button,a.button,button,input[type=button],input[type=reset],input[type=submit],label[type=button]{border:1px solid var(--accent);background-color:var(--accent);color:var(--accent-text);padding:.5rem .9rem;text-decoration:none;line-height:normal}.button[aria-disabled=true],button[disabled],input:disabled,select:disabled,textarea:disabled{cursor:not-allowed;background-color:var(--disabled);border-color:var(--disabled);color:var(--text-light)}input[type=range]{padding:0}abbr[title]{cursor:help;text-decoration-line:underline;text-decoration-style:dotted}.button:not([aria-disabled=true]):hover,button:enabled:hover,input[type=button]:enabled:hover,input[type=reset]:enabled:hover,input[type=submit]:enabled:hover,label[type=button]:hover{background-color:var(--accent-hover);border-color:var(--accent-hover);cursor:pointer}.button:focus-visible,button:focus-visible:where(:enabled),input:enabled:focus-visible:where([type=submit],[type=reset],[type=button]){outline:2px solid var(--accent);outline-offset:1px}header>nav{font-size:1rem;line-height:2;padding:1rem 0 0 0}header>nav ol,header>nav ul{align-content:space-around;align-items:center;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;list-style-type:none;margin:0;padding:0}header>nav ol li,header>nav ul li{display:inline-block}header>nav a,header>nav a:visited{margin:0 .5rem 1rem .5rem;border:1px solid var(--border);border-radius:var(--standard-border-radius);color:var(--text);display:inline-block;padding:.1rem 1rem;text-decoration:none}header>nav a.current,header>nav a:hover,header>nav a[aria-current=page]{border-color:var(--accent);color:var(--accent);cursor:pointer}@media only screen and (max-width:720px){header>nav a{border:none;padding:0;text-decoration:underline;line-height:1}}aside,details,pre,progress{background-color:var(--accent-bg);border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem}aside{font-size:1rem;width:30%;padding:0 15px;margin-inline-start:15px;float:right}[dir=rtl] aside{float:left}@media only screen and (max-width:720px){aside{width:100%;float:none;margin-inline-start:0}}article,dialog,fieldset{border:1px solid var(--border);padding:1rem;border-radius:var(--standard-border-radius);margin-bottom:1rem}article h2:first-child,section h2:first-child{margin-top:1rem}section{border-top:1px solid var(--border);border-bottom:1px solid var(--border);padding:2rem 1rem;margin:3rem 0}section+section,section:first-child{border-top:0;padding-top:0}section:last-child{border-bottom:0;padding-bottom:0}details{padding:.7rem 1rem}summary{cursor:pointer;font-weight:700;padding:.7rem 1rem;margin:-.7rem -1rem;word-break:break-all}details[open]>summary+*{margin-top:0}details[open]>summary{margin-bottom:.5rem}details[open]>:last-child{margin-bottom:0}table{border-collapse:collapse;margin:1.5rem 0}figure>table{width:max-content}td,th{border:1px solid var(--border);text-align:start;padding:.5rem}th{background-color:var(--accent-bg);font-weight:700}tr:nth-child(even){background-color:var(--accent-bg)}table caption{font-weight:700;margin-bottom:.5rem}.button,button,input,select,textarea{font-size:inherit;font-family:inherit;padding:.5rem;margin-bottom:.5rem;border-radius:var(--standard-border-radius);box-shadow:none;max-width:100%;display:inline-block}input,select,textarea{color:var(--text);background-color:var(--bg);border:1px solid var(--border)}label{display:block}textarea:not([cols]){width:100%}select:not([multiple]){background-image:linear-gradient(45deg,transparent 49%,var(--text) 51%),linear-gradient(135deg,var(--text) 51%,transparent 49%);background-position:calc(100% - 15px),calc(100% - 10px);background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-inline-end:25px}[dir=rtl] select:not([multiple]){background-position:10px,15px}input[type=checkbox],input[type=radio]{vertical-align:middle;position:relative;width:min-content}input[type=checkbox]+label,input[type=radio]+label{display:inline-block}input[type=radio]{border-radius:100%}input[type=checkbox]:checked,input[type=radio]:checked{background-color:var(--accent)}input[type=checkbox]:checked::after{content:" ";width:.18em;height:.32em;border-radius:0;position:absolute;top:.05em;left:.17em;background-color:transparent;border-right:solid var(--bg) .08em;border-bottom:solid var(--bg) .08em;font-size:1.8em;transform:rotate(45deg)}input[type=radio]:checked::after{content:" ";width:.25em;height:.25em;border-radius:100%;position:absolute;top:.125em;background-color:var(--bg);left:.125em;font-size:32px}@media only screen and (max-width:720px){input,select,textarea{width:100%}}input[type=color]{height:2.5rem;padding:.2rem}input[type=file]{border:0}hr{border:none;height:1px;background:var(--border);margin:1rem auto}mark{padding:2px 5px;border-radius:var(--standard-border-radius);background-color:var(--marked);color:#000}mark a{color:#0d47a1}img,video{max-width:100%;height:auto;border-radius:var(--standard-border-radius)}figure{margin:0;display:block;overflow-x:auto}figcaption{text-align:center;font-size:.9rem;color:var(--text-light);margin-bottom:1rem}blockquote{margin-inline-start:2rem;margin-inline-end:0;margin-block:2rem;padding:.4rem .8rem;border-inline-start:.35rem solid var(--accent);color:var(--text-light);font-style:italic}cite{font-size:.9rem;color:var(--text-light);font-style:normal}dt{color:var(--text-light)}code,kbd,pre,pre span,samp{font-family:var(--mono-font);color:var(--code)}kbd{color:var(--preformatted);border:1px solid var(--preformatted);border-bottom:3px solid var(--preformatted);border-radius:var(--standard-border-radius);padding:.1rem .4rem}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto;color:var(--preformatted)}pre code{color:var(--preformatted);background:0 0;margin:0;padding:0}progress{width:100%}progress:indeterminate{background-color:var(--accent-bg)}progress::-webkit-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent-bg)}progress::-webkit-progress-value{border-radius:var(--standard-border-radius);background-color:var(--accent)}progress::-moz-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent);transition-property:width;transition-duration:.3s}progress:indeterminate::-moz-progress-bar{background-color:var(--accent-bg)}dialog{max-width:40rem;margin:auto}dialog::backdrop{background-color:var(--bg);opacity:.8}@media only screen and (max-width:720px){dialog{max-width:100%;margin:auto 1em}}sub,sup{vertical-align:baseline;position:relative}sup{top:-.4em}sub{top:.3em}.notice{background:var(--accent-bg);border:2px solid var(--border);border-radius:var(--standard-border-radius);padding:1.5rem;margin:2rem 0} \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..fd5d138 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,18 @@ + + + + + + + + + + + {{i18n.head.title}} + + + + +

{{i18n.body.h1}}

+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..706f1b1 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + {{i18n.head.title}} + + + + + {% block content %} +

{{i18n.body.h1}}

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} +

+

{{ form.submit() }}

+
+{% endblock %} + + diff --git a/user.py b/user.py new file mode 100644 index 0000000..07b502f --- /dev/null +++ b/user.py @@ -0,0 +1,48 @@ +from flask import Flask +from flask_bcrypt import Bcrypt +class User: + def __init__(self, app: Flask, uid: str, display_name: str, email:str, password: str, admin: bool = False): + self.uid = uid + self.display_name = display_name + self.email = email + self.is_admin = admin + self.is_authenticated = False + self.is_active = False + self.is_anonymous = False + self.bcrypt = Bcrypt(app) + self.salt = self.get_salt() + self.password_hash = self.bcrypt.generate_password_hash(password + self.salt).decode('utf-8') + + def check_password(self, password: str): + return self.bcrypt.check_password_hash(self.password_hash, password + self.salt) + + def get_id(self): + return self.uid + + def get_display_name(self): + return self.display_name + + def get_email(self): + return self.email + + def get_salt(self): + return "salt" + + def set_active(self, active: bool): + self.is_active = active + + def set_authenticated(self, authenticated: bool): + self.is_authenticated = authenticated + + def set_anonymous(self, anonymous: bool): + self.is_anonymous = anonymous + + def set_admin(self, admin: bool): + self.is_admin = admin + + def set_email(self, email: str): + self.email = email + + def set_password(self, password: str): + self.password_hash = self.bcrypt.generate_password_hash(password + self.salt).decode('utf-8') +