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 %}
+ {% if cuser.team not in ("$root$", "$tutor$") %}
+
+ {{ cuser.username }} |
+ {{ cuser.lastprint_ts|format_timestamp }} |
+ {{ cuser.pages }} |
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
Настройки принимающего сервера
+
Перед любыми изменениям убедитесь, что вы знаете, что делаете! По любым вопросам обращайтесь к Администратору системы.
+
+
+
+
+{% 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 %}
+
+{% 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