import re import random from functools import wraps from datetime import datetime from flask import Flask, render_template, request, session, redirect, url_for, flash, abort from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from faker import Faker fake = Faker('ru_RU') app = Flask(__name__) application = app app.secret_key = '1234567890secret' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) bcrypt = Bcrypt(app) login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'auth' login_manager.login_message = 'Для доступа к данной странице необходимо пройти процедуру аутентификации.' login_manager.login_message_category = 'warning' # ===== Кастомные типы данных ===== class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) description = db.Column(db.Text, nullable=True) users = db.relationship('User', back_populates='role') def __repr__(self): return f'' class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) login = db.Column(db.String(100), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) last_name = db.Column(db.String(100), nullable=True) first_name = db.Column(db.String(100), nullable=False) middle_name = db.Column(db.String(100), nullable=True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) role = db.relationship('Role', back_populates='users') def get_full_name(self): parts = [self.last_name, self.first_name, self.middle_name] return ' '.join(p for p in parts if p) def check_password(self, password): return bcrypt.check_password_hash(self.password_hash, password) def set_password(self, password): self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8') def __repr__(self): return f'' class VisitLog(db.Model): __tablename__ = 'visit_logs' id = db.Column(db.Integer, primary_key=True) path = db.Column(db.String(100)) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) user = db.relationship('User', backref='visit_logs') def __repr__(self): return f'' @login_manager.user_loader def load_user(user_id): return User.query.filter_by(id=int(user_id)).first() def init_db(): db.create_all() if not Role.query.first(): roles = [ Role(name='Администратор', description='Полный доступ к системе'), Role(name='Пользователь', description='Базовый доступ к системе'), ] db.session.add_all(roles) db.session.commit() if not User.query.first(): admin_role = Role.query.filter_by(name='Администратор').first() admin = User( login='admin', first_name='Администратор', last_name='Системный', middle_name=None, role_id=admin_role.id if admin_role else None ) admin.set_password('Admin123!') db.session.add(admin) db.session.commit() # ===== Декоратор проверки прав ===== def check_rights(action): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: flash('У вас недостаточно прав для доступа к данной странице.', 'warning') return redirect(url_for('index')) role_name = current_user.role.name if current_user.role else None allowed = False if role_name == 'Администратор': allowed = True elif role_name == 'Пользователь': if action in ('edit_users', 'view_profile'): user_id = kwargs.get('user_id') allowed = (user_id == current_user.id) elif action == 'view_logs': allowed = True if not allowed: flash('У вас недостаточно прав для доступа к данной странице.', 'warning') return redirect(url_for('index')) return f(*args, **kwargs) return decorated_function return decorator # ===== Логирование посещений ===== @app.before_request def log_visit(): if not request.path.startswith('/static/'): try: log_entry = VisitLog( path=request.path, user_id=current_user.id if current_user.is_authenticated else None ) db.session.add(log_entry) db.session.commit() except Exception: db.session.rollback() # ===== Валидация ===== def validate_login(login): errors = [] if not login: errors.append('Поле не может быть пустым.') elif len(login) < 5: errors.append('Логин должен содержать не менее 5 символов.') elif not re.match(r'^[a-zA-Z0-9]+$', login): errors.append('Логин должен состоять только из латинских букв и цифр.') return errors def validate_password(password): errors = [] if not password: errors.append('Поле не может быть пустым.') return errors if len(password) < 8: errors.append('Пароль должен содержать не менее 8 символов.') if len(password) > 128: errors.append('Пароль не должен превышать 128 символов.') if not re.search(r'[A-Z]', password): errors.append('Пароль должен содержать хотя бы одну заглавную букву.') if not re.search(r'[a-z]', password): errors.append('Пароль должен содержать хотя бы одну строчную букву.') if not re.search(r'\d', password): errors.append('Пароль должен содержать хотя бы одну цифру.') if ' ' in password: errors.append('Пароль не должен содержать пробелы.') allowed_special = set('~!?@#$%^&*_-+()[]{}><\\/|"\'.,;:') for ch in password: if not (ch.isalpha() or ch.isdigit() or ch in allowed_special): errors.append('Пароль содержит недопустимые символы.') break return errors # ===== Предыдущие функции (Lab 1-3) ===== images_ids = ['Pic1', 'Pic2', 'Pic3', 'Pic4', 'Pic5'] def generate_comments(replies=True): comments = [] for i in range(random.randint(1, 3)): comment = {'author': fake.name(), 'text': fake.text()} if replies: comment['replies'] = generate_comments(replies=False) comments.append(comment) return comments def generate_post(i): return { 'title': fake.company(), 'text': fake.paragraph(nb_sentences=100), 'author': fake.name(), 'date': fake.date_time_between(start_date='-2y', end_date='now'), 'image_id': f'{images_ids[i]}.jpg', 'comments': generate_comments() } posts_list = sorted([generate_post(i) for i in range(5)], key=lambda p: p['date'], reverse=True) @app.route('/posts') def posts(): return render_template('posts.html', title='Посты', posts=posts_list) @app.route('/posts/') def post(index): p = posts_list[index] return render_template('post.html', title=p['title'], post=p) @app.route('/about') def about(): return render_template('about.html', title='Об авторе') @app.route('/visits') def visits(): if 'visits' in session: session['visits'] = session.get('visits') + 1 else: session['visits'] = 1 return render_template('visits.html', title='Счётчик посещений', visits=session['visits']) @app.route('/auth', methods=['GET', 'POST']) def auth(): if current_user.is_authenticated: return redirect(url_for('index')) if request.method == 'POST': login_input = request.form.get('login', '') password_input = request.form.get('password', '') remember = request.form.get('remember') == 'on' user = User.query.filter_by(login=login_input).first() if user and user.check_password(password_input): login_user(user, remember=remember) flash('Вы успешно вошли в систему!', 'success') next_page = request.args.get('next') return redirect(next_page or url_for('index')) flash('Неверный логин или пароль.', 'error') return render_template('login.html', title='Вход') @app.route('/logout') def logout(): logout_user() return redirect(url_for('index')) @app.route('/secret') @login_required def secret(): return render_template('secret.html', title='Секретная страница') @app.route('/request-data', methods=['GET', 'POST']) def request_data(): form_data = None if request.method == 'POST': form_data = { 'login': request.form.get('login', ''), 'password': request.form.get('password', '') } return render_template( 'request_data.html', title='Данные запроса', url_params=request.args, headers=request.headers, cookies=request.cookies, form_data=form_data ) def validate_phone(phone): allowed = set('0123456789 ()-.+') for ch in phone: if ch not in allowed: return None, 'Недопустимый ввод. В номере телефона встречаются недопустимые символы.' digits = re.sub(r'\D', '', phone) stripped = phone.strip() if stripped.startswith('+7') or stripped.startswith('8'): if len(digits) != 11: return None, 'Недопустимый ввод. Неверное количество цифр.' else: if len(digits) != 10: return None, 'Недопустимый ввод. Неверное количество цифр.' if len(digits) == 11: digits = digits[1:] formatted = f'8-{digits[0:3]}-{digits[3:6]}-{digits[6:8]}-{digits[8:10]}' return formatted, None @app.route('/phone', methods=['GET', 'POST']) def phone(): error = None formatted_phone = None phone_value = '' if request.method == 'POST': phone_value = request.form.get('phone', '') formatted_phone, error = validate_phone(phone_value) return render_template( 'phone.html', title='Проверка номера телефона', error=error, formatted_phone=formatted_phone, phone_value=phone_value ) # ===== Обновлённый функционал (Lab 4-5) ===== @app.route('/') def index(): return render_template('index.html', title='Задание — Лабораторная работа №5') @app.route('/users') def users_list(): users = User.query.all() return render_template('users.html', title='Управление пользователями', users=users) @app.route('/users/') @check_rights('view_profile') def user_view(user_id): user = User.query.filter_by(id=user_id).first_or_404() return render_template('user_view.html', title=f'Пользователь: {user.login}', user=user) @app.route('/users/create', methods=['GET', 'POST']) @check_rights('create_users') def user_create(): roles = Role.query.all() errors = {} form_data = {} if request.method == 'POST': login = request.form.get('login', '').strip() password = request.form.get('password', '') last_name = request.form.get('last_name', '').strip() first_name = request.form.get('first_name', '').strip() middle_name = request.form.get('middle_name', '').strip() role_id = request.form.get('role_id') or None form_data = { 'login': login, 'last_name': last_name, 'first_name': first_name, 'middle_name': middle_name, 'role_id': role_id, } login_errors = validate_login(login) if login_errors: errors['login'] = login_errors elif User.query.filter_by(login=login).first(): errors['login'] = ['Пользователь с таким логином уже существует.'] password_errors = validate_password(password) if password_errors: errors['password'] = password_errors if not last_name: errors['last_name'] = ['Поле не может быть пустым.'] if not first_name: errors['first_name'] = ['Поле не может быть пустым.'] if not errors: try: user = User( login=login, first_name=first_name, last_name=last_name or None, middle_name=middle_name or None, role_id=int(role_id) if role_id else None ) user.set_password(password) db.session.add(user) db.session.commit() flash('Пользователь успешно создан!', 'success') return redirect(url_for('users_list')) except Exception as e: db.session.rollback() flash(f'Ошибка при сохранении: {e}', 'error') return render_template('user_create.html', title='Создать пользователя', roles=roles, errors=errors, form_data=form_data) @app.route('/users//edit', methods=['GET', 'POST']) @check_rights('edit_users') def user_edit(user_id): user = User.query.filter_by(id=user_id).first_or_404() roles = Role.query.all() errors = {} is_admin = current_user.role and current_user.role.name == 'Администратор' if request.method == 'POST': last_name = request.form.get('last_name', '').strip() first_name = request.form.get('first_name', '').strip() middle_name = request.form.get('middle_name', '').strip() role_id = request.form.get('role_id') or None form_data = { 'last_name': last_name, 'first_name': first_name, 'middle_name': middle_name, 'role_id': role_id, } if not last_name: errors['last_name'] = ['Поле не может быть пустым.'] if not first_name: errors['first_name'] = ['Поле не может быть пустым.'] if not errors: try: user.last_name = last_name or None user.first_name = first_name user.middle_name = middle_name or None if is_admin: user.role_id = int(role_id) if role_id else None db.session.commit() flash('Пользователь успешно обновлён!', 'success') return redirect(url_for('users_list')) except Exception as e: db.session.rollback() flash(f'Ошибка при сохранении: {e}', 'error') else: form_data = { 'last_name': user.last_name or '', 'first_name': user.first_name or '', 'middle_name': user.middle_name or '', 'role_id': user.role_id, } return render_template('user_edit.html', title='Редактировать пользователя', user=user, roles=roles, errors=errors, form_data=form_data, disable_role=not is_admin) @app.route('/users//delete', methods=['POST']) @check_rights('delete_users') def user_delete(user_id): user = User.query.filter_by(id=user_id).first_or_404() try: db.session.delete(user) db.session.commit() flash('Пользователь успешно удалён!', 'success') except Exception as e: db.session.rollback() flash(f'Ошибка при удалении: {e}', 'error') return redirect(url_for('users_list')) @app.route('/change-password', methods=['GET', 'POST']) @login_required def change_password(): errors = {} if request.method == 'POST': old_password = request.form.get('old_password', '') new_password = request.form.get('new_password', '') confirm_password = request.form.get('confirm_password', '') if not current_user.check_password(old_password): errors['old_password'] = ['Неверный текущий пароль.'] new_password_errors = validate_password(new_password) if new_password_errors: errors['new_password'] = new_password_errors if new_password and not new_password_errors and confirm_password != new_password: errors['confirm_password'] = ['Пароли не совпадают.'] if not errors: try: current_user.set_password(new_password) db.session.commit() flash('Пароль успешно изменён!', 'success') return redirect(url_for('index')) except Exception as e: db.session.rollback() flash(f'Ошибка при изменении пароля: {e}', 'error') return render_template('change_password.html', title='Смена пароля', errors=errors) # ===== Регистрация Blueprint журнала посещений ===== from visit_logs import visit_logs_bp app.register_blueprint(visit_logs_bp) # ===== Инициализация БД ===== with app.app_context(): init_db() if __name__ == '__main__': app.run(debug=True)