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

1""" 

2views.py 

3 

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. 

7 

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""" 

14 

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 

23 

24 

25bp = Blueprint('views', __name__) 

26 

27 

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') 

80 

81 

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() 

86 

87 

88@bp.route('/logout', methods=['POST']) 

89def logout(): 

90 session.pop('user_id', None) 

91 return redirect('/') 

92 

93 

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') 

99 

100 

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') 

127 

128