login flow through ui

This commit is contained in:
D. Scott Boggs 2025-05-30 09:12:39 -04:00
parent a481ae9526
commit 41f36b0fd7
8 changed files with 164 additions and 4 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ build/
**/__pycache__ **/__pycache__
mounts/ mounts/
**/*.pem **/*.pem
**/*.secret

View file

@ -29,6 +29,7 @@ packages = ["roc_fnb"]
[project.scripts] [project.scripts]
# Put scripts here # Put scripts here
bootstrap-first-admin = "roc_fnb.scripts.bootstrap_first_admin:bootstrap_first_admin"
[tool.yapf] [tool.yapf]
based_on_style = "facebook" based_on_style = "facebook"

View file

View file

@ -0,0 +1,24 @@
from click import command, option, prompt, confirm
from roc_fnb.website.database import Database
from roc_fnb.website.models.user import User
@command
@option('--name', '-n', type=str, required=True)
@option('--email', '-e', type=str, required=True)
def bootstrap_first_admin(name: str, email: str):
password = prompt('Enter the account password',
hide_input=True, prompt_suffix=': ')
confirmation = prompt('Confirm the account password',
hide_input=True, prompt_suffix=': ')
if password != confirmation:
raise ValueError('passwords did not match')
admin = User.create(email, name, password, moderator=True, admin=True)
db = Database.from_env()
db.store_user(admin)
if confirm('Display an auth token for testing?', default=False):
print(admin.jwt)
if __name__ == '__main__':
bootstrap_first_admin()

View file

@ -1,19 +1,59 @@
from flask import Flask, redirect, url_for, request, send_file from pathlib import Path
from sys import stderr
from flask import (Flask, redirect, url_for, request, send_file, make_response,
abort, render_template, g)
from roc_fnb.website.database import Database
from roc_fnb.website.models.user import User
db = Database.from_env()
app = Flask( app = Flask(
import_name=__name__.split('.')[0], import_name=__name__.split('.')[0],
static_url_path='/' static_url_path='/',
template_folder=Path(__file__).absolute().parent / 'templates',
static_folder=Path(__file__).absolute().parent / 'static',
) )
@app.before_request
def decode_user():
if token := request.cookies.get('auth-token'):
g.user = User.verify_jwt(token)
@app.route('/ig') @app.route('/ig')
def ig_redir(): def ig_redir():
return redirect('https://instagram.com/RocFNB') return redirect('https://instagram.com/RocFNB')
@app.route('/donate') @app.route('/donate')
def donate_redir(): def donate_redir():
return redirect('https://venmo.com/RocFoodNotBombs') return redirect('https://venmo.com/RocFoodNotBombs')
@app.route('/') @app.route('/')
def index(): def index():
return redirect('/index.html') return redirect('/index.html')
@app.post('/login')
def submit_login():
form = request.json
user = db.get_user_by_name(form['name'])
if not user.check_password(form['password']):
abort(401) # unauthorized
response = make_response(redirect('/me'))
response.set_cookie('auth-token', user.jwt)
return response
@app.get('/login')
def render_login_page():
return render_template('login.html')
@app.get('/me')
def get_profile():
if g.user is not None:
return render_template('profile.html', user=g.user)
abort(401)

View file

@ -0,0 +1,27 @@
<html>
<head>
<link rel="stylesheet" href="style.css">
<title>
Rochester Food Not Bombs
</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Teko:wght@300..700&display=swap" rel="stylesheet">
<script>
function start_animation() {
var element = document.getElementById("biglogo")
element.style.animation = 'none';
element.offsetHeight;
element.style.animation = null;
}
</script>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,60 @@
{% extends "base.html" %} {% block content %}
<script>
document.addEventListener("readystatechange", () => {
if (document.readyState === "complete") {
/**
* @type {HTMLInputElement}
*/
const nameInput = document.getElementById("input-name");
/**
* @type {HTMLInputElement}
*/
const passwordInput = document.getElementById("input-password");
/**
* @type {HTMLButtonElement}
*/
const button = document.getElementById("submit-button");
button.addEventListener("click", async (event) => {
const name = nameInput.value;
const password = passwordInput.value;
const result = await fetch("/login", {
method: "POST",
body: JSON.stringify({ name, password }),
headers: {'Content-Type': 'application/json'}
});
if (result.ok) {
window.location = '/me'
} else {
console.dir(result)
// TODO handle error!
}
});
}
});
</script>
<img
id="biglogo"
class="spinny"
onclick="start_animation()"
src="logo.png"
alt="logo"
/>
<h1>Rochester Food Not Bombs!</h1>
<h3>Login:</h3>
<div>
<label for="name">Your name</label>
<input type="text" id="input-name" />
</div>
<div>
<label for="password">Your password</label>
<input type="password" id="input-password" />
</div>
<div>
<button id="submit-button" type="submit">Log in</button>
</div>
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<img id="biglogo" class="spinny" onclick="start_animation()" src="logo.png" alt="logo">
<h1>Rochester Food Not Bombs!</h1>
<p>This will be the profile/settings page for {{user.name}}</p>
{% endblock %}