diff --git a/server-tx/app/__init__.py b/server-tx/app/__init__.py new file mode 100644 index 0000000..8c233fe --- /dev/null +++ b/server-tx/app/__init__.py @@ -0,0 +1,34 @@ +from flask import Flask +from flask import globals +from flask import render_template +from datetime import timedelta + +from app.admin import m_admin +from app.auth import m_auth +from app.print import m_print +from app.config import _SECRET_SESSION_KEY as SESSION_KEY + +def get_app(): + app = Flask(__name__) + app.secret_key = SESSION_KEY + + app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) + app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10mb + + @app.errorhandler(Exception) + def e_handler(e): + code = getattr(e, 'code', 500) + if code.__eq__(404): + return app.redirect('/') + return render_template('error.html', error=e), code + + app.register_blueprint(m_print) + app.register_blueprint(m_auth, url_prefix='/auth') + app.register_blueprint(m_admin, url_prefix='/admin') + + from app._tools.database import DataManager + from app._tools.database import SessionManager + DataManager.init_table() + SessionManager.init_table() + + return app \ No newline at end of file diff --git a/server-tx/app/_tools/database.py b/server-tx/app/_tools/database.py new file mode 100644 index 0000000..e3f000e --- /dev/null +++ b/server-tx/app/_tools/database.py @@ -0,0 +1,155 @@ +import time +import sqlite3 as sql +import json + +from app._tools.models.user import User +from app._tools.models.session import Session + +from app.config import _USER_DATABASE as U_DB_PATH +from app.config import _TOKEN_DATABASE as T_DB_PATH +from app.config import _SERVER_CONFIG as S_CFG_PATH +from app.config import _SCHEMA_USERS as SCHEMA_USERS +from app.config import _SCHEMA_TOKENS as SCHEMA_TOKENS + +class ConnectManager: + def __init__(self, db_file : str): + self._conn = sql.connect( + db_file, + timeout = 10, + check_same_thread = False + ) + self._curs = self._conn.cursor() + self._is_destroyed = False + def _save_and_close(self, mode : int): + if mode == 0: + self._conn.commit() + self._conn.close() + def execute(self, request, mode : int = 0, close_connection : bool = True): + """ + mode: + - 0 if just execute (with commit); + - 1 if fetch one (no commit); + - 2 if fetch many (no commit); + """ + if self._is_destroyed: + return "Connection was destroyed." + if isinstance(request, tuple): + response = self._curs.execute(*request) + else: + response = self._curs.execute(request) + if mode == 1: + response = self._curs.fetchone() + if mode == 2: + response = self._curs.fetchall() + if close_connection: + self._is_destroyed = True + self._save_and_close(mode) + return response + +class SessionManager: + """ + Static methods only; + No need to create class sample. + """ + def __init__(self): + pass + def init_table(): + c_man = ConnectManager(T_DB_PATH) + c_man.execute(SCHEMA_TOKENS, close_connection = False) + c_man.execute("VACUUM;") + def add_session(token : str, user_id : int, expires : int = None): + """ + Expires feature is not realized yet :( + """ + c_man = ConnectManager(T_DB_PATH) + c_man.execute( + ("INSERT INTO tokens VALUES (?, ?, ?)", (token, user_id, 0)) + ) + def get_session(token : str): + c_man = ConnectManager(T_DB_PATH) + data = c_man.execute( + ("SELECT * FROM tokens WHERE token=?", (token,)), + 1 + ) + if data == None: + return None + return Session(*data) + def remove_session(token : str): + c_man = ConnectManager(T_DB_PATH) + c_man.execute( + ("DELETE FROM tokens WHERE token=?", (token,)) + ) + def remove_session_by_user(id : int): + c_man = ConnectManager(T_DB_PATH) + c_man.execute( + ("DELETE FROM tokens WHERE user_id=?", (id,)) + ) + +class DataManager: + """ + Static methods only; + No need to create class sample. + """ + def __init__(self): + pass + def init_table(): + c_man = ConnectManager(U_DB_PATH) + c_man.execute(SCHEMA_USERS, close_connection = False) + c_man.execute("VACUUM;") + def get_user_by_name(username : str) -> User: + c_man = ConnectManager(U_DB_PATH) + data = c_man.execute( + ("SELECT * FROM users WHERE username=?", (username,)), + 1 + ) + if data == None: + return None + return User(*data) + def get_user(id : int) -> User: + c_man = ConnectManager(U_DB_PATH) + data = c_man.execute( + ("SELECT * FROM users WHERE id=?", (id,)), + 1 + ) + if data == None: + return None + return User(*data) + def get_all_users() -> list: + c_man = ConnectManager(U_DB_PATH) + data = c_man.execute( + "SELECT * FROM users", + 2 + ) + userlist = list() + for user in data: + userlist.append(User(*user)) + return userlist + def add_user(username : str, team : str, loc : str, passkey : str): + c_man = ConnectManager(U_DB_PATH) + c_man.execute( + ("INSERT INTO users (username, team, loc, passkey, reg_ts) VALUES (?, ?, ?, ?, ?)", (username, team, loc, passkey, int(time.time()))) + ) + def edit_user(id : int, field : str, value): + c_man = ConnectManager(U_DB_PATH) + c_man.execute( + (f"UPDATE users SET {field} = ? WHERE id = ?", (value, id)) + ) + def del_user(id : int): + c_man = ConnectManager(U_DB_PATH) + c_man.execute( + ("DELETE FROM users WHERE id = ?", (id,)) + ) + +class ConfigManager: + """ + Static methods only; + No need to create class sample. + """ + def __init__(): + pass + def get_config(): + with open(S_CFG_PATH, 'r', encoding = "utf-8") as file: + return json.load(file) + def set_config(config): + with open(S_CFG_PATH, "w", encoding = "utf-8") as file: + json.dump(config, file, indent = 4, ensure_ascii = False) \ No newline at end of file diff --git a/server-tx/app/_tools/decorators.py b/server-tx/app/_tools/decorators.py new file mode 100644 index 0000000..7d6435e --- /dev/null +++ b/server-tx/app/_tools/decorators.py @@ -0,0 +1,31 @@ +# +# @auth_required decorator +# @admin_required decorator +# + +from flask import session, redirect +from app._tools.database import SessionManager +from app._tools.database import DataManager + +def auth_required(func): + def auth_check(*args, **kwargs): + if ("token" not in session): + return redirect("/auth") + token_i = SessionManager.get_session(session["token"]) + if (token_i == None): + return redirect("/auth") + return func(*args, **kwargs) + auth_check.__name__ = func.__name__ + return auth_check + +def admin_required(func): + def admin_check(*args, **kwargs): + token_i = SessionManager.get_session(session["token"]) + user = DataManager.get_user(token_i.user_id) + if (user == None): + return redirect("/") + if (user.team != "$root$"): + return redirect("/") + return func(*args, **kwargs) + admin_check.__name__ = func.__name__ + return admin_check \ No newline at end of file diff --git a/server-tx/app/_tools/models/session.py b/server-tx/app/_tools/models/session.py new file mode 100644 index 0000000..e6a9a20 --- /dev/null +++ b/server-tx/app/_tools/models/session.py @@ -0,0 +1,10 @@ +class Session: + def __init__( + self, + token : str, + user_id : int, + expires : int + ): + self.token = token + self.user_id = user_id + self.expires = expires \ No newline at end of file diff --git a/server-tx/app/_tools/models/user.py b/server-tx/app/_tools/models/user.py new file mode 100644 index 0000000..1fda728 --- /dev/null +++ b/server-tx/app/_tools/models/user.py @@ -0,0 +1,20 @@ +class User: + def __init__( + self, + id : int, + username : str, + team : str, + loc : str, + passkey : str, + reg_ts : int, + lastprint_ts : int, + pages : int + ): + self.id = id + self.username = username + self.team = team + self.loc = loc + self.passkey = passkey + self.reg_ts = reg_ts + self.lastprint_ts = lastprint_ts + self.pages = pages \ No newline at end of file diff --git a/server-tx/app/_tools/passhash.py b/server-tx/app/_tools/passhash.py new file mode 100644 index 0000000..e4cc4b4 --- /dev/null +++ b/server-tx/app/_tools/passhash.py @@ -0,0 +1,27 @@ +import bcrypt +from secrets import token_hex + +_ENCODING = "utf-8" + +def hashPassword( + pswd : str, + salt : str +) -> str: + return bcrypt.hashpw( + pswd.encode(encoding=_ENCODING), + salt.encode(encoding=_ENCODING) + ).decode(encoding=_ENCODING) + +def getSalt() -> str: + return bcrypt.gensalt().decode(encoding=_ENCODING) + +def isValid( + pswd : str, + salt : str, + hash : str +) -> bool: + hashed = hashPassword(pswd, salt) + return (hash.__eq__(hashed)) + +def genToken(): + return token_hex(32) \ No newline at end of file diff --git a/server-tx/app/admin/__init__.py b/server-tx/app/admin/__init__.py new file mode 100644 index 0000000..7c26fa0 --- /dev/null +++ b/server-tx/app/admin/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +m_admin = Blueprint( + 'admin', + __name__, + template_folder='templates' +) + +from . import views \ No newline at end of file diff --git a/server-tx/app/admin/templates/admin/admin_main.html b/server-tx/app/admin/templates/admin/admin_main.html new file mode 100644 index 0000000..8359652 --- /dev/null +++ b/server-tx/app/admin/templates/admin/admin_main.html @@ -0,0 +1,101 @@ +{% extends "general.html" %} + +{% block title %}Панель администратора{% endblock %} +{% block header %}Админ: {{ user.username }} | Панель | На главную{% endblock %} + +{% block content %} +
+

