Compare commits

..

18 Commits

Author SHA1 Message Date
472e2eac45 Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#12) from joshlaymon-patch-6 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/12
2025-11-27 16:20:54 +00:00
ce42cb3883 Update web/static/data/wol-pub-codes.v1.json 2025-11-27 16:20:45 +00:00
e80137dfb5 Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#11) from joshlaymon-patch-5 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/11
2025-10-26 13:54:18 +00:00
a04547c57b Update web/static/data/wol-pub-codes.v1.json 2025-10-26 13:54:10 +00:00
80e48333be Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#10) from joshlaymon-patch-4 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/10
2025-10-14 01:53:05 +00:00
b5577d57cd Update web/static/data/wol-pub-codes.v1.json 2025-10-14 01:52:59 +00:00
af9c1bc8ff Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#9) from joshlaymon-patch-3 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/9
2025-10-02 21:52:01 +00:00
febfe374ed Update web/static/data/wol-pub-codes.v1.json 2025-10-02 21:51:52 +00:00
c4088df999 Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#8) from joshlaymon-patch-2 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/8
2025-09-15 22:51:27 +00:00
b1dff396d9 Update web/static/data/wol-pub-codes.v1.json 2025-09-15 22:51:16 +00:00
992f7f90a7 Merge pull request 'Update web/static/data/wol-pub-codes.v1.json' (#7) from joshlaymon-patch-1 into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/7
2025-09-13 01:27:01 +00:00
360fc6b4be Update web/static/data/wol-pub-codes.v1.json 2025-09-13 01:26:45 +00:00
a8008c37b4 Merge pull request 'seperate out WOL publication codes, offer GUI for viewing, and add new KC publication source' (#6) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/6
2025-09-09 00:52:20 +00:00
72c3831eb4 Merge pull request 'Delete button moved from results to edit pages. Tweak announcement feature for easier use.' (#5) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/5
2025-09-07 20:57:30 +00:00
a632775da9 Merge pull request 'Update web/core/views.py to have proper invalidscripture tools' (#4) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/4
2025-09-07 20:34:45 +00:00
84d08ee675 Merge pull request 'Allow search of 'invalidscripture'' (#3) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/3
2025-09-07 18:49:33 +00:00
3b802f2e34 Merge pull request 'Integrate swipe and d-pad to transit entry_view' (#2) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/2
2025-09-07 17:58:34 +00:00
95391f82d5 Merge pull request 'develop' (#1) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/1
2025-09-07 03:43:12 +00:00
9 changed files with 37 additions and 237 deletions

14
.env
View File

@ -20,16 +20,4 @@ ADMIN_PASSWORD=1993llelMO65026illustrations
# Seed import
IMPORT_SEED_ON_START=false
# SEED_CSV=/data/imports/illustrations_seed.csv
OPENAI_API_KEY=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
# Authentik OIDC
OIDC_RP_CLIENT_ID=yQnn1VzxZh7hglqCWuBX4ekwpuvDgxss0IhDuULx
OIDC_RP_CLIENT_SECRET=LmWIlfXJMGMPbc4NKOkMFZ5gxKRpvHykkeMe7OQ1ugykihVa9yPlDQl4hPbfvKiYoj5Nm2ONU7RkQGSWovmS2dLUjGhYUxv4JgSzK0s2ECdHmUn1QH6iVaNncWn3zZMb
# Endpoints (see next section)
OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.lakehandyman.biz/application/o/authorize/
OIDC_OP_TOKEN_ENDPOINT=https://auth.lakehandyman.biz/application/o/token/
OIDC_OP_USER_ENDPOINT=https://auth.lakehandyman.biz/application/o/userinfo/
OIDC_OP_JWKS_ENDPOINT=https://auth.lakehandyman.biz/application/o/illustration-database/jwks/
OIDC_RP_SCOPES=openid email profile
OPENAI_API_KEY=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

View File

@ -1,68 +0,0 @@
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
class AuthentikOIDCBackend(OIDCAuthenticationBackend):
"""
Custom backend to integrate Authentik OIDC with existing Django users
without breaking local authentication.
"""
def filter_users_by_claims(self, claims):
email = (claims.get("email") or "").strip().lower()
username = (
claims.get("preferred_username")
or claims.get("username")
or ""
).strip()
# 1) Prefer matching by email
if email:
qs = self.UserModel.objects.filter(email__iexact=email)
if qs.exists():
return qs
# 2) Fallback to username match (critical for existing local users)
if username:
qs = self.UserModel.objects.filter(username__iexact=username)
if qs.exists():
return qs
# 3) Otherwise, no match -> create user
return self.UserModel.objects.none()
def create_user(self, claims):
user = super().create_user(claims)
email = (claims.get("email") or "").strip().lower()
username = (
claims.get("preferred_username")
or claims.get("username")
or email
or ""
).strip()
if email:
user.email = email
# Only set username if Django hasn't already set one
if username and not user.username:
user.username = username
user.first_name = claims.get("given_name", "") or user.first_name
user.last_name = claims.get("family_name", "") or user.last_name
# SSO-only account unless you explicitly add a password later
user.set_unusable_password()
user.save()
return user
def update_user(self, user, claims):
email = (claims.get("email") or "").strip().lower()
if email and user.email.lower() != email:
user.email = email
user.first_name = claims.get("given_name", "") or user.first_name
user.last_name = claims.get("family_name", "") or user.last_name
user.save()
return user

View File

@ -53,22 +53,15 @@ EXPECTED_HEADERS = [
"Date Edited",
]
def is_admin(user):
return user.is_superuser or user.is_staff
def login_view(request):
# If Django session already exists, go to app
if request.user.is_authenticated:
return redirect("search")
# Only auto-start OIDC if this is a fresh browser visit
# and NOT a redirect coming from Django itself
if (
request.method == "GET"
and "next" not in request.GET
):
return redirect("oidc_authentication_init")
# Fallback: show login page (rare, but prevents loops)
ctx = {}
if request.method == "POST":
u = request.POST.get("username")
p = request.POST.get("password")
@ -77,13 +70,9 @@ def login_view(request):
login(request, user)
return redirect("search")
ctx["error"] = "Invalid credentials"
return render(request, "login.html", ctx)
def is_admin(user):
return user.is_superuser or user.is_staff
def entry_context(entry, result_ids):
"""
Build the navigation + chips context for the entry pages.

View File

@ -12,7 +12,7 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
INSTALLED_APPS = [
"django.contrib.admin","django.contrib.auth","django.contrib.contenttypes",
"django.contrib.sessions","django.contrib.messages","django.contrib.staticfiles",
"core","mozilla_django_oidc",
"core",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -52,12 +52,6 @@ DATABASES = {
}
}
AUTHENTICATION_BACKENDS = (
"core.auth_oidc.AuthentikOIDCBackend", # OIDC via Authentik
"django.contrib.auth.backends.ModelBackend", # keep existing username/password login
)
LANGUAGE_CODE="en-us"
TIME_ZONE="America/Chicago"
USE_I18N=True
@ -72,22 +66,6 @@ LOGIN_REDIRECT_URL="/search/"
LOGOUT_REDIRECT_URL="/login/"
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
# --- Authentik OIDC ---
OIDC_RP_CLIENT_ID = os.getenv("OIDC_RP_CLIENT_ID", "")
OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET", "")
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv("OIDC_OP_AUTHORIZATION_ENDPOINT", "")
OIDC_OP_TOKEN_ENDPOINT = os.getenv("OIDC_OP_TOKEN_ENDPOINT", "")
OIDC_OP_USER_ENDPOINT = os.getenv("OIDC_OP_USER_ENDPOINT", "")
OIDC_OP_JWKS_ENDPOINT = os.getenv("OIDC_OP_JWKS_ENDPOINT", "")
OIDC_RP_SCOPES = os.getenv("OIDC_RP_SCOPES", "openid email profile")
OIDC_CREATE_USER = True
USE_X_FORWARDED_HOST = True
OIDC_RP_SIGN_ALGO = "RS256"
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
# Ensure MEDIA_ROOT exists (you likely already have this)

View File

@ -42,7 +42,6 @@ urlpatterns = [
# Auth
path("login/", core_views.login_view, name="login"),
path("oidc/", include("mozilla_django_oidc.urls")),
path("logout/", auth_views.LogoutView.as_view(next_page="/login/"), name="logout"),
path("admin/", admin.site.urls),

View File

@ -1,5 +1,4 @@
Django==5.0.6
psycopg2-binary==2.9.9
python-dateutil==2.9.0.post0
openai>=1.0,<2.0
mozilla-django-oidc==5.0.2
openai>=1.0,<2.0

View File

@ -5,6 +5,6 @@
"fg", "fy", "gt", "hb", "im", "ip", "it", "jv", "ka", "kj", "kl",
"lf", "lff", "ll", "ly", "my", "od", "pe", "po", "pt", "rr", "rs",
"sg", "sh", "si", "td", "tp", "tr", "ts", "un", "jy",
"uw", "su", "re", "lvs", "lp", "yy", "yp2", "yp", "sl", "pm", "kc"
"uw", "su", "re", "lvs", "lp", "yy", "yp2", "yp", "sl", "pm", "kc", "jd", "jr", "ia", "hs", "ia", "hs", "lv", "kr", "km", "wcg", "bw", "ce", "ad"
]
}

View File

@ -3,95 +3,50 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Illustrations · Sign in</title>
<title>Sign in · Illustrations</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="{% static 'app.css' %}">
</head>
<body class="login-page">
<div class="login-hero">
<div class="login-card">
<h1 class="login-title">Sign in</h1>
<!-- App title instead of generic "Sign in" -->
<h1 class="login-title">Illustrations Database Login</h1>
{% if form.non_field_errors %}
<div class="login-alert">
{% for e in form.non_field_errors %}{{ e }}{% if not forloop.last %}<br>{% endif %}{% endfor %}
</div>
{% endif %}
<div class="login-sso">
<div class="login-box">
<a href="{% url 'oidc_authentication_init' %}"
class="btn btn-primary btn-lg login-sso-button">
Log in with SSO
</a>
</div>
</div>
<form method="post" action="{% url 'login' %}" novalidate>
{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
<!-- Legacy toggle -->
<div class="login-divider" style="margin-top: 1.25rem;">
<span>
<a href="#"
onclick="event.preventDefault(); document.getElementById('alt-login').toggleAttribute('hidden');">
Use a different sign-in method
</a>
</span>
</div>
<label for="id_username" class="login-label">Username</label>
<input id="id_username" name="username" type="text" autocomplete="username"
value="{{ form.username.value|default:'' }}" required autofocus class="login-input">
<!-- Legacy login (hidden by default) -->
<div id="alt-login" hidden>
{% if form.non_field_errors %}
<div class="login-alert">
{% for e in form.non_field_errors %}
{{ e }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}
{% if form.username.errors %}
<div class="login-field-error">
{% for e in form.username.errors %}{{ e }}{% if not forloop.last %}<br>{% endif %}{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'login' %}" novalidate>
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}">
{% endif %}
<label for="id_password" class="login-label">Password</label>
<input id="id_password" name="password" type="password" autocomplete="current-password"
required class="login-input">
<label for="id_username" class="login-label">Username</label>
<input id="id_username"
name="username"
type="text"
autocomplete="username"
value="{{ form.username.value|default:'' }}"
class="login-input">
{% if form.username.errors %}
<div class="login-field-error">
{% for e in form.username.errors %}
{{ e }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
<label for="id_password" class="login-label">Password</label>
<input id="id_password"
name="password"
type="password"
autocomplete="current-password"
class="login-input">
{% if form.password.errors %}
<div class="login-field-error">
{% for e in form.password.errors %}
{{ e }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
<button class="btn btn-primary btn-lg login-submit" type="submit">
Log in
</button>
</form>
</div>
{% if form.password.errors %}
<div class="login-field-error">
{% for e in form.password.errors %}{{ e }}{% if not forloop.last %}<br>{% endif %}{% endfor %}
</div>
{% endif %}
<button class="btn btn-primary btn-lg login-submit" type="submit">Sign in</button>
</form>
</div>
</div>
</body>
</html>
</html>

View File

@ -11,8 +11,7 @@
<button class="btn btn-primary">Search</button>
</div>
<!-- Desktop filters -->
<div class="filter-row desktop-filters">
<div class="filter-row">
{% for f in field_options %}
<label class="check-pill">
<input type="checkbox" name="{{ f.name }}" {% if f.checked %}checked{% endif %}>
@ -20,19 +19,6 @@
</label>
{% endfor %}
</div>
<!-- Mobile dropdown filters -->
<div class="filter-row mobile-filters">
<button type="button" id="filterDropdownBtn" class="btn btn-secondary" aria-expanded="false">Filters ▾</button>
<div id="filterDropdownPanel" class="dropdown-panel">
{% for f in field_options %}
<label class="check-pill" style="display:block; margin:6px 0;">
<input type="checkbox" name="{{ f.name }}" {% if f.checked %}checked{% endif %}>
<span>{{ f.label }}</span>
</label>
{% endfor %}
</div>
</div>
</form>
{% if ran_search and result_count == 0 %}
@ -211,17 +197,6 @@
});
});
// Mobile filter dropdown toggle
(function(){
const btn = document.getElementById('filterDropdownBtn');
const panel = document.getElementById('filterDropdownPanel');
if (!btn || !panel) return;
btn.addEventListener('click', ()=>{
const open = panel.classList.toggle('open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
})();
// ===============================
// No-results: show a random funny illustration
// ===============================
@ -367,21 +342,6 @@
padding:10px 16px;
margin:0;
}
/* Mobile filter dropdown styling */
.mobile-filters { display:none; margin-top:10px; }
@media (max-width: 700px){
.desktop-filters { display:none; }
.mobile-filters { display:block; }
#filterDropdownPanel {
border:1px solid var(--border);
background:#fff;
border-radius:10px;
margin-top:6px;
padding:10px;
}
#filterDropdownBtn { width:100%; text-align:left; }
}
</style>
<!-- Save q + selected fields exactly as submitted -->