Compare commits
6 Commits
v1.3.5
...
a6d2d176ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d2d176ba | ||
|
|
6e58cce73a | ||
|
|
7d821d1f7b | ||
|
|
c816cb4630 | ||
|
|
3c8d50162b | ||
|
|
483a747600 |
@@ -14,7 +14,9 @@ LOG_FILE_PATH = os.path.join(LOGS_DIR, "backend.log")
|
|||||||
|
|
||||||
def setup_logger():
|
def setup_logger():
|
||||||
logger = logging.getLogger("ainventory")
|
logger = logging.getLogger("ainventory")
|
||||||
logger.setLevel(logging.INFO)
|
# Set to DEBUG for development; change to INFO for production
|
||||||
|
log_level = os.environ.get("LOG_LEVEL", "DEBUG")
|
||||||
|
logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
||||||
|
|
||||||
# Avoid duplicate handlers if setup multiple times
|
# Avoid duplicate handlers if setup multiple times
|
||||||
if logger.handlers:
|
if logger.handlers:
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ def get_ldap_config():
|
|||||||
def authenticate_ldap(username, password):
|
def authenticate_ldap(username, password):
|
||||||
config = get_ldap_config()
|
config = get_ldap_config()
|
||||||
if not config.get("ldap_enabled"):
|
if not config.get("ldap_enabled"):
|
||||||
|
log.debug("LDAP: LDAP is disabled in config")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
log.debug(f"LDAP: Config loaded: server_uri={config.get('server_uri')}, base_dn={config.get('base_dn')}")
|
||||||
try:
|
try:
|
||||||
server = ldap3.Server(config["server_uri"], use_ssl=config.get("use_tls", False), get_info=ldap3.ALL)
|
server = ldap3.Server(config["server_uri"], use_ssl=config.get("use_tls", False), get_info=ldap3.ALL)
|
||||||
|
log.debug(f"LDAP: Server object created: {config['server_uri']}")
|
||||||
user_dn = config["user_template"].format(username=username)
|
user_dn = config["user_template"].format(username=username)
|
||||||
log.debug(f"LDAP: Attempting bind for DN: {user_dn}")
|
log.debug(f"LDAP: Attempting bind for DN: {user_dn}")
|
||||||
|
|
||||||
@@ -91,7 +94,9 @@ def authenticate_ldap(username, password):
|
|||||||
|
|
||||||
return assigned_role
|
return assigned_role
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"LDAP: Auth Error: {str(e)}")
|
log.error(f"LDAP: Auth Error: {type(e).__name__}: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
log.debug(f"LDAP: Full traceback: {traceback.format_exc()}")
|
||||||
return None
|
return None
|
||||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||||
|
|
||||||
@@ -159,16 +164,24 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
|
|||||||
authenticated = False
|
authenticated = False
|
||||||
if user and user.hashed_password:
|
if user and user.hashed_password:
|
||||||
if verify_password(form_data.password, user.hashed_password):
|
if verify_password(form_data.password, user.hashed_password):
|
||||||
|
log.debug(f"Local auth successful for {form_data.username}")
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
else:
|
||||||
|
log.debug(f"Local auth failed: password mismatch for {form_data.username}")
|
||||||
elif user and not user.hashed_password:
|
elif user and not user.hashed_password:
|
||||||
|
log.debug(f"User {form_data.username} exists but has no hashed password (LDAP user), skipping local auth")
|
||||||
# [SECURITY FIX C-02] Bypass for passwordless users has been removed.
|
# [SECURITY FIX C-02] Bypass for passwordless users has been removed.
|
||||||
# LDAP users must authenticate via the LDAP flow below.
|
# LDAP users must authenticate via the LDAP flow below.
|
||||||
pass
|
pass
|
||||||
|
elif not user:
|
||||||
|
log.debug(f"User {form_data.username} not found in database, will try LDAP")
|
||||||
|
|
||||||
# If local failed, try LDAP
|
# If local failed, try LDAP
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
|
log.debug(f"Local auth failed for {form_data.username}, attempting LDAP")
|
||||||
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
||||||
if ldap_role:
|
if ldap_role:
|
||||||
|
log.debug(f"LDAP auth successful for {form_data.username}, role={ldap_role}")
|
||||||
authenticated = True
|
authenticated = True
|
||||||
# Cache hash for offline support
|
# Cache hash for offline support
|
||||||
new_hash = get_password_hash(form_data.password)
|
new_hash = get_password_hash(form_data.password)
|
||||||
@@ -191,6 +204,7 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
else:
|
else:
|
||||||
|
log.warning(f"Login failed: LDAP auth also failed for {form_data.username}")
|
||||||
raise HTTPException(status_code=401, detail="Invalid username or password, or insufficient permissions")
|
raise HTTPException(status_code=401, detail="Invalid username or password, or insufficient permissions")
|
||||||
|
|
||||||
if not authenticated or not user:
|
if not authenticated or not user:
|
||||||
|
|||||||
1
data/ldap_config.json
Normal file
1
data/ldap_config.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"ldap_enabled": true, "server_uri": "ldap://192.168.84.107:3890", "base_dn": "dc=example,dc=com", "user_template": "cn={username},ou=people,dc=example,dc=com", "groups_dn": "ou=groups", "use_tls": false, "role_mappings": [{"group": "inventory_admins", "role": "admin"}, {"group": "inventory_users", "role": "user"}]}
|
||||||
@@ -1,13 +1,52 @@
|
|||||||
# CURRENT AI WORKING SESSION — COMPLETED
|
# CURRENT AI WORKING SESSION — IN PROGRESS
|
||||||
|
|
||||||
**Active AI:** Claude (Sonnet 4.6)
|
**Active AI:** Claude (Haiku 4.5)
|
||||||
**Last Updated:** 2026-04-11
|
**Last Updated:** 2026-04-11
|
||||||
**Current Version:** v1.3.5
|
**Current Version:** v1.3.5
|
||||||
**Branch:** dev (v1.3.5 release branch created)
|
**Branch:** dev
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 SESSION COMPLETED — ALL TASKS FINALIZED + ENGLISH COMPLIANCE VERIFIED
|
## 🔧 CURRENT ISSUE — CORS Configuration Clarification
|
||||||
|
|
||||||
|
### Two Deployment Methods
|
||||||
|
The project supports two distinct deployment approaches:
|
||||||
|
|
||||||
|
#### 1. **Docker Compose** (Production / Containerized)
|
||||||
|
- Uses `docker-compose.yml` with Caddy reverse proxy
|
||||||
|
- Backend container on internal port 8000 → exposed via Caddy on 3002
|
||||||
|
- Frontend container on internal port 3000 → exposed via Caddy on 3003
|
||||||
|
- **Configuration:** `ALLOWED_ORIGINS` environment variable in docker-compose.yml
|
||||||
|
- **Current setting:** `http://localhost:3000,http://localhost:3002`
|
||||||
|
- **For remote deployment:** Must update ALLOWED_ORIGINS to include actual domain (e.g., `https://192.168.84.140:3003`)
|
||||||
|
|
||||||
|
#### 2. **start_server.sh** (Development / Native)
|
||||||
|
- Runs Backend (uvicorn) + Frontend (Next.js) + local-ssl-proxy directly
|
||||||
|
- Backend on port 8000 → SSL proxy on 3002
|
||||||
|
- Frontend on port 3001 → SSL proxy on 3003
|
||||||
|
- **Issue Fixed:** Script now automatically detects local IP and exports ALLOWED_ORIGINS
|
||||||
|
- **Auto-configured for:** localhost + detected IP address on both HTTP and HTTPS
|
||||||
|
|
||||||
|
### Fix Applied (Commit Pending)
|
||||||
|
Updated `start_server.sh` to:
|
||||||
|
1. Detect local IP address (en0/en1 interface)
|
||||||
|
2. Export ALLOWED_ORIGINS with all applicable origins:
|
||||||
|
- HTTP localhost on dev ports (3000/3001)
|
||||||
|
- HTTPS localhost on proxy ports (3002/3003)
|
||||||
|
- HTTPS on detected IP address on proxy ports
|
||||||
|
3. Display CORS configuration when starting backend
|
||||||
|
4. Export JWT_SECRET_KEY if not already set
|
||||||
|
|
||||||
|
**Result:** Frontend at `https://<LOCAL_IP>:3003` can now access backend at `https://<LOCAL_IP>:3002` without CORS blocks.
|
||||||
|
|
||||||
|
### English Language Compliance
|
||||||
|
Fixed Romanian comments in docker-compose.yml:
|
||||||
|
- `"personalizează pentru producție"` → `"customize for production"`
|
||||||
|
- `"GENEREAZĂ O VALOARE SIGURĂ"` → `"GENERATE A SECURE VALUE"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 PREVIOUS SESSION — ALL TASKS FINALIZED + ENGLISH COMPLIANCE VERIFIED
|
||||||
|
|
||||||
### Final Status
|
### Final Status
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- LOGS_DIR=/app/logs
|
- LOGS_DIR=/app/logs
|
||||||
# [M-01] CORS allowed origins — personalizează pentru producție
|
# [M-01] CORS allowed origins — customize for production
|
||||||
- ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002
|
- ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002
|
||||||
# [C-01] JWT secret key — GENEREAZĂ O VALOARE SIGURĂ PENTRU PRODUCȚIE!
|
# [C-01] JWT secret key — GENERATE A SECURE VALUE FOR PRODUCTION!
|
||||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production}
|
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ source .venv/bin/activate
|
|||||||
echo "📦 Updating Python dependencies..."
|
echo "📦 Updating Python dependencies..."
|
||||||
pip install -q -r backend/requirements.txt
|
pip install -q -r backend/requirements.txt
|
||||||
|
|
||||||
# 4. Start Backend (Python/FastAPI)
|
# 4. Get Local IP and set CORS origins
|
||||||
|
LOCAL_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1 || echo "localhost")
|
||||||
|
export ALLOWED_ORIGINS="http://localhost:$FRONTEND_PORT,http://localhost:$BACKEND_PORT,https://localhost:$FRONTEND_SSL_PORT,https://localhost:$BACKEND_SSL_PORT,https://$LOCAL_IP:$FRONTEND_SSL_PORT,https://$LOCAL_IP:$BACKEND_SSL_PORT"
|
||||||
|
export JWT_SECRET_KEY="${JWT_SECRET_KEY:-ephemeral-dev-key-$(date +%s)}"
|
||||||
|
|
||||||
echo "🔥 Starting Backend on port $BACKEND_PORT..."
|
echo "🔥 Starting Backend on port $BACKEND_PORT..."
|
||||||
|
echo " CORS origins: $ALLOWED_ORIGINS"
|
||||||
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT --reload &
|
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT --reload &
|
||||||
|
|
||||||
# 5. Start Frontend (Next.js)
|
# 5. Start Frontend (Next.js)
|
||||||
@@ -39,11 +44,7 @@ cd ..
|
|||||||
npx local-ssl-proxy --source $BACKEND_SSL_PORT --target $BACKEND_PORT --hostname 0.0.0.0 > /dev/null 2>&1 &
|
npx local-ssl-proxy --source $BACKEND_SSL_PORT --target $BACKEND_PORT --hostname 0.0.0.0 > /dev/null 2>&1 &
|
||||||
npx local-ssl-proxy --source $FRONTEND_SSL_PORT --target $FRONTEND_PORT --hostname 0.0.0.0 > /dev/null 2>&1 &
|
npx local-ssl-proxy --source $FRONTEND_SSL_PORT --target $FRONTEND_PORT --hostname 0.0.0.0 > /dev/null 2>&1 &
|
||||||
|
|
||||||
# 7. Get Local IP
|
# 7. Print Unified Access Banner
|
||||||
LOCAL_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1 || echo "<YOUR-IP>")
|
|
||||||
|
|
||||||
# 8. Printing Unified Access Banner
|
|
||||||
LOCAL_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1 || echo "<YOUR-IP>")
|
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
|
|||||||
Reference in New Issue
Block a user