Список пользователей

+

Тык, чтобы создать пользователя

+ + + + + + + + + + {% for cuser in users %} + + + + + + {% endfor %} + +
ЛогинКомандаДействия
{{ cuser.username }}{{ cuser.team }} + {% if cuser.team not in ("$root$") %} + ✏️ + 🗑️ + {% else %} + Только по запросу | ID: {{ cuser.id }} + {% endif %} +
+
+
+

Активность пользователей

+

Активность администраторов и преподавательского состава не отображается.

+ + + + + + + + + + {% for cuser in users %} + {% if cuser.team not in ("$root$", "$tutor$") %} + + + + + + {% endif %} + {% endfor %} + +
ЛогинПоследняя печатьКоличество страниц
{{ cuser.username }}{{ cuser.lastprint_ts|format_timestamp }}{{ cuser.pages }}
+
+ +
+

Настройки печати

+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

Настройки принимающего сервера

+

Перед любыми изменениям убедитесь, что вы знаете, что делаете! По любым вопросам обращайтесь к Администратору системы.

+

+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/admin/templates/admin/user_create.html b/server-tx/app/admin/templates/admin/user_create.html new file mode 100644 index 0000000..5ce529c --- /dev/null +++ b/server-tx/app/admin/templates/admin/user_create.html @@ -0,0 +1,29 @@ +{% extends "general.html" %} + +{% block title %}Создание пользователя{% endblock %} +{% block header %}Создание пользователя{% endblock %} + +{% block content %} +
+

