Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
280 commits
Select commit Hold shift + click to select a range
7f24d1f
add debug line to gha workflow
ajslater Apr 5, 2026
43e8d38
remove debug line
ajslater Apr 5, 2026
f41c84c
fix test make order
ajslater Apr 6, 2026
aeddd8f
stop docker compose when done in gha
ajslater Apr 6, 2026
d8388a9
i don't think i need DJGNO_SETTINGS_MODULE th ci thing
ajslater Apr 6, 2026
29066c5
fix down call to own bring down ci task
ajslater Apr 6, 2026
af1f331
fix docker compose down"
ajslater Apr 6, 2026
5262a02
fix test results upload
ajslater Apr 6, 2026
f97ed4a
Merge branch 'main' into develop
ajslater Apr 6, 2026
496a597
fix test step name
ajslater Apr 6, 2026
3f86dd9
fix gha permissions
ajslater Apr 6, 2026
dd7ed5f
Merge branch 'main' into develop
ajslater Apr 6, 2026
75c7d11
bump news
ajslater Apr 6, 2026
b52af0f
update deps
ajslater Apr 6, 2026
ba591d2
browser set_params method saves last route and saves params to settings
ajslater Apr 6, 2026
f583d46
bump version to 1.10.4
ajslater Apr 6, 2026
331d728
modify gha discord notification
ajslater Apr 6, 2026
f0630ee
use happy-dom for frontend tests
ajslater Apr 6, 2026
a6c15f8
Merge branch 'main' into develop
ajslater Apr 6, 2026
88ae04a
docker tag script better than the one in ci/
ajslater Apr 6, 2026
95d24ef
update deps
ajslater Apr 6, 2026
c8ffdd0
far saner param initialization for browsers and opds start pages
ajslater Apr 6, 2026
4ae504d
update dpes & bump alpha version
ajslater Apr 6, 2026
0fd9b1c
v1.10.5a0 (#551)
ajslater Apr 7, 2026
cba000e
regular version 1.10.5
ajslater Apr 7, 2026
9380888
Merge branch 'main' into develop
ajslater Apr 7, 2026
bb2dfa7
remove develop circleci builds
ajslater Apr 7, 2026
ff2d7d3
adjust variable name
ajslater Apr 7, 2026
20b82a2
bump version to 6 alpha 0
ajslater Apr 7, 2026
bcd4cf4
fix default params for feed_views in opds 2
ajslater Apr 7, 2026
f8d7192
debug logging for django crash
ajslater Apr 7, 2026
d45b1b1
v1.10.6a0 (#553)
ajslater Apr 7, 2026
1f034cd
regular v1.10.6 version
ajslater Apr 7, 2026
0d328cf
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Apr 7, 2026
f9939f3
Merge branch 'main' into develop
ajslater Apr 7, 2026
5360e82
fix docker-tag-latest cscript
ajslater Apr 7, 2026
ec0eee2
ignore gh token file
ajslater Apr 7, 2026
837ed69
try to log more request errors
ajslater Apr 7, 2026
c7b3edf
reconfigure logging to hopefully be more verbose about request errors…
ajslater Apr 7, 2026
bb9aa03
update deps and bump to alpha version
ajslater Apr 7, 2026
63eb55f
bump news (#555)
ajslater Apr 7, 2026
68b5c44
v1.10.7
ajslater Apr 7, 2026
4e845b2
bump news
ajslater Apr 7, 2026
125b061
update devenv
ajslater Apr 7, 2026
1811aa3
update devevn and deps
ajslater Apr 7, 2026
d0203e6
fix pm script
ajslater Apr 7, 2026
6761799
move django-check to test category
ajslater Apr 7, 2026
ccc3b15
workflow build frontend and collect static for prodcution build. fix …
ajslater Apr 7, 2026
575f3ca
bump version and news 1.10.8
ajslater Apr 7, 2026
2a4be92
fix dev-module script
ajslater Apr 7, 2026
a9b9c54
fix news
ajslater Apr 7, 2026
c4f42eb
explain news
ajslater Apr 7, 2026
03edb5b
Merge branch 'main' into develop
ajslater Apr 7, 2026
4d07d2a
Merge branch 'main' into develop
ajslater Apr 8, 2026
499e853
fix opds clear search setting
ajslater Apr 8, 2026
1968845
fix clear search button
ajslater Apr 8, 2026
188c890
Merge branch 'main' into develop
ajslater Apr 8, 2026
382ea9b
use a registry cache instead of gha cache for the dist-builder
ajslater Apr 8, 2026
68926b9
gha use more env vars for image names. retain python dist for 2 days.
ajslater Apr 8, 2026
cab90ff
new quick deploy gha script. update deps & devenv.
ajslater Apr 9, 2026
9179cd1
silence watchfiles 5 second timeout debug message
ajslater Apr 9, 2026
299d24d
consolidate null values const
ajslater Apr 9, 2026
384d6f0
make scope private
ajslater Apr 9, 2026
929ac98
update devenv
ajslater Apr 10, 2026
3f23d63
update deps. migrate to unhead v3
ajslater Apr 10, 2026
ca2b99a
update deps
ajslater Apr 12, 2026
ac23fb6
fix creating reader global settings
ajslater Apr 12, 2026
b118359
fix caching
ajslater Apr 12, 2026
a9bab2a
rename codex build-dist to codex-ci
ajslater Apr 12, 2026
498a2d0
fix image name. make gha steps depend on each other more.
ajslater Apr 12, 2026
00307e2
fix gha syntax errors
ajslater Apr 12, 2026
0e62ca4
Merge branch 'main' into develop
ajslater Apr 12, 2026
1a0865a
names for gha steps
ajslater Apr 13, 2026
8d73870
use ghcr.io for python-debian base
ajslater Apr 13, 2026
ce3c806
update deps
ajslater Apr 13, 2026
aea24a4
format dockerfile
ajslater Apr 13, 2026
928084a
picopt treestamps
ajslater Apr 13, 2026
34325e8
fix custom covers not importing. v1.10.11
ajslater Apr 13, 2026
8364163
fix custom covers count in admin view
ajslater Apr 13, 2026
84a958e
bump news for custom cover count fix
ajslater Apr 13, 2026
d28662c
update deps
ajslater Apr 13, 2026
97daa11
Merge branch 'main' into develop
ajslater Apr 13, 2026
9abd4fd
codex identification in server tag and opds generator tag
ajslater Apr 14, 2026
67a823f
update deps
ajslater Apr 14, 2026
2791819
force no entries on opds start page
ajslater Apr 14, 2026
fe6ef60
common opds start page mixin. emtpy group objects on start page
ajslater Apr 14, 2026
68f1c65
update deps. typechecking.
ajslater Apr 15, 2026
c2a37bf
api change q to search
ajslater Apr 15, 2026
62baa9e
standardize search param as 'search' instead of 'q' or other variations
ajslater Apr 15, 2026
045293d
remove errant icecream
ajslater Apr 15, 2026
44a9d32
clear settings on backend
ajslater Apr 15, 2026
ceba72d
Squashed commit of the following:
ajslater Apr 15, 2026
ee330e1
simplify settings class hierarchy
ajslater Apr 15, 2026
8e57ae4
rename select-many store to browser-select-many
ajslater Apr 15, 2026
afd71d2
switch to bun. updated devenv
ajslater Apr 16, 2026
0eb4799
add a claude md
ajslater Apr 16, 2026
4bff1ee
use frozenattrdict to speed up configuration
ajslater Apr 16, 2026
49c508f
fix rename of browserSelectMany store
ajslater Apr 23, 2026
b297064
auth token help
ajslater Apr 23, 2026
505baf3
fix sort-ignores to make deterministic across shells with different l…
ajslater Apr 23, 2026
fabd716
fix crash on settings not being raw
ajslater Apr 20, 2026
74a506c
another gaurd for getMetadta()
ajslater Apr 22, 2026
c6ae79b
remove keys from unhead meta headers
ajslater Apr 20, 2026
2cf3e49
fix unhead description for admin tabs"
ajslater Apr 22, 2026
889b99d
fix overzealous lazy importer
ajslater Apr 21, 2026
b192b59
fix lazyImportEnabled variable in metadata-activator
ajslater Apr 21, 2026
31baf54
fix metadata activator from bad cherry pick
ajslater Apr 23, 2026
2a46944
fix errant quote
ajslater Apr 23, 2026
a7d0c86
fix typechecking
ajslater Apr 23, 2026
2ae90fc
update devenv & deps
ajslater Apr 23, 2026
49284b4
bump news
ajslater Apr 23, 2026
8a396f0
fix import bug linking folders
ajslater Apr 20, 2026
d6dcbe4
fix possible batching crashes. adjust import variables for throughput
ajslater Apr 20, 2026
b3ec926
batch comic updates
ajslater Apr 20, 2026
65f2b05
move INTERNAL_IPS setting to general django area
ajslater Apr 23, 2026
e0719d1
fix typechecking issue
ajslater Apr 23, 2026
8901560
update deps
ajslater Apr 23, 2026
0885048
bump version to v1.10.12
ajslater Apr 23, 2026
40ce697
fix redirect on OPDS alternate view with metadata
ajslater Apr 23, 2026
6d20994
minor change to browser empty page for better first time experience
ajslater Apr 23, 2026
77174e4
allow browsing to comics with any top group in opds
ajslater Apr 23, 2026
7dfd33b
bring project up to speed with bun and no package-lock.json
ajslater Apr 23, 2026
c0edcc7
Merge branch 'main' into develop
ajslater Apr 23, 2026
23c3ac2
fix browser paginator
ajslater Apr 24, 2026
d59ae4f
bump news for browser paginaor fix
ajslater Apr 24, 2026
8c23450
fix search combobox clearing
ajslater Apr 24, 2026
6bab5e8
uppercase book close button
ajslater Apr 27, 2026
17df41a
version v1.10.13. update deps
ajslater Apr 27, 2026
59f52ae
fix pdfs not displaying
ajslater Apr 27, 2026
731b59a
fix csp for pdfs
ajslater Apr 27, 2026
e511828
fix OPDS FK constraint failure when session row is missing (#607)
ajslater Apr 27, 2026
7264de8
extend session-key validation to bookmark + reader-settings paths (#608)
ajslater Apr 27, 2026
251b09e
format
ajslater Apr 27, 2026
0b51523
bump news
ajslater Apr 27, 2026
8063871
Merge branch 'main' into develop
ajslater Apr 27, 2026
ae40799
Stats: fix user_registered_count / auth_group_count always-zero bug (…
ajslater Apr 27, 2026
8861b9f
defaults for dockerfile ARGs
ajslater Apr 28, 2026
62dc95b
format'
ajslater Apr 28, 2026
982a507
Frontend correctness: 10 bug fixes from sub-plan 01 (#647)
ajslater Apr 28, 2026
517a41c
news for cherry pick
ajslater Apr 28, 2026
a214681
OPDS auth: send WWW-Authenticate + opds-authentication content-type o…
ajslater Apr 28, 2026
e0fb6a1
fix typing inheritence with drf perms
ajslater Apr 28, 2026
d796b40
bump version to v1.10.14
ajslater Apr 28, 2026
14c3d32
update picopt treestamps
ajslater Apr 28, 2026
6625743
fix typing import error
ajslater Apr 28, 2026
4ade941
fix vulture ignorelist
ajslater Apr 28, 2026
8e36cef
readd the mistakenly removed vulture_ignorelist
ajslater Apr 28, 2026
4dcffb0
Merge branch 'main' into develop
ajslater Apr 28, 2026
7924207
v1.10.15 fix show order bug. update deps
ajslater May 1, 2026
90e0cb3
fix nightly job manual expansion
ajslater May 1, 2026
ce5d1b6
fix news version number
ajslater May 1, 2026
ea2d589
Merge branch 'main' into develop
ajslater May 1, 2026
8975999
V1.11 performance (#714)
ajslater May 4, 2026
158c29e
use comicbox 3
ajslater May 4, 2026
d6a2c6c
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater May 4, 2026
de2c237
update version to v1.11.0
ajslater May 4, 2026
f6b99b7
Merge branch 'main' into develop
ajslater May 4, 2026
c429b9a
v1.11.1 fix anonymous user crash on midddleware setting timezone
ajslater May 4, 2026
50a4102
middleware: use ``session.aget`` in the async path of CodexMiddleware…
ajslater May 4, 2026
22c9ffd
update devenv
ajslater May 4, 2026
6d1558b
Merge branch 'main' into develop
ajslater May 4, 2026
8e13ca1
frontend: use static import for useCommonStore in api/v3/base.js (#719)
ajslater May 4, 2026
95ac80f
format news and picopt treestamps
ajslater May 4, 2026
a24d6f9
reader: restore scroll-to-top on horizontal page change with cacheBoo…
ajslater May 4, 2026
9d8b70d
Fix reader not scrolling to top of page
ajslater May 4, 2026
95716be
add folder view default feature to news
ajslater May 4, 2026
b5878fd
reader: default cache_book to False; reset global override on upgrade…
ajslater May 4, 2026
8e84ba9
browser: collapse browser pane to a single scroller (#723)
ajslater May 4, 2026
bf20a20
bump news. update deps
ajslater May 4, 2026
6a1741b
update deps
ajslater May 4, 2026
ce4782b
Merge branch 'main' into develop
ajslater May 4, 2026
0d65678
metadata: include ids in current-group list field (#725)
ajslater May 5, 2026
ab3679d
update version and bump news v1.11.3
ajslater May 5, 2026
6c211d0
metadata: extract group_list helper for GroupSerializer queryset shap…
ajslater May 5, 2026
afe14e1
frontend: simplify vuetify-items, remove ES2024 dependency (#727)
ajslater May 5, 2026
768d6aa
frontend(api/v3): cleanup pass — dead code, mutations, conventions (#…
ajslater May 5, 2026
8853970
frontend: move vuetify-items.js out of api/v3/ (#729)
ajslater May 5, 2026
58d6294
frontend(api/v3): convert default exports to named exports (#730)
ajslater May 5, 2026
4f5b3c6
Merge branch 'main' into develop
ajslater May 5, 2026
7014ab6
v1.11.4 fix firefox scrollbar
ajslater May 5, 2026
dca5365
update deps
ajslater May 5, 2026
0dbd42e
build(icons): replace cairosvg+inkscape with resvg-py (#733)
ajslater May 5, 2026
8bdbd52
update deps
ajslater May 5, 2026
9f532b4
format svg
ajslater May 5, 2026
8b76180
regenerate and optimize svg
ajslater May 5, 2026
f49e04f
format svg
ajslater May 5, 2026
f8feb7b
move build icons to a seperate step outide of build where it's done v…
ajslater May 5, 2026
b798f6c
Merge branch 'main' into develop
ajslater May 5, 2026
5263f71
format makefiles
ajslater May 6, 2026
ff5cf0c
fix links in readmes and organize OPDS clients
ajslater May 6, 2026
5129326
fix doubled "waiting for manual poll" log on every poll cycle (#734)
ajslater May 7, 2026
1254af4
update version to 1.11.5 & deps
ajslater May 7, 2026
2eb7e3f
fix snapshot inode-collision corruption + cleanup migration (#735)
ajslater May 7, 2026
82be7ce
refresh stale Comic.stat across inode rotations without re-import (#736)
ajslater May 7, 2026
8ed35f7
ty ignore
ajslater May 7, 2026
b69cd0b
update deps
ajslater May 7, 2026
b7a706f
bump news for fs fix
ajslater May 7, 2026
debbbfc
fix Ctrl+C hang from cover ProcessPoolExecutor (#714 regression) (#737)
ajslater May 8, 2026
5fa5f55
update deps
ajslater May 8, 2026
119f729
fix dev Vite HMR blocked by CSP / allowedHosts mismatch (#738)
ajslater May 8, 2026
85943ee
mangle DHCP-assigned FQDNs down to mDNS .local form (#739)
ajslater May 8, 2026
8572935
reader: add ?hide_text=1 to suppress visible OCR text on PDF pages (#…
ajslater May 9, 2026
25e0d00
Revert "reader: add ?hide_text=1 to suppress visible OCR text on PDF …
ajslater May 9, 2026
b3decfc
update deps
ajslater May 9, 2026
de1d732
fix dev Vite HMR still blocked by stale service worker CSP (#742)
ajslater May 9, 2026
1630a5c
fix local hmr server
ajslater May 9, 2026
814a5b4
add fixes to news
ajslater May 9, 2026
ff84a29
bump comicbox version
ajslater May 9, 2026
49862d6
Merge branch 'main' into develop
ajslater May 9, 2026
12d1239
reader: serve image-dominant PDF pages as <img>, drop full-PDF mode (…
ajslater May 9, 2026
ecd3e3d
Add browser table view (#745)
ajslater May 9, 2026
3449bd4
remove old tasks
ajslater May 9, 2026
4c8db45
bump news and version to v1.12.0
ajslater May 9, 2026
93015d3
update deps
ajslater May 9, 2026
b090a8d
format
ajslater May 9, 2026
e1a69f0
fix complexipy warnings in browser table-view dispatch helpers (#746)
ajslater May 9, 2026
639f936
split consolidated 0041 into separate phantom-cleanup + table-view mi…
ajslater May 9, 2026
26235aa
add table view to readme features
ajslater May 9, 2026
31a9c96
fix radon complexity warnings across browser table-view code + tests …
ajslater May 9, 2026
d4b9fb8
fix `make ty` warnings in browser table-view code (#749)
ajslater May 9, 2026
2349194
add per-user favorites (#750)
ajslater May 9, 2026
c3659fe
update deps
ajslater May 9, 2026
3f44a08
format
ajslater May 9, 2026
b4bf05b
fix type checking
ajslater May 9, 2026
5af21e9
speling
ajslater May 9, 2026
cfbac48
add smart canonical-order column insertion in browser table column pi…
ajslater May 9, 2026
bbb9f3d
filter orphan keys from generated browser choices json (#752)
ajslater May 9, 2026
2957e79
fix codespell config silently skipping all files (#753)
ajslater May 9, 2026
15efb19
remove old tasks
ajslater May 9, 2026
99e04c5
fix spelnig
ajslater May 9, 2026
c3ad4a7
remove ignore words frome codespell
ajslater May 9, 2026
8964deb
fix codespell skips
ajslater May 9, 2026
57ac98a
audit and fix tool ignore/skip configs (#754)
ajslater May 10, 2026
bb56f23
v1.12.0a0 (#755)
ajslater May 10, 2026
6e30091
restore regular v1.12.0 version
ajslater May 10, 2026
d4b6e71
remove old dev uv sources
ajslater May 10, 2026
248fcae
update devenv
ajslater May 10, 2026
48ff676
update deps
ajslater May 10, 2026
d2f0a4d
Merge branch 'main' into develop
ajslater May 10, 2026
f0cc758
Add optional failed-login log for fail2ban et al. (#757)
ajslater May 10, 2026
23e8441
bump version and news for fail2ban support. v1.12.1
ajslater May 10, 2026
490e005
bump news for new comicbox & comicfn2dict
ajslater May 10, 2026
10d5662
update features section
ajslater May 10, 2026
c68ee16
adjust news
ajslater May 10, 2026
6008813
add toml reference to readme
ajslater May 10, 2026
f16dab1
Downgrade routine anon-403s on /api/v3/auth/profile/ to DEBUG (#759)
ajslater May 10, 2026
9256b08
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater May 10, 2026
4548ffd
bump news
ajslater May 10, 2026
37b51ca
Keep failed-login IPs out of the main log (#760)
ajslater May 10, 2026
7681470
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater May 10, 2026
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
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ width: 128px;
border-radius: 128px;
" />

## v1.12.1

- Features
- Fail2Ban support with a dedicated log. See README.
- Fixes
- Comic filename parsing improvements.
- Downgrade noisy profile forbidden WARNINGS to DEBUG. They are a normal
part of connecting.

## v1.12.0

- Features
Expand Down
208 changes: 186 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,46 @@ A final docker.io image has been released on dockerhub.

## ✨ Features

- Codex is a web server.
- Full text search of comic metadata and bookmarks.
- Filter and sort on all comic metadata and unread status per user.
- Browse a tree of Publishers, Imprints, Series, Volumes, or your own folder
hierarchy, or by tagged Story Arc.
- Browse a multi-sortable table of comic metadata.
- Mark Publishers, Series, Volumes, Folders, Story Arcs, and individual Issues
as favorites and filter on them per user.
- Read comics in a variety of aspect ratios and directions that fit your screen.
- Watches the filesystem and automatically imports new or changed comics.
- Anonymous browsing and reading or reigistered users only, to your preference.
- Per user bookmarking & settings, even before you make an account.
- Private Libraries accessible only to certain groups of users.
- Optional Age Restrictions per user for Age tagged comics.
- Reads CBZ, CBR, CBT, and PDF formatted comics.
- Syndication with OPDS 1 & 2, streaming, search and authentication.
- Save and load named views and searches.
- Add custom covers to Folders, Publishers, Imprints, Series, and Story Arcs.
- Fastest bulk comic importing by far of any comic server.
- Remote-User HTTP header SSO support.
- Runs in 1GB of RAM, faster with more.
- GPLv3 Licenced.
### 📚 Library

- Reads **CBZ, CBR, CB7, CBT and PDF** comics.
- **Fastest bulk importer** of any comic server.
- **Watches the filesystem** and auto-imports new or changed comics.
- **Custom covers** for Folders, Publishers, Imprints, Series, and Story Arcs.

### 🔎 Browse & Search

- Browse a tree of **Publishers, Imprints, Series, Volumes**, your **folder
hierarchy**, or by tagged **Story Arc**.
- **Full-text search** across comic metadata and bookmarks.
- **Filter and sort** on any metadata field, including per-user unread status.
- A **multi-sortable metadata table** view for power browsing.
- **Save and load** named views and searches.
- **Favorites** at the Publisher, Series, Volume, Folder, Story Arc, or Issue
level — filterable per user.

### 📖 Read

- Adapts to any screen with multiple **aspect ratios and reading directions**.
- **Per-user bookmarks and reading settings**, preserved even without an
account.

### 👥 Users & Access

- **Anonymous browsing** or registration-required mode — your choice.
- **Private libraries** restricted to specific groups of users.
- Optional **age restrictions** for age-tagged comics.

### 🔌 Integrations

- **OPDS 1 & 2** syndication with streaming, search, and authentication.
- **Remote-User HTTP header SSO** for reverse-proxy single sign-on.
- **Fail2Ban** log for IP-banning failed login attempts.

### 🪶 Operations

- Runs in **1 GB of RAM** (faster with more).
- **GPLv3** licensed.

### Examples

Expand Down Expand Up @@ -279,6 +297,72 @@ url_path_prefix = ""
The config directory also holds the main sqlite database, a Django cache and
comic book cover thumbnails.

### Full `codex.toml` Reference

All available options with their defaults. Uncomment to override. Codex writes
this file to the config directory on first startup if one is not already
present.

```toml
# Codex Configuration File
# Copy to config/codex.toml and edit as needed.
# Environment variables override values in this file.
# See README.md for full documentation.

# [server]
# Granian ASGI server settings
# host = "0.0.0.0"
# port = 9810
# Number of worker processes. 1 is recommended for containerized environments.
# workers = 1
# HTTP version: "auto", "1", or "2"
# http = "auto"
# Enable websockets (required for Codex live updates)
# websockets = true
# HTTP path prefix for codex (e.g. "/codex" for reverse proxy sub-path)
# url_path_prefix = ""

# [logging]
# Log level: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL
# loglevel = "INFO"
# log_retention = "6 months"
# log_to_console = true
# log_to_file = true
# Directory for log files. Defaults to <config_dir>/logs.
# log_dir = ""

# [cache]
# Directory for the file-based cache (covers, query results, etc).
# Defaults to <config_dir>/cache.
# dir = ""

# [browser]
# max_obj_per_page = 100

# [throttle]
# Rate limiting (requests per minute). 0 = disabled.
# anon = 0
# user = 0
# opds = 0
# opensearch = 0

# [auth]
# Allows authentication without authorization via the Remote-User header.
# Only enable if you have authorization in front of Codex. Dangerous.
# remote_user = false
# Log failed login attempts to a separate file. Useful as input for
# banning tools like fail2ban, CrowdSec, or sshguard.
# Line format: "<ISO timestamp> | Failed login from <ip> user=<username>"
# Example fail2ban failregex:
# ^\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| Failed login from <HOST> user=.*$
# failed_login_log = false
# Path to the failed-login log. Defaults to <log_dir>/failed_logins.log.
# failed_login_log_path = ""
# When behind a reverse proxy, trust X-Forwarded-For for the client IP.
# Disable if Codex is exposed directly (otherwise clients can forge their IP).
# failed_login_log_trust_forwarded_for = true
```

### Environment Variables

Environment variables override values set in the TOML config file.
Expand Down Expand Up @@ -360,6 +444,17 @@ to 2 queries per second.
- `CODEX_AUTH_REMOTE_USER` will allow unauthenticated logins with the
Remote-User HTTP header. This can be very insecure if not configured properly.
Please read the Remote-User docs devoted to it below.
- `CODEX_AUTH_FAILED_LOGIN_LOG=1` will append every failed login attempt (form
login and OPDS Basic auth) to a separate log file for consumption by banning
tools like fail2ban, CrowdSec, or sshguard. Disabled by default. See the
[Failed-Login Log](#failed-login-log) section below.
- `CODEX_AUTH_FAILED_LOGIN_LOG_PATH` overrides the failed-login log path.
Defaults to `$CODEX_LOG_DIR/failed_logins.log`.
- `CODEX_AUTH_FAILED_LOGIN_LOG_TRUST_FORWARDED_FOR=0` makes the failed-login log
use `REMOTE_ADDR` instead of the leftmost `X-Forwarded-For` entry. Default is
`1` (trust XFF), which is correct when Codex sits behind a reverse proxy. Set
to `0` when Codex is exposed directly so that clients can't forge
`X-Forwarded-For` to poison the log.

### Reverse Proxy

Expand Down Expand Up @@ -442,6 +537,75 @@ set user_token 'user-token-taken-from-web-ui';
proxy_set_header Authorization "Bearer $user_token";
```

### Failed-Login Log

Codex can append every failed login attempt to a dedicated log file in a format
easy for IP-banning tools (fail2ban, CrowdSec, sshguard, etc.) to parse. The
feature is off by default. Enable it by setting `CODEX_AUTH_FAILED_LOGIN_LOG=1`
or in `codex.toml`:

```toml
[auth]
failed_login_log = true
# failed_login_log_path = "" # defaults to <log_dir>/failed_logins.log
# failed_login_log_trust_forwarded_for = true # set false if exposed directly
```

A single signal receiver covers both the form login at `/api/v3/auth/login/` and
OPDS HTTP Basic auth — no separate setup per endpoint. The IP-bearing line is
written **only** to `failed_logins.log`; the main `codex.log` still records
Django's standard `"Unauthorized: /api/v3/auth/login/"` (or `"Forbidden: ..."`)
WARNING for the same request, so the failure is visible in the main log without
the client IP. This keeps PII (IP + username) concentrated in one file that you
can chmod, forward to a SIEM, or retain on its own schedule.

Each line looks like:

```
2026-05-10 12:34:56 | Failed login from 192.168.1.42 user=alice
```

#### X-Forwarded-For trust

The client IP is taken from the leftmost `X-Forwarded-For` entry when
`failed_login_log_trust_forwarded_for = true` (the default), falling back to
`REMOTE_ADDR`. This is correct when Codex sits behind a reverse proxy that sets
the header (the typical Docker deployment).

If Codex is exposed directly on its port, set
`failed_login_log_trust_forwarded_for = false` — otherwise a client can set
their own `X-Forwarded-For: 8.8.8.8` and your banning tool will ban that address
instead of the real attacker.

#### Example fail2ban filter

`/etc/fail2ban/filter.d/codex.conf`:

```ini
[Definition]
failregex = ^\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| Failed login from <HOST> user=.*$
ignoreregex =
```

`/etc/fail2ban/jail.d/codex.conf`:

```ini
[codex]
enabled = true
filter = codex
logpath = /path/to/codex/config/logs/failed_logins.log
maxretry = 5
findtime = 10m
bantime = 1h
```

Validate the filter against a real log with `fail2ban-regex` before enabling the
jail:

```sh
fail2ban-regex /path/to/codex/config/logs/failed_logins.log /etc/fail2ban/filter.d/codex.conf
```

### Restricted Memory Environments

Codex can run with as little as 1GB available RAM. Large batch jobs –like
Expand Down
116 changes: 116 additions & 0 deletions codex/failed_login_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Dedicated log of failed-login attempts.

Designed for consumption by IP-banning tools (fail2ban, CrowdSec, sshguard,
etc.) that tail a log and match offending addresses by regex. One record per
failed credential attempt is emitted with ``logger.bind(failed_login=True)``,
which the dedicated loguru sink in :mod:`codex.startup.loguru` filters into a
separate file. The main stdout / codex.log sinks apply the inverse filter
(:func:`not_failed_login_filter`) so the IP-bearing line **only** lands in the
dedicated log — Django's own request logger still records the bare
``"Unauthorized: /api/v3/auth/login/"`` at WARNING so the failure is visible
in the main log, just without the client IP. Concentrating IPs in one place
makes the privacy story easier to reason about (one file to chmod, one file
to forward to a SIEM, one file to retain on a different schedule).

The :class:`RequestContextMiddleware` stashes each request in a
:class:`~contextvars.ContextVar` so the signal handler can recover the client
IP even when the caller did not propagate ``request=`` into Django's
:func:`~django.contrib.auth.authenticate`. ``rest_registration``'s default
login authenticator omits the request, so without this fallback every form-
login failure would log a dash for the IP.
"""

from contextvars import ContextVar
from inspect import iscoroutinefunction
from typing import TYPE_CHECKING, Final

from asgiref.sync import markcoroutinefunction
from loguru import logger

from codex.settings import AUTH_FAILED_LOGIN_LOG_TRUST_FORWARDED_FOR

if TYPE_CHECKING:
from django.http import HttpRequest

_FAILED_LOGIN_KEY: Final = "failed_login"
_MISSING: Final = "-"
_failed_login_logger = logger.bind(**{_FAILED_LOGIN_KEY: True})
_current_request: ContextVar["HttpRequest | None"] = ContextVar(
"codex_current_request", default=None
)


def get_client_ip(request: "HttpRequest | None") -> str:
"""Return the client IP, preferring leftmost X-Forwarded-For when trusted."""
if request is None:
return _MISSING
if AUTH_FAILED_LOGIN_LOG_TRUST_FORWARDED_FOR and (
forwarded := request.META.get("HTTP_X_FORWARDED_FOR", "").strip()
):
return forwarded.split(",", 1)[0].strip() or _MISSING
return request.META.get("REMOTE_ADDR") or _MISSING


def failed_login_filter(record) -> bool:
"""Loguru sink filter: keep only records tagged via ``logger.bind``."""
return bool(record["extra"].get(_FAILED_LOGIN_KEY))


def not_failed_login_filter(record) -> bool:
"""
Loguru sink filter: drop records tagged as failed-login.

Applied to the main stdout / codex.log sinks so IP-bearing lines stay
confined to the dedicated log file.
"""
return not record["extra"].get(_FAILED_LOGIN_KEY)


def on_login_failed(sender, credentials=None, request=None, **_kwargs) -> None:
"""Receiver for ``django.contrib.auth.signals.user_login_failed``."""
del sender
if request is None:
request = _current_request.get()
ip = get_client_ip(request)
username = (credentials or {}).get("username") or _MISSING
_failed_login_logger.warning(f"Failed login from {ip} user={username}")


class RequestContextMiddleware:
"""
Stash the active request in a contextvar for the signal handler.

DRF's :class:`~rest_framework.authentication.BasicAuthentication` does
propagate ``request=`` into :func:`django.contrib.auth.authenticate`, so
OPDS basic-auth failures already arrive at the signal with a request.
``rest_registration``'s default login authenticator does not, so the
contextvar is the only way to recover the IP for form-login failures.
"""

sync_capable = True
async_capable = True

def __init__(self, get_response) -> None:
"""Initialize response method."""
self.get_response = get_response
self._async = iscoroutinefunction(get_response)
if self._async:
markcoroutinefunction(self) # ty: ignore[invalid-argument-type]

async def _acall(self, request):
token = _current_request.set(request)
try:
return await self.get_response(request)
finally:
_current_request.reset(token)

def __call__(self, request):
"""Set the current-request contextvar around the inner call."""
if self._async:
return self._acall(request)
token = _current_request.set(request)
try:
return self.get_response(request)
finally:
_current_request.reset(token)
16 changes: 16 additions & 0 deletions codex/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ def _vite_dev_server_host() -> str:
##############################

AUTH_REMOTE_USER = get_bool(CODEX_CONFIG, "auth.remote_user", default=False)
AUTH_FAILED_LOGIN_LOG = get_bool(CODEX_CONFIG, "auth.failed_login_log", default=False)
_AUTH_FAILED_LOGIN_LOG_PATH_CONFIG = get_str(
CODEX_CONFIG, "auth.failed_login_log_path", default=""
)
AUTH_FAILED_LOGIN_LOG_PATH = (
Path(_AUTH_FAILED_LOGIN_LOG_PATH_CONFIG)
if _AUTH_FAILED_LOGIN_LOG_PATH_CONFIG
else LOG_DIR / "failed_logins.log"
)
AUTH_FAILED_LOGIN_LOG_TRUST_FORWARDED_FOR = get_bool(
CODEX_CONFIG,
"auth.failed_login_log_trust_forwarded_for",
default=True,
)

##############################
# Codex Config: Browser #
Expand Down Expand Up @@ -497,6 +511,8 @@ def _get_middleware(features: FeatureFlags) -> tuple[str, ...]:
]
if AUTH_REMOTE_USER:
middleware.append("codex.authentication.HttpRemoteUserMiddleware")
if AUTH_FAILED_LOGIN_LOG:
middleware.append("codex.failed_login_log.RequestContextMiddleware")
middleware += [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
Expand Down
Loading