Skip to content

Commit 1a84003

Browse files
committed
v1.2.0: Authentication, directory permissions, and security hardening
- User authentication system (none/full modes) with admin panel - Directory-level permissions with root '/' full-access option - Protect direct /fits/ downloads via nginx auth_request - GAIN FITS header indexing with schema upgrade system - Faster ZIP downloads (STORE compression + streaming) - Permission-aware duplicate badges and improved sorting
2 parents eaf1098 + bf6bcbb commit 1a84003

33 files changed

Lines changed: 1398 additions & 74 deletions

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@ MYSQL_PORT=3306
2525
HEADER_TITLE=Astro Web Indexer
2626

2727
# Data paths
28-
FITS_DATA_PATH=./data/fits
28+
FITS_DATA_PATH=./data/fits
29+
30+
# Authentication configuration
31+
# AUTH_MODE: 'none' = no auth (default), 'full' = login required with directory permissions
32+
AUTH_MODE=none
33+
34+
# Initial admin user (created automatically on first startup when AUTH_MODE is not 'none')
35+
ADMIN_USER=admin
36+
ADMIN_PASSWORD=changeme

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Changelog
2+
3+
## v1.2.0
4+
5+
### Authentication & Security
6+
- **User authentication system** with two modes: `none` (legacy, no auth) and `full` (login required)
7+
- **Directory-level permissions** — restrict user access to specific root directories
8+
- **Root `/` permission** — grants full access to all directories via a single toggle
9+
- **Admin panel** for user CRUD with directory checkboxes and last-admin protection
10+
- **Protect direct file downloads** — nginx `auth_request` gates all `/fits/` access through PHP session/permission checks
11+
- Auto-creation of initial admin user via `ADMIN_USER` / `ADMIN_PASSWORD` env vars
12+
- CSRF protection on all forms
13+
- i18n for all auth strings (en, it, fr, es, de)
14+
15+
### Features
16+
- **GAIN FITS header indexing** — new `gain` column (camera gain setting), separate from the existing `egain` (electrons per ADU)
17+
- **Schema upgrade system** — lightweight header-only reindex when new fields are added (no thumbnail regeneration)
18+
- Fix: AstroBin CSV export now includes `filter` and uses `gain` instead of `egain`
19+
20+
### Performance
21+
- **Faster ZIP downloads**`STORE` compression for binary FITS/XISF files + nginx streaming (no buffering)
22+
23+
### Improvements
24+
- **Permission-aware duplicate badges** — duplicate counts reflect only files the user can access
25+
- **Duplicate sorting** — secondary sort by total count when ordering by visible duplicates
26+
- Language selector on login and admin pages
27+
- UI layout fixes for header when auth is enabled
28+
29+
## v1.1.0
30+
31+
Initial public release.

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,15 @@ The indexing engine is designed to be fast, efficient, and resilient, making it
6666
- 🎨 Modern, dark-themed interface
6767
- ⚡ Fast and efficient file browsing
6868

69+
### 🔒 Authentication & Access Control
70+
- Optional user authentication with two modes: `none` (open access) and `full` (login required)
71+
- Directory-level permissions to restrict user access to specific folders
72+
- Admin panel for user management
73+
- Direct file download protection via nginx
74+
6975
### Technical Features
7076
- 🐳 Dockerized deployment for easy setup
7177
- 🗄️ MariaDB backend with schema migrations managed by **Phinx**.
72-
- 🔒 Secure file handling and access control
7378
- 📊 Extensive FITS/XISF header metadata extraction and indexing.
7479

7580
## 📋 Requirements
@@ -146,6 +151,14 @@ These variables are shared across all services to connect to the MariaDB contain
146151
| `DB_PASSWORD` | The password for the database user. | `awi_password` |
147152
| `MYSQL_ROOT_PASSWORD` | The root password for the MariaDB server. **It is highly recommended to change this.** | `rootpassword` |
148153
154+
### 🔒 Authentication
155+
156+
| Variable | Description | Default |
157+
|----------|-------------|---------|
158+
| `AUTH_MODE` | Set to `full` to enable login and directory permissions. Set to `none` to disable authentication. | `none` |
159+
| `ADMIN_USER` | Username for the initial admin account (created on first startup). | `admin` |
160+
| `ADMIN_PASSWORD` | Password for the initial admin account. **Change this immediately.** | `changeme` |
161+
149162
### 🎨 Custom Logo
150163
151164
You can replace the default logo with your own by mapping a local SVG file.