Создание новой команды

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/admin/templates/admin/user_create_many.html b/server-tx/app/admin/templates/admin/user_create_many.html new file mode 100644 index 0000000..5aa5a91 --- /dev/null +++ b/server-tx/app/admin/templates/admin/user_create_many.html @@ -0,0 +1,29 @@ +{% extends "general.html" %} + +{% block title %}Создание пользователя{% endblock %} +{% block header %}Создание пользователя{% endblock %} + +{% block content %} +
+

Создание новой команды

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} diff --git a/server-tx/app/admin/templates/admin/user_created.html b/server-tx/app/admin/templates/admin/user_created.html new file mode 100644 index 0000000..fc6c03c --- /dev/null +++ b/server-tx/app/admin/templates/admin/user_created.html @@ -0,0 +1,33 @@ +{% extends "general.html" %} + +{% block title %}Пользователь создан{% endblock %} +{% block header %}Пользователь создан{% endblock %} + +{% block content %} +
+

Успешно создан пользователь {{ cuser.username }}!

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ Данную страницу можно отправить новому пользователю платформы! +
+
+ +
+ Вернуться обратно +
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/admin/templates/admin/user_edit.html b/server-tx/app/admin/templates/admin/user_edit.html new file mode 100644 index 0000000..2d071f4 --- /dev/null +++ b/server-tx/app/admin/templates/admin/user_edit.html @@ -0,0 +1,29 @@ +{% extends "general.html" %} + +{% block title %}Редактирование пользователя{% endblock %} +{% block header %}Редактирование {{ edituser.username }}{% endblock %} + +{% block content %} +
+

