Coverage for app_modules/views.py: 61%
88 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 00:55 +0200
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 00:55 +0200
1"""
2views.py
4Purpose:
5 User-facing HTML pages that are not pure APIs: login/logout, selection page,
6 add camera page. This blueprint wires routes and uses shared services.
8Routes:
9 - GET/POST /, /login: login
10 - GET /logout: logout
11 - GET /select: main selection page shell
12 - GET/POST /dodaj-kameru: add camera form
13"""
15import re
16from datetime import datetime, timedelta
17from flask import Blueprint, render_template, request, redirect, url_for, session, flash, make_response
18from .db import get_db
19from .security import login_required
20from .rate_limit import is_login_allowed, is_ip_allowed, record_login_failure, record_login_success, record_ip_failure
21from .security_enhancements import secure_password_check
22from .audit import log_auth_event, get_request_ip
25bp = Blueprint('views', __name__)
28@bp.route('/', methods=['GET', 'POST'], endpoint='login_root')
29@bp.route('/login', methods=['GET', 'POST'], endpoint='login_login')
30def login():
31 import bcrypt
32 if request.method == 'POST':
33 username = request.form.get('username', '').strip().lower()
34 password = request.form.get('password', '')
35 # Rate limit per (username, ip)
36 client_ip = get_request_ip(request) or ''
37 allowed_user, retry_after_user = is_login_allowed(username, client_ip)
38 allowed_ip, retry_after_ip = is_ip_allowed(client_ip)
39 if not allowed_user or not allowed_ip:
40 flash('Previše pokušaja. Pokušajte ponovno kasnije.', 'error')
41 try:
42 # escalate IP counter even if we blocked before credential check
43 record_ip_failure(client_ip)
44 except Exception:
45 pass
46 try:
47 log_auth_event('login_rate_limited', username=username, ip=client_ip)
48 except Exception:
49 pass
50 resp = make_response(render_template('login.html'))
51 resp.headers['Retry-After'] = str(max(retry_after_user, retry_after_ip))
52 return resp, 429
53 db = get_db()
54 cur = db.execute('SELECT * FROM users WHERE LOWER(username) = ?', (username,))
55 user = cur.fetchone()
56 # Use timing-attack resistant password verification
57 if secure_password_check(username, password, user):
58 # Regenerate session to prevent fixation
59 session.clear()
60 session['user_id'] = user['id']
61 record_login_success(username, client_ip)
62 try:
63 log_auth_event('login_success', username=username, user_id=user['id'], ip=client_ip)
64 except Exception:
65 pass
66 return redirect(url_for('views.select_page'))
67 # Record failed attempt
68 try:
69 record_login_failure(username, client_ip)
70 except Exception:
71 pass
72 try:
73 log_auth_event('login_failure', username=username, ip=client_ip)
74 except Exception:
75 pass
76 flash('Pogrešno korisničko ime ili lozinka.', 'error')
77 # Use the root endpoint to avoid hosts that block /login
78 return redirect(url_for('views.login_root'))
79 return render_template('login.html')
82# Alias to avoid potential upstream restrictions on /login path in some hosts/WAFs
83@bp.route('/auth/login', methods=['POST'])
84def login_post_alias():
85 return login()
88@bp.route('/logout', methods=['POST'])
89def logout():
90 session.pop('user_id', None)
91 return redirect('/')
94@bp.route('/select')
95@login_required
96def select_page():
97 # Server renders shell; data fetched via /api/kamere
98 return render_template('select.html')
101@bp.route('/dodaj-kameru', methods=['GET', 'POST'])
102@login_required
103def add_camera_redesign():
104 if request.method == 'POST':
105 # CSRF protection is enforced globally in initializer
106 camera_id = (request.form.get('camera_id') or '').strip()
107 camera_name = (request.form.get('camera_name') or '').strip()
108 if not camera_id.isdigit() or len(camera_id) != 12:
109 flash('Broj kamere mora imati točno 12 znamenki.', 'error')
110 return redirect(url_for('views.add_camera_redesign'))
111 if not camera_name:
112 flash('Ime kamere je obavezno.', 'error')
113 return redirect(url_for('views.add_camera_redesign'))
114 db = get_db()
115 import sqlite3
116 try:
117 db.execute(
118 'INSERT OR IGNORE INTO cameras (user_id, camera_id, camera_name) VALUES (?, ?, ?)',
119 (session['user_id'], camera_id, camera_name),
120 )
121 db.commit()
122 flash('Kamera je dodana.', 'success')
123 except sqlite3.Error:
124 flash('Dogodila se greška. Pokušajte ponovno.', 'error')
125 return redirect(url_for('views.select_page'))
126 return render_template('add_camera.html')