docker-compose.release.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
services:
66
nginx:
7-
image: ghcr.io/michelegz/astro-web-indexer-nginx:v1.1.0
7+
image: ghcr.io/michelegz/astro-web-indexer-nginx:v1.2.0
88
container_name: nginx-awi
99
ports:
1010
- "${NGINX_PORT:-2080}:80"
@@ -15,7 +15,7 @@ services:
1515
- php
1616

1717
php:
18-
image: ghcr.io/michelegz/astro-web-indexer-php:v1.1.0
18+
image: ghcr.io/michelegz/astro-web-indexer-php:v1.2.0
1919
container_name: php-awi
2020
volumes:
2121
- ${FITS_DATA_PATH:-./data/fits}:/var/fits:ro
@@ -29,13 +29,16 @@ services:
2929
- DB_NAME=${DB_NAME:-awi_db}
3030
- DB_USER=${DB_USER:-awi_user}
3131
- DB_PASSWORD=${DB_PASSWORD:-awi_password}
32+
- AUTH_MODE=${AUTH_MODE:-none}
33+
- ADMIN_USER=${ADMIN_USER:-admin}
34+
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}
3235
restart: unless-stopped
3336
depends_on:
3437
mariadb:
3538
condition: service_healthy
3639

3740
python:
38-
image: ghcr.io/michelegz/astro-web-indexer-python:v1.1.0
41+
image: ghcr.io/michelegz/astro-web-indexer-python:v1.2.0
3942
container_name: python-awi
4043
volumes:
4144
- ${FITS_DATA_PATH:-./data/fits}:/var/fits:ro
@@ -54,7 +57,7 @@ services:
5457
condition: service_healthy
5558

5659
mariadb:
57-
image: ghcr.io/michelegz/astro-web-indexer-mariadb:v1.1.0
60+
image: ghcr.io/michelegz/astro-web-indexer-mariadb:v1.2.0
5861
container_name: mariadb-awi
5962
environment:
6063
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ services:
3333
- DB_NAME=${DB_NAME:-awi_db}
3434
- DB_USER=${DB_USER:-awi_user}
3535
- DB_PASSWORD=${DB_PASSWORD:-awi_password}
36+
- AUTH_MODE=${AUTH_MODE:-none}
37+
- ADMIN_USER=${ADMIN_USER:-admin}
38+
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}
3639
restart: unless-stopped
3740
depends_on:
3841
mariadb:

docker/nginx/default.conf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,23 @@ server {
1010
}
1111

1212
location /fits/ {
13+
auth_request /auth_check;
1314
alias /var/fits/;
1415
autoindex on;
1516
}
1617