Редактирование команды

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/admin/views.py b/server-tx/app/admin/views.py new file mode 100644 index 0000000..09a0b29 --- /dev/null +++ b/server-tx/app/admin/views.py @@ -0,0 +1,187 @@ +from . import m_admin + +import json + +from datetime import datetime +from flask import render_template +from flask import Response +from flask import redirect +from flask import request +from flask import session +from app._tools.decorators import auth_required +from app._tools.decorators import admin_required +from app._tools.database import DataManager +from app._tools.database import SessionManager +from app._tools.database import ConfigManager +from app.config import _SUPER_ADMINS as SUPER +from app.config import _SALT as SALT +from app._tools import passhash + +@m_admin.app_template_filter("format_timestamp") +def format_timestamp(value): + try: + dt = datetime.fromtimestamp(value) + return dt.strftime("%d.%m.%Y %H:%M") + except: + return "Invalid timestamp" + +@m_admin.route('/') +@auth_required +@admin_required +def panel(): + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + users = DataManager.get_all_users() + return render_template('admin/admin_main.html', user = user, users = users, settings = ConfigManager.get_config()) + +@m_admin.route('/create') +@auth_required +@admin_required +def create(): + return render_template("admin/user_create.html") + +@m_admin.route('/create_many') +@auth_required +@admin_required +def create_manu(): + return render_template("admin/user_create_many.html") + +@m_admin.route('/account_created_many', methods=['POST']) +@auth_required +@admin_required +def create_many_proc(): + data = dict(request.form) + usernames = str(data['username']).split(';') + passwords = str(data['password']).split(';') + teams = str(data['team_name']).split(';') + locations = str(data['location']).split(';') + + for i in range(len(usernames)): + if (DataManager.get_user_by_name(usernames[i]) != None): + return render_template("error.html", error = "Пользователь с таким логином существует!") + if len(passwords[i]) < 4: + return render_template("error.html", error = "Длина пароля должна превышать 3 символа!") + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + if teams[i] == "$root$" and user.username not in SUPER: + return render_template("error.html", error = "У вас нет прав на работу с группой $root$!") + DataManager.add_user( + usernames[i], + teams[i], + locations[i], + passhash.hashPassword(passwords[i], SALT) + ) + cuser = DataManager.get_user_by_name(data['username']) + return "Done" + +@m_admin.route('/account_created', methods=['POST']) +@auth_required +@admin_required +def create_proc(): + data = dict(request.form) + if (DataManager.get_user_by_name(data['username']) != None): + return render_template("error.html", error = "Пользователь с таким логином существует!") + if len(data['password']) < 4: + return render_template("error.html", error = "Длина пароля должна превышать 3 символа!") + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + if data['team_name'] == "$root$" and user.username not in SUPER: + return render_template("error.html", error = "У вас нет прав на работу с группой $root$!") + DataManager.add_user( + data['username'], + data['team_name'], + data['location'], + passhash.hashPassword(data['password'], SALT) + ) + cuser = DataManager.get_user_by_name(data['username']) + return render_template("admin/user_created.html", cuser = cuser, password_onetime = data['password']) + +@m_admin.route('/edit/') +@auth_required +@admin_required +def edit_team(id : int): + euser = DataManager.get_user(id) + return render_template('admin/user_edit.html', edituser = euser) + +@m_admin.route('/edit/settings', methods=['POST']) +@auth_required +@admin_required +def edit_settings(): + data = dict(request.form) + cur_config = ConfigManager.get_config() + cur_config['print']['watermark'] = data['watermark'] + data['pages-per-request'] = int(data['pages-per-request']) + data['seconds-per-request'] = int(data['seconds-per-request']) + if data['pages-per-request'] in range(1, 51): + cur_config['print']['limits']['pages-per-request'] = data['pages-per-request'] + else: + return render_template("error.html", error = "Лимит страниц должен быть в рамках от 1 до 50 страниц!") + if data['seconds-per-request'] in range(5, 61): + cur_config['print']['limits']['seconds-per-request'] = data['seconds-per-request'] + else: + return render_template("error.html", error = "Задержка между запросами должна быть от 5 до 60 секунд!") + ConfigManager.set_config(cur_config) + return redirect("/admin/") + +@m_admin.route('/edit/server-rx', methods=['POST']) +@auth_required +@admin_required +def edit_server_rx(): + data = dict(request.form) + cur_config = ConfigManager.get_config() + if int(data['server-port']) not in range(1, 65536): + return render_template("error.html", error = "Порт в диапазоне от 1 до 65535!") + cur_config['server']['port'] = int(data['server-port']) + cur_config['server']['ip'] = str(data['server-ip']) + cur_config['server']['secret'] = str(data['server-secret']) + ConfigManager.set_config(cur_config) + gen_json = dict() + gen_json = cur_config['server'] + gen_json = json.dumps( + gen_json, + indent = 4 + ) + response = Response(gen_json, mimetype='application/json') + response.headers['Content-Disposition'] = 'attachment; filename=server-rx.json' + response.headers['Content-Type'] = 'application/json' + response.headers['Refresh'] = '1; url=' + "/admin" + return response + +@m_admin.route('/edit/proc/', methods=['POST']) +@auth_required +@admin_required +def edit_proc(id : int): + data = dict(request.form) + e_user = DataManager.get_user(id) + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + if data['team_name'] == "$root$" and user.username not in SUPER: + return render_template("error.html", error = "У вас нет прав на работу с группой $root$!") + if e_user.team == "$root$" and user.username not in SUPER: + return render_template("error.html", error = "Вы не можете редактировать других Администраторов!") + if (len(data['new_password']) > 3): + DataManager.edit_user( + id, + 'passkey', + passhash.hashPassword(data['new_password'], SALT) + ) + if e_user.team != data['team_name']: + DataManager.edit_user( + id, + 'team', + data['team_name'] + ) + if e_user.loc != data['location']: + DataManager.edit_user( + id, + 'loc', + data['location'] + ) + return redirect("/admin") + +@m_admin.route('/remove/') +@auth_required +@admin_required +def remove_team(id : int): + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + if user.username not in SUPER: + return render_template("error.html", error = "Обратитесь к Супер-администратору. У вас нет прав на удаление пользователей!") + SessionManager.remove_session_by_user(id) + DataManager.del_user(id) + return redirect("/admin") diff --git a/server-tx/app/auth/__init__.py b/server-tx/app/auth/__init__.py new file mode 100644 index 0000000..5e86a3f --- /dev/null +++ b/server-tx/app/auth/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +m_auth = Blueprint( + 'auth', + __name__, + template_folder='templates' +) + +from . import views \ No newline at end of file diff --git a/server-tx/app/auth/templates/auth/login_form.html b/server-tx/app/auth/templates/auth/login_form.html new file mode 100644 index 0000000..56914e2 --- /dev/null +++ b/server-tx/app/auth/templates/auth/login_form.html @@ -0,0 +1,21 @@ +{% extends "general.html" %} + +{% block title %}Авторизация{% endblock %} +{% block header %}Авторизация на платформе{% endblock %} + +{% block content %} +
+

