parent
109a11de30
commit
c084b2c820
@ -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"]
|
||||
|
@ -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')
|
@ -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')
|
@ -0,0 +1,5 @@
|
||||
head:
|
||||
title: LoToSA
|
||||
|
||||
body:
|
||||
h1: LoToSA
|
@ -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]
|
@ -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
|
After Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='simple.min.css')}}">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='main.css')}}">
|
||||
<script src="{{url_for('static', filename='main.js')}}"></script>
|
||||
|
||||
<title>{{i18n.head.title}}</title>
|
||||
<link rel="icon" href="{{url_for('static', filename='favicon.png')}}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{i18n.body.h1}}</h1>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='simple.min.css')}}">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='main.css')}}">
|
||||
<script src="{{url_for('static', filename='main.js')}}"></script>
|
||||
|
||||
<title>{{i18n.head.title}}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}
|
||||
<h1>{{i18n.body.h1}}</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
@ -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')
|
||||
|
Loading…
Reference in new issue