518 lines
18 KiB
Python
518 lines
18 KiB
Python
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'<Role {self.name}>'
|
||
|
||
|
||
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'<User {self.login}>'
|
||
|
||
|
||
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'<VisitLog {self.path}>'
|
||
|
||
|
||
@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/<int:index>')
|
||
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/<int:user_id>')
|
||
@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/<int:user_id>/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/<int:user_id>/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)
|