Авторизация

+
+
+ + +
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/auth/views.py b/server-tx/app/auth/views.py new file mode 100644 index 0000000..7ef3e66 --- /dev/null +++ b/server-tx/app/auth/views.py @@ -0,0 +1,41 @@ +from . import m_auth + +from flask import render_template +from flask import redirect +from flask import request +from flask import session +from app._tools.database import SessionManager +from app._tools.database import DataManager +from app._tools.decorators import auth_required +from app._tools import passhash +from app.config import _SALT as SALT + +@m_auth.route('/') +def login_page(): + return render_template('auth/login_form.html') + +@m_auth.route('/session/login', methods=['POST']) +def login(): + data = dict(request.form) + if (len(data['username']) == 0 or len(data['password']) == 0): + return redirect("/auth") + user_i = DataManager.get_user_by_name(data['username']) + if user_i == None: + return render_template("error.html", error = "Пользователь не найден!") + if not passhash.isValid(data['password'], SALT, user_i.passkey): + return render_template("error.html", error = "Неверный пароль!") + + new_token = passhash.genToken() + SessionManager.add_session(new_token, user_i.id) + + session.permanent = True + session["token"] = new_token + return redirect("/") + +@m_auth.route('/logout') +@auth_required +def logout(): + current_token = session["token"] + session.clear() + SessionManager.remove_session(current_token) + return redirect("/") \ No newline at end of file diff --git a/server-tx/app/config.py b/server-tx/app/config.py new file mode 100644 index 0000000..7c64a2a --- /dev/null +++ b/server-tx/app/config.py @@ -0,0 +1,42 @@ + +# +# Web-site platform settings +# It's better to edit only #edit fields! +# + +_USER_DATABASE = "databases/users.db" #default +_TOKEN_DATABASE = "databases/tokens.db" #default +_SERVER_CONFIG = "server-tx.json" #default +_SALT = '$2b$12$DE09JKsqy6Ii/zAxEAA5n.' #edit +_SECRET_SESSION_KEY = "fpmi_cup_event_2025_by_MMCFPMI_developed_by_gallahad_ru_Welcomeee!!!" #edit +_SUPER_ADMINS = ( #edit + "god" +) + +# +# !!! WARNING !!! +# Please, don't edit this +# w/o understanding! +# + +_SCHEMA_TOKENS = """ +CREATE TABLE IF NOT EXISTS tokens ( + token TEXT, + user_id INTEGER, + expires INTEGER +); +""" +_SCHEMA_USERS = """ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY + UNIQUE + NOT NULL, + username TEXT, + team TEXT, + loc TEXT, + passkey TEXT, + reg_ts INTEGER, + lastprint_ts INTEGER DEFAULT (0), + pages INTEGER DEFAULT (0) +); +""" diff --git a/server-tx/app/print/__init__.py b/server-tx/app/print/__init__.py new file mode 100644 index 0000000..756914e --- /dev/null +++ b/server-tx/app/print/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +m_print = Blueprint( + 'print', + __name__, + template_folder='templates' +) + +from . import views \ No newline at end of file diff --git a/server-tx/app/print/static/font.ttf b/server-tx/app/print/static/font.ttf new file mode 100644 index 0000000..b198383 Binary files /dev/null and b/server-tx/app/print/static/font.ttf differ diff --git a/server-tx/app/print/templates/print/print_code.html b/server-tx/app/print/templates/print/print_code.html new file mode 100644 index 0000000..1992250 --- /dev/null +++ b/server-tx/app/print/templates/print/print_code.html @@ -0,0 +1,20 @@ +{% extends "general.html" %} + +{% block title %}Загрузка кода на печать{% endblock %} +{% block header %}Команда: {{ user.team }} | Локация: {{ user.loc }} | Выйти{% endblock %} + +{% block content %} +{% block extra %} +{% endblock %} +
+

Отправка кода на печать

+

Скопируйте код с IDE и вставьте его в поле "Код".

+
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/print/templates/print/print_code_a.html b/server-tx/app/print/templates/print/print_code_a.html new file mode 100644 index 0000000..b8e1b41 --- /dev/null +++ b/server-tx/app/print/templates/print/print_code_a.html @@ -0,0 +1,4 @@ +{% extends "print/print_code_t.html" %} + +{% block title %}Загрузка кода на печать{% endblock %} +{% block header %}Админ: {{ user.username }} | Панель администратора | Выйти{% endblock %} \ No newline at end of file diff --git a/server-tx/app/print/templates/print/print_code_t.html b/server-tx/app/print/templates/print/print_code_t.html new file mode 100644 index 0000000..47db658 --- /dev/null +++ b/server-tx/app/print/templates/print/print_code_t.html @@ -0,0 +1,18 @@ +{% extends "print/print_code.html" %} + +{% block title %}Загрузка кода на печать{% endblock %} +{% block header %}Преподаватель: {{ user.username }} | Выйти{% endblock %} + +{% block extra %} +
+

Отправка файла на печать

+

У вас есть права на печать документов напрямую, загружая .pdf!

+
+ + + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/print/views.py b/server-tx/app/print/views.py new file mode 100644 index 0000000..f5082cc --- /dev/null +++ b/server-tx/app/print/views.py @@ -0,0 +1,175 @@ +from . import m_print + +import requests +import time +from datetime import datetime + +from flask import render_template +from flask import redirect +from flask import request +from flask import session +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from io import BytesIO +from app._tools.decorators import auth_required +from app._tools.database import DataManager +from app._tools.database import SessionManager +from app._tools.database import ConfigManager + + +RENDER_TEMPLATE = { + "$root$" : "print/print_code_a.html", + "$tutor$" : "print/print_code_t.html" +} +pdfmetrics.registerFont(TTFont('webprint', ConfigManager.get_config()['font-path'])) + +def watermark_gen(p : canvas.Canvas, config, user, page) -> None: + REPLACEMENTS = { + "$time$": datetime.now().strftime('%d.%m.%Y %H:%M'), + "$team$": getattr(user, 'team', 'Unknown Team'), + "$loc$": getattr(user, 'loc', 'Unknown Location'), + "$page$": str(page) + } + wm_format = str(config['print']['watermark']) + for key, value in REPLACEMENTS.items(): + wm_format = wm_format.replace(key, value) + p.setFont("webprint", 13) + p.setFillColorRGB(0.5, 0.5, 0.5) + p.rotate(45) + p.drawString(200, 200, wm_format * 10) + p.rotate(-45) + +@m_print.route('/') +@auth_required +def print_code(): + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + render = 'print/print_code.html' + if user.team in RENDER_TEMPLATE: + render = RENDER_TEMPLATE[user.team] + return render_template( + f'{render}', user = user + ) + +@m_print.route('/printcode', methods=['POST']) +@auth_required +def print_proc(): + user_text = request.form.get('code') + lines = user_text.split('\n') + if len(lines) < 4: + return render_template("error.html", error = "Код, в котором так мало строк, можно запомнить и без печати :)") + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + config = ConfigManager.get_config() + timedelta = int(time.time()) - user.lastprint_ts + timedelta = config['print']['limits']['seconds-per-request'] - timedelta + if timedelta > 0: + return render_template("error.html", error = f"Пожалуйста, подождите ещё {timedelta} с. перед повторным запросом!") + DataManager.edit_user(user.id, "lastprint_ts", int(time.time())) + buffer = BytesIO() + proc_pdf = canvas.Canvas(buffer, pagesize = A4) + width, height = A4 + + left_margin = 15 + top_margin = height - 30 + line_height = 15 + y = top_margin + + pages = 1 + watermark_gen(proc_pdf, config, user, pages) + proc_pdf.setFillColorRGB(0, 0, 0, 1) + proc_pdf.setFont("webprint", 9) + for line in lines: + line = line.replace('\r', '') + if y < 30: + pages += 1 + if (pages > config['print']['limits']['pages-per-request']): + return render_template( + "error.html", + error = f"Достигнут лимит на генерацию в {config['print']['limits']['pages-per-request']} страниц за запрос!" + ) + proc_pdf.showPage() + watermark_gen(proc_pdf, config, user, pages) + proc_pdf.setFillColorRGB(0, 0, 0, 1) + proc_pdf.setFont("webprint", 9) + y = top_margin + proc_pdf.drawString(left_margin, y, line) + y -= line_height + + proc_pdf.showPage() + proc_pdf.save() + buffer.seek(0) + proc_pdf = buffer.getvalue() + + try: + DataManager.edit_user( + user.id, + "pages", + user.pages + pages + ) + response = requests.post( + f"http://{config['server']['ip']}:{config['server']['port']}/printfile", + files = { + 'file' : (f"wp_{user.username}_{user.lastprint_ts}.pdf", proc_pdf) + }, + data = { + 'secret-key' : config['server']['secret'] + }, + timeout = 10 + ) + if response.status_code == 200: + return redirect("/") + else: + return render_template( + "error.html", + error = "Ошибка при отправке PDF файла на печать. Обратитесь к Администратору." + ) + except Exception as e: + return render_template( + "error.html", + error = f"Ошибка при подключении к серверу печати: {str(e)}" + ) + +@m_print.route('/printpdf', methods=['POST']) +@auth_required +def print_pdf_proc(): + user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id) + config = ConfigManager.get_config() + if user.team not in RENDER_TEMPLATE: + return redirect("/") + samples = 1 + file = request.files.get('file') + if file.filename == '': + return render_template("error.html", error = "Не был прикреплён файл!") + if 'samples' in request.form: + samples = int(request.form.get('samples')) + if samples not in range(1, 16): + return render_template( + "error.html", + error = "Допустимый диапазон копий составляет 1..15!" + ) + DataManager.edit_user(user.id, "lastprint_ts", int(time.time())) + try: + response = requests.post( + f"http://{config['server']['ip']}:{config['server']['port']}/printfile", + files = { + 'file' : (f"wp_{user.username}_{user.lastprint_ts}.pdf", file) + }, + data = { + 'secret-key' : config['server']['secret'], + 'samples' : samples + }, + timeout = 10 + ) + if response.status_code == 200: + return redirect("/") + else: + return render_template( + "error.html", + error = "Ошибка при отправке PDF файла на печать. Обратитесь к Администратору." + ) + except Exception as e: + return render_template( + "error.html", + error = f"Ошибка при подключении к серверу печати: {str(e)}" + ) diff --git a/server-tx/app/static/style/fav.png b/server-tx/app/static/style/fav.png new file mode 100644 index 0000000..046dee8 Binary files /dev/null and b/server-tx/app/static/style/fav.png differ diff --git a/server-tx/app/static/style/style.css b/server-tx/app/static/style/style.css new file mode 100644 index 0000000..7207637 --- /dev/null +++ b/server-tx/app/static/style/style.css @@ -0,0 +1,171 @@ +:root { + --primary-color: #ff6f00; /* Orange */ + --secondary-color: #0077ff; /* Blue */ + --gradient-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + --text-color: #ffffff; + --bg-color: #121212; /* Dark background */ + --card-bg: #1e1e1e; /* Dark card */ + --input-bg: #2e2e2e; /* Dark input field */ + --input-border: #444444; /* Input border color */ + --shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: var(--bg-color); + color: var(--text-color); +} + +header { + background: var(--gradient-bg); + color: var(--text-color); + padding: 1rem; + text-align: center; + font-size: 1.5rem; + font-weight: bold; + box-shadow: var(--shadow); +} + +.container { + max-width: 800px; + margin: 2rem auto; + padding: 1rem; +} + +.card { + background: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + box-shadow: var(--shadow); + margin-bottom: 1.5rem; +} + +.button { + background: var(--gradient-bg); + color: var(--text-color); + padding: 0.5rem 1rem; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1rem; +} + +.button:hover { + opacity: 0.9; +} + +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + margin-bottom: 0.5rem; +} + +input { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--input-border); + border-radius: 5px; + background-color: var(--input-bg); + color: var(--text-color); +} + +input:focus { + outline: none; + border-color: var(--primary-color); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; + table-layout: fixed; +} + +th, td { + padding: 1rem; + text-align: left; + border: 1px solid #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +th { + background: var(--primary-color); + color: var(--text-color); +} + +td.username { + width: 20%; +} + +td.team { + width: 60%; +} + +td.actions { + width: 20%; + text-align: center; +} + +.file-input-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; +} + +.file-input-label { + display: inline-block; + padding: 0.5rem 1rem; + background: var(--gradient-bg); + color: var(--text-color); + border-radius: 5px; + cursor: pointer; +} + +.file-input { + display: none; +} + +.file-name { + color: var(--text-color); + margin-top: 0.5rem; +} + +a { + color: white; + text-decoration: none; + text-decoration: underline; +} + +textarea { + font-family: monospace; + width: 100%; + background-color: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 5px; + padding: 0.5rem; +} + +.action-buttons { + display: flex; + gap: 0.5rem; +} + +.icon-button { + background: none; + border: none; + color: var(--primary-color); + cursor: pointer; + font-size: 1.2rem; +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/server-tx/app/templates/error.html b/server-tx/app/templates/error.html new file mode 100644 index 0000000..3b5425a --- /dev/null +++ b/server-tx/app/templates/error.html @@ -0,0 +1,9 @@ +{% extends "general.html" %} + +{% block title %}Произошла ошибка{% endblock %} + +{% block content %} +
+

{{ error }}

+
+{% endblock %} \ No newline at end of file diff --git a/server-tx/app/templates/general.html b/server-tx/app/templates/general.html new file mode 100644 index 0000000..abb60ad --- /dev/null +++ b/server-tx/app/templates/general.html @@ -0,0 +1,20 @@ + + + + + + WebPrint | {% block title %}Default Title{% endblock %} + + + + +
+ {% block header %}WebPrint{% endblock %} +
+
+ {% block content %}{% endblock %} +
+ {% block script %} + {% endblock %} + + \ No newline at end of file diff --git a/server-tx/databases/.gitkeep b/server-tx/databases/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server-tx/gunicorn.py b/server-tx/gunicorn.py new file mode 100644 index 0000000..9bb1c7a --- /dev/null +++ b/server-tx/gunicorn.py @@ -0,0 +1,3 @@ +bind = '127.0.0.1:25565' +workers = 2 +timeout = 120 diff --git a/server-tx/requirements.txt b/server-tx/requirements.txt new file mode 100644 index 0000000..e4b742e --- /dev/null +++ b/server-tx/requirements.txt @@ -0,0 +1,18 @@ +bcrypt==4.2.1 +blinker==1.9.0 +certifi==2024.12.14 +chardet==5.2.0 +charset-normalizer==3.4.1 +click==8.1.8 +Flask==3.1.0 +gunicorn==23.0.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +packaging==24.2 +pillow==11.1.0 +reportlab==4.2.5 +requests==2.32.3 +urllib3==2.3.0 +Werkzeug==3.1.3 \ No newline at end of file diff --git a/server-tx/server-tx.json b/server-tx/server-tx.json new file mode 100644 index 0000000..24ce21d --- /dev/null +++ b/server-tx/server-tx.json @@ -0,0 +1,15 @@ +{ + "font-path": "app/print/static/font.ttf", + "server": { + "ip": "127.0.0.1", + "port": 33333, + "secret": "PROJECT_BY_GAZAKBAYEV_AHMET" + }, + "print": { + "watermark": " * КОМАНДА $team$ ЛОК $loc$ ВРЕМ $time$ СТРАНИЦА $page$ * ", + "limits": { + "pages-per-request": 20, + "seconds-per-request": 20 + } + } +} \ No newline at end of file diff --git a/server-tx/wsgi.py b/server-tx/wsgi.py new file mode 100644 index 0000000..036a943 --- /dev/null +++ b/server-tx/wsgi.py @@ -0,0 +1,3 @@ +from app import get_app + +app = get_app() \ No newline at end of file