18+
# Internal endpoint for auth_request subrequests
19+
location = /auth_check {
20+
internal;
21+
fastcgi_pass php:9000;
22+
include fastcgi_params;
23+
fastcgi_param SCRIPT_FILENAME /var/www/html/auth_check.php;
24+
fastcgi_param CONTENT_LENGTH "";
25+
fastcgi_param CONTENT_TYPE "";
26+
fastcgi_pass_request_body off;
27+
fastcgi_param HTTP_X_ORIGINAL_URI $request_uri;
28+
}
29+
1730
location ~ \.php$ {
1831
include fastcgi_params;
1932
fastcgi_pass php:9000;
@@ -29,6 +42,10 @@ server {
2942

3043
# Increase the timeout specifically for zip file generation
3144
fastcgi_read_timeout 3600;
45+
46+
# Disable buffering for streaming ZIP downloads
47+
fastcgi_buffering off;
48+
proxy_buffering off;
3249
}
3350

3451
location ~ /\. {

docker/php/docker-entrypoint.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ set -e
55
echo "Running database migrations..."
66
vendor/bin/phinx migrate
77

8+
# Create initial admin user if AUTH_MODE is set and no users exist
9+
if [ "$AUTH_MODE" != "" ] && [ "$AUTH_MODE" != "none" ]; then
10+
if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_PASSWORD" ]; then
11+
php -r "
12+
require_once '/var/www/html/includes/config.php';
13+
require_once '/var/www/html/includes/db_functions.php';
14+
require_once '/var/www/html/includes/auth.php';
15+
\$conn = connectDB();
16+
if (!hasAnyUser(\$conn)) {
17+
createUser(\$conn, getenv('ADMIN_USER'), getenv('ADMIN_PASSWORD'), true, true, []);
18+
echo \"Initial admin user created.\n\";
19+
}
20+
"
21+
fi
22+
fi
23+
824
# Execute the command passed to the script (e.g., "php-fpm")
925
echo "Starting PHP-FPM..."
1026
exec "$@"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Schema upgrade utilities for the reindexer.
3+
4+
When a new FITS header field is added to the database schema, a new entry
5+
must be added to SCHEMA_VERSION_FIELDS mapping the new schema version to the
6+
list of DB column names that are populated from the FITS header.
7+
8+
The upgrade worker reads only the file header (no pixel data, no thumbnail
9+
regeneration), extracts the relevant fields, and returns them to the main
10+
process which performs the DB UPDATE.
11+
12+
To add a new schema version in the future:
13+
1. Add the DB column via a Phinx migration.
14+
2. Add the new version and its fields to SCHEMA_VERSION_FIELDS.
15+
3. Add the corresponding header extraction inside schema_upgrade_worker.
16+
4. Increment current_schema_version in reindex.py.
17+
"""
18+
19+
import os
20+
import logging
21+
22+
from astropy.io import fits
23+
from xisf import XISF
24+
25+
from indexer_lib.file_utils import get_header_value, get_xisf_header_value
26+
27+
logger = logging.getLogger('reindex')
28+
29+
# Maps each schema version to the DB column names introduced in that step.
30+
# Keys are version numbers; values are lists of field names.
31+
# Only steps with version > old_version are applied to a given file.
32+
SCHEMA_VERSION_FIELDS = {
33+
2: ['gain'], # v1 -> v2: GAIN FITS header (camera gain setting, ADU units)
34+
}
35+
36+
37+
def schema_upgrade_worker(task, fits_root):
38+
"""Lightweight multiprocessing worker for schema version upgrades.
39+
40+
Reads only the FITS/XISF header — does NOT load pixel data or regenerate
41+
thumbnails.
42+
43+
Args:
44+
task: Tuple of (full_path: str, old_schema_version: int)
45+
fits_root: Root directory of the FITS file tree
46+
47+
Returns:
48+
dict with 'status', 'path', 'old_version', and one key per extracted
49+
field. On error: {'status': 'error', 'path': ..., 'reason': ...}
50+
"""
51+
full_path, old_version = task
52+
rel_path = os.path.relpath(full_path, fits_root)
53+
file_lower = full_path.lower()
54+
55+
# Determine which fields need to be extracted for this upgrade path
56+
needed_fields = set()
57+
for step_v in sorted(SCHEMA_VERSION_FIELDS.keys()):
58+
if step_v > old_version:
59+
needed_fields.update(SCHEMA_VERSION_FIELDS[step_v])
60+
61+
try:
62+
result = {'status': 'success', 'path': rel_path, 'old_version': old_version}
63+
64+
if needed_fields:
65+
header = None
66+
get_value = None
67+
if file_lower.endswith(('.fits', '.fit')):
68+
with fits.open(full_path, ignore_missing_end=True) as hdul:
69+
header = hdul[0].header
70+
get_value = get_header_value
71+
elif file_lower.endswith('.xisf'):
72+
xisf_file = XISF(full_path)
73+
images_meta = xisf_file.get_images_metadata()
74+
if images_meta:
75+
header = images_meta[0].get('FITSKeywords', {})
76+
get_value = get_xisf_header_value
77+
78+
if header is not None and get_value is not None:
79+
# v1 -> v2
80+
if 'gain' in needed_fields:
81+
result['gain'] = get_value(header, 'GAIN', None, float)
82+
# v2 -> v3: add new fields here in the future
83+
84+
return result
85+
except Exception as e:
86+
logger.error(f"Schema upgrade error for {rel_path}: {e}")
87+
return {'status': 'error', 'path': rel_path, 'reason': str(e)}

0 commit comments

Comments
 (0)