Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
191797b
Added initial s3 storage methods and refactored index handling
simcax Mar 30, 2025
4d3e99a
Black formatted file
simcax Mar 30, 2025
d718ef3
Skipping tests for now, have to figure out why secrets are not available
simcax Mar 30, 2025
1b7b275
Fixed index loading for navigation
simcax Mar 30, 2025
ba268c7
Added branch name option to full version, for versions not on main
simcax Apr 1, 2025
34fd4fc
Checking in after update toml script has run
simcax Apr 1, 2025
e1f628c
Refactoring fly toml update script to detect github actions
simcax Apr 1, 2025
c39a383
Complete refactor of index and page handling
simcax Apr 7, 2025
afc743c
Changed index file and added markdown editor
simcax Apr 8, 2025
85a9ce5
Added initial markdown editor support
simcax Apr 27, 2025
790e9c9
Adds editor routes and navigation enhancements
simcax May 2, 2025
7fc51ad
Removes current pages configuration
simcax May 2, 2025
7c873c8
Updates index handling and adds footer
simcax May 2, 2025
7204967
Enhances page editing and display functionalities
simcax May 2, 2025
4a8e427
Implements user authentication and session management
simcax May 4, 2025
11f0d81
Updates index handling for renamed pages
simcax May 4, 2025
beb221c
Updates page index to reflect content changes
simcax May 4, 2025
cafed9d
Added configuration for fly redis
simcax May 4, 2025
a3432d5
Sets maximum content length for uploads
simcax May 4, 2025
c21154c
Configures request limits in Gunicorn
simcax May 4, 2025
248db88
Removes unnecessary line break.
simcax May 4, 2025
7dbcb6c
Configures Gunicorn using a config file
simcax May 4, 2025
f6b5be5
Removes unnecessary parentheses.
simcax May 4, 2025
49a37ef
Increases VM memory allocation.
simcax May 4, 2025
4dcfdd0
Refactors authentication blueprint import
simcax May 4, 2025
ef4627c
Adds logging for user authentication
simcax May 4, 2025
e47163e
Updates logging statements with f-strings
simcax May 4, 2025
70097f3
Sets session cookie domain from URL
simcax May 4, 2025
a79c38f
Stores user data in session upon login.
simcax May 4, 2025
963827c
Adds session debugging information
simcax May 6, 2025
2eacd7d
Uses SECRET_KEY for application security.
simcax May 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/fly-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@ jobs:
- name: Update fly.toml with version
run: |
uv run python utils/update_fly_toml.py
# - name: Run tests
# run: |
# export MD_PATH="$(pwd)/tmp/markdown_pages"
# mkdir -p $MD_PATH
# uv run pytest --cov-report=xml --cov-report=html --cov=lfweb -n 5 -m "not integration"
- name: Modify URL
id: modify-url
run: |
url=${{ steps.deploy.outputs.url }}
modified_url=${url#https://}
echo "::set-output name=url::$modified_url"
- name: Deploy PR app to Fly.io
id: deploy
uses: superfly/[email protected]
Expand All @@ -61,4 +65,13 @@ jobs:
REDIS_HOST=${{ secrets.REDIS_HOST }}
SESSION_COOKIE_DOMAIN=${{ steps.modify-url.outputs.url }}
DOORCOUNT_URL=${{ secrets.DOORCOUNT_URL}}
GOOGLE_MAPS_API_KEY=${{ secrets.GOOGLE_MAPS_API_KEY }}
GOOGLE_MAPS_API_KEY=${{ secrets.GOOGLE_MAPS_API_KEY }}
ENVIRONMENT_NAME=${{ secrets.ENVIRONMENT_NAME }}
AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_ENDPOINT_URL_S3=${{ secrets.AWS_ENDPOINT_URL_S3 }}
AWS_REGION=${{ secrets.AWS_REGION }}
AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
BUCKET_NAME=${{ secrets.BUCKET_NAME }}
ENVIRONMENT_NAME=${{ secrets.ENVIRONMENT_NAME }}
SECRET_KEY=${{ secrets.SECRET_KEY }}

3 changes: 2 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ WORKDIR /app

# Copy the application code
COPY ./lfweb ./lfweb
COPY ./docker/config/gunicorn.py ./gunicorn.py

# Copy the dependencies from the builder image
COPY --from=builder /app/.venv /app/.venv
Expand All @@ -45,4 +46,4 @@ EXPOSE 8000
ENV FLASK_APP=lfweb

# Command to run the application using Gunicorn
ENTRYPOINT ["/app/.venv/bin/gunicorn", "-b", "0.0.0.0:8000", "lfweb:create_app()"]
ENTRYPOINT ["/app/.venv/bin/gunicorn","-c", "gunicorn.py", "-b", "0.0.0.0:8000", "lfweb:create_app()"]
9 changes: 5 additions & 4 deletions docker/config/gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import multiprocessing
import os

from distutils.util import strtobool


bind = os.getenv("WEB_BIND", "0.0.0.0:8000")
accesslog = "-"
access_log_format = "%(h)s %(l)s %(u)s %(t)s '%(r)s' %(s)s %(b)s '%(f)s' '%(a)s' in %(D)sµs" # noqa: E501

workers = int(os.getenv("WEB_CONCURRENCY", multiprocessing.cpu_count() * 2))
threads = int(os.getenv("PYTHON_MAX_THREADS", 1))

reload = bool(strtobool(os.getenv("WEB_RELOAD", "false")))

reload = os.getenv("WEB_RELOAD", "false").lower() in ("true", "1")

limit_request_line = 0
limit_request_field_size = 0
34 changes: 20 additions & 14 deletions fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ primary_region = "arn"
dockerfile = "docker/Dockerfile"

[env]
PORT = "8080"
SESSION_COOKIE_NAME = "lfweb"
API_BASE_URL = "https://foreninglet.dk/api/"
API_VERSION = "version=1"
API_MEMBERS_API = "members"
API_ACTIVITIES_API = "activities"
ACTIVITY_LIST_URL = "https://activities.lejre.fitness/activity_list"
VERSION = "0.1.0-aecd15b"
PORT = "8080"
SESSION_COOKIE_NAME = "lfweb"
API_BASE_URL = "https://foreninglet.dk/api/"
API_VERSION = "version=1"
API_MEMBERS_API = "members"
API_ACTIVITIES_API = "activities"
ACTIVITY_LIST_URL = "https://activities.lejre.fitness/activity_list"
VERSION = "0.1.0-34fd4fc-feature/pages-in-bucket"
MD_PATH = "lfweb/markdown_pages"
REDIS_HOST = "fly-lfweb.upstash.io"


[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = [ "app",]
internal_port = 8000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = [ "app",]

[[vm]]
memory = "2gb"
24 changes: 22 additions & 2 deletions lfweb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
"""Lejre Fitness Website - Flask App"""

from datetime import timedelta
import os
import uuid
from datetime import datetime, timedelta
from os import environ, urandom

import redis
import sentry_sdk
from flask import Flask # , render_template, send_from_directory, session
from flask import Flask, session # , render_template, send_from_directory, session
from flask_session import Session
from loguru import logger
from werkzeug.http import dump_cookie

from lfweb.main import ( # pylint: disable=import-outside-toplevel
auth,
editor_bp,
frontpage_bp,
images_bp,
pages_bp,
permalinks_bp,
)

basedir = os.path.abspath(os.path.dirname(__file__))

app_environment = environ.get("ENVIRONMENT_NAME", "development")
version = environ.get("VERSION")

# from .routes import ()
sentry_sdk.init(
dsn="https://f90b2619be9af44f465a5b48a7135f31@o4505902934130688.ingest.us.sentry.io/4505902934261760",
Expand All @@ -26,6 +36,9 @@
# of sampled transactions.
# We recommend adjusting this value in production.
profiles_sample_rate=1.0,
send_default_pii=True,
environment=app_environment,
release=version,
)


Expand Down Expand Up @@ -53,7 +66,11 @@ def create_app(test_config=None):
SESSION_COOKIE_NAME=str(environ.get("SESSION_COOKIE_NAME", site_short_name)),
SESSION_COOKIE_HTTPONLY=True, # Prevents JavaScript access to cookies
PERMANENT_SESSION_LIFETIME=timedelta(days=14), # Controls session expiration
MAX_CONTENT_LENGTH=1024 * 1024 * 16, # 16 MB
)
app.config["MDEDITOR_FILE_UPLOADER"] = os.path.join(
basedir, "uploads"
) # this floder uesd to save your uploaded image

print(secret_key)
if test_config:
Expand All @@ -65,9 +82,12 @@ def create_app(test_config=None):
sess.init_app(app)
# app.register_blueprint(some_route.bp1)

app.register_blueprint(editor_bp)
app.register_blueprint(frontpage_bp)
app.register_blueprint(images_bp)
app.register_blueprint(pages_bp)
app.register_blueprint(permalinks_bp)
app.register_blueprint(auth.bp)

app.logger.info("App routes loaded")
app.logger.info(app.url_map)
Expand Down
3 changes: 3 additions & 0 deletions lfweb/main/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from flask import Blueprint # noqa: F401
from loguru import logger # noqa: F401

from . import auth
from .editor_routes import bp as editor_bp # noqa: F401
from .images import bp as images_bp # noqa: F401
from .pages_route import bp as pages_bp # noqa: F401
from .pages_route import bp_permalinks as permalinks_bp # noqa: F401
from .routes import frontpage_bp # noqa: F401
137 changes: 137 additions & 0 deletions lfweb/main/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import functools
import json
import os
import socket
import uuid

import requests
from flask import (
Blueprint,
flash,
g,
redirect,
render_template,
request,
session,
url_for,
)
from loguru import logger

hostname = socket.gethostname()
bp = Blueprint("auth", __name__, url_prefix="/auth")


@bp.route("/login", methods=("GET", "POST"))
def login():
logger.info("/login loaded.")
logger.info(session)
logger.info(session.get("user_id"))
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]

error, r = lfUserLogin(username, password)
if error is None:
session.clear()
userData = json.loads(r.text)
session["user_id"] = userData["id"]
session["user_name"] = f"{userData['first_name']} {userData['last_name']}"
session["user_email"] = userData["email"]
logger.info(
"User %s logged in. redirecting to index page.", session["user_name"]
)
logger.info(g)
return render_template("snippets/logged_in.html", userdata=userData)

flash(error, "login_error")

return render_template("snippets/auth_result.html")


def lfUserLogin(username, password):
apiPass = os.environ.get("API_PASSWORD")
apiUser = os.environ.get("API_USERNAME")
if apiUser and apiPass:
loginData = {
"credentials": {
"username": username,
"password": password,
"field": "email",
}
}

url = "https://foreninglet.dk/api/memberlogin?version=1"
error = None
try:
r = requests.post(url, auth=(apiUser, apiPass), json=loginData)
logger.info(f"API Login succeeded for {username}")
if r.status_code != 200:
data = json.loads(r.text)
error = "Incorrect username or password."
logger.info(f"{error} {data}")
except requests.exceptions.RequestException as e:
raise (SystemExit(e))
else:
error = "Api username or password not set."
r = "No data retrieved."
logger.error(error)
return error, r


@bp.before_app_request
def load_logged_in_user():
user_id = session.get("user_id")
logger.info(f"User ID: {user_id}")
if user_id is None:
g.user = None
else:
logger.info(f"User ID found: {user_id}")
apiPass = os.environ.get("API_PASSWORD")
apiUser = os.environ.get("API_USERNAME")
if not apiPass or not apiUser:
raise ("Missing API Credentials")
url = "https://foreninglet.dk/api/members?version=1"
try:
r = requests.get(url, auth=(apiUser, apiPass))
users = json.loads(r.text)
logger.info("API User data retrieved.")
for user in users:
if user["MemberId"] == user_id:
g.user = user
session["user_id"] = user["MemberId"]
session["user_name"] = f"{user['FirstName']} {user['LastName']}"
session["user_email"] = user["Email"]
logger.info(f"User found: {user['FirstName']} {user['LastName']}")
break
except requests.exceptions.RequestException as e:
raise (SystemExit(e))
# db = get_conn()

# with db.cursor() as cur:
# cur.execute("SELECT * FROM soc.user WHERE id = '{}'".format(user_id))
# g.user = cur.fetchone()[0]


@bp.route("/logout", methods=("POST",))
def logout():
session.clear()
return render_template("snippets/logged_out.html")


def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for("auth.login"))

return view(**kwargs)

return wrapped_view


@bp.before_app_request
def debug_session():
if "session_id" not in session:
session["session_id"] = str(uuid.uuid4())
logger.info(f"Session ID: {session['session_id']}")
logger.info(f"Session Contents: {session}")
Loading