Skip to content

Commit 9c5d591

Browse files
committed
Admin interfaces and problem validation
1 parent 567b71d commit 9c5d591

32 files changed

+609
-365
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.env
22

3+
.ruff_cache/
34
# Byte-compiled / optimized / DLL files
45
__pycache__/
56
*.py[cod]

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,24 @@
11
# Race Condition
22

3+
This project uses [Poetry](https://python-poetry.org/docs/#installation)
4+
5+
## Quick start
6+
7+
```bash
8+
poetry install
9+
poetry shell
10+
flask --app race_condition init-db
11+
flask --app race_condition --debug run --host=0.0.0.0 --port=5000
12+
```
13+
14+
# Administration
15+
16+
Register an admin account ASAP. New admin registrations will not be accepted once the first user has been registered
17+
http://127.0.0.1:5000/auth/register
18+
19+
List admin capabilities
20+
http://127.0.0.1:5000/admin
21+
22+
Contestants never need to leave the root of the site http://127.0.0.1:5000/
23+
24+
Admins can set the challenge that is active http://127.0.0.1:5000/problems/list/

poetry.lock

Lines changed: 22 additions & 233 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
[tool.poetry]
22
name = "race-condition"
33
version = "0.1.0"
4-
description = "A barebones FastAPI application."
4+
description = "A barebones Flask application."
55
authors = ["Your Name <[email protected]>"]
66
license = "MIT"
77

88
[tool.poetry.dependencies]
99
python = "^3.12"
10-
fastapi = "^0.112.1"
1110
uvicorn = "^0.30.6"
1211
python-dotenv = "^1.0.1"
1312
flask = "^3.0.3"
14-
flask-httpauth = "^4.8.0"
1513
markdown = "^3.7"
1614

1715
[tool.poetry.dev-dependencies]
1816
pytest = "^7.1.2"
17+
ruff = "^0.6.1"
1918

2019
[build-system]
2120
requires = ["poetry-core>=1.0.0"]

race_condition/__init__.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
from flask import Flask
3+
from race_condition import settings
4+
5+
def create_app(test_config=None):
6+
# create and configure the app
7+
app = Flask(__name__, instance_relative_config=True)
8+
app.config.from_mapping(
9+
SECRET_KEY=settings.SECRET_KEY,
10+
DATABASE=os.path.join(app.instance_path, "race_condition.sqlite"),
11+
)
12+
13+
if test_config is None:
14+
# load the instance config, if it exists, when not testing
15+
app.config.from_pyfile("config.py", silent=True)
16+
else:
17+
# load the test config if passed in
18+
app.config.from_mapping(test_config)
19+
20+
# ensure the instance folder exists
21+
try:
22+
os.makedirs(app.instance_path)
23+
except OSError:
24+
pass
25+
26+
# a simple page that says hello
27+
@app.route("/hello")
28+
def hello():
29+
return "Hello, World!"
30+
31+
from race_condition import db
32+
33+
db.init_app(app)
34+
35+
from race_condition import auth
36+
from race_condition import problems
37+
from race_condition import admin
38+
39+
app.register_blueprint(problems.bp)
40+
app.register_blueprint(auth.bp)
41+
app.register_blueprint(admin.bp)
42+
43+
return app

race_condition/admin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from flask import Blueprint, render_template
2+
from race_condition.auth import login_required
3+
4+
bp = Blueprint("admin", __name__, url_prefix="/admin")
5+
6+
@bp.route("/")
7+
@login_required
8+
def index():
9+
template_vars = {}
10+
return render_template("admin/index.html", **template_vars)

race_condition/app.py

Lines changed: 0 additions & 79 deletions
This file was deleted.

race_condition/auth.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import functools
2+
3+
from flask import (
4+
Blueprint,
5+
flash,
6+
g,
7+
redirect,
8+
render_template,
9+
request,
10+
session,
11+
url_for,
12+
)
13+
from werkzeug.security import check_password_hash, generate_password_hash
14+
15+
from race_condition.db import get_db
16+
from race_condition import settings
17+
18+
bp = Blueprint("auth", __name__, url_prefix="/auth")
19+
20+
21+
@bp.route("/register", methods=("GET", "POST"))
22+
def register():
23+
db = get_db()
24+
cursor = db.execute("SELECT COUNT(*) FROM user")
25+
count = cursor.fetchone()[0]
26+
if count >= settings.REGISTRATION_LIMIT:
27+
return "registration is closed", 403
28+
# return 'There are rows in the database.'
29+
30+
if request.method == "POST":
31+
username = request.form["username"]
32+
password = request.form["password"]
33+
error = None
34+
35+
if not username:
36+
error = "Username is required."
37+
elif not password:
38+
error = "Password is required."
39+
40+
if error is None:
41+
try:
42+
db.execute(
43+
"INSERT INTO user (username, password) VALUES (?, ?)",
44+
(username, generate_password_hash(password)),
45+
)
46+
db.commit()
47+
except db.IntegrityError:
48+
error = f"User {username} is already registered."
49+
else:
50+
return redirect(url_for("auth.login"))
51+
52+
flash(error)
53+
54+
return render_template("auth/register.html")
55+
56+
57+
@bp.route("/login", methods=("GET", "POST"))
58+
def login():
59+
if request.method == "POST":
60+
username = request.form["username"]
61+
password = request.form["password"]
62+
db = get_db()
63+
error = None
64+
user = db.execute(
65+
"SELECT * FROM user WHERE username = ?", (username,)
66+
).fetchone()
67+
68+
if user is None:
69+
error = "Incorrect username."
70+
elif not check_password_hash(user["password"], password):
71+
error = "Incorrect password."
72+
73+
if error is None:
74+
session.clear()
75+
session["user_id"] = user["id"]
76+
return redirect(url_for("admin.index"))
77+
flash(error)
78+
return render_template("auth/login.html")
79+
80+
81+
@bp.before_app_request
82+
def load_logged_in_user():
83+
user_id = session.get("user_id")
84+
85+
if user_id is None:
86+
g.user = None
87+
else:
88+
g.user = (
89+
get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone()
90+
)
91+
92+
93+
@bp.route("/logout")
94+
def logout():
95+
session.clear()
96+
return redirect(url_for("problems.index"))
97+
98+
99+
def login_required(view):
100+
@functools.wraps(view)
101+
def wrapped_view(**kwargs):
102+
if g.user is None:
103+
return redirect(url_for("auth.login"))
104+
105+
return view(**kwargs)
106+
107+
return wrapped_view

race_condition/db.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import sqlite3
2+
3+
import click
4+
from flask import current_app, g
5+
6+
7+
def get_db():
8+
if "db" not in g:
9+
g.db = sqlite3.connect(
10+
current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES
11+
)
12+
g.db.row_factory = sqlite3.Row
13+
14+
return g.db
15+
16+
17+
def close_db(e=None):
18+
db = g.pop("db", None)
19+
20+
if db is not None:
21+
db.close()
22+
23+
24+
def init_db():
25+
db = get_db()
26+
with current_app.open_resource("schema.sql") as f:
27+
db.executescript(f.read().decode("utf8"))
28+
29+
30+
@click.command("init-db")
31+
def init_db_command():
32+
"""Clear the existing data and create new tables."""
33+
init_db()
34+
click.echo("Initialized the database.")
35+
36+
37+
def init_app(app):
38+
app.teardown_appcontext(close_db)
39+
app.cli.add_command(init_db_command)

0 commit comments

Comments
 (0)