server-tx part : web UI, PDF generator & transfer to RX server

This commit is contained in:
Ахмет Газакбаев 2025-05-15 14:34:08 +03:00
parent d969addd18
commit e1780fad99
32 changed files with 1272 additions and 0 deletions

34
server-tx/app/__init__.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
from flask import Blueprint
m_admin = Blueprint(
'admin',
__name__,
template_folder='templates'
)
from . import views

View File

@ -0,0 +1,101 @@
{% extends "general.html" %}
{% block title %}Панель администратора{% endblock %}
{% block header %}Админ: {{ user.username }} | Панель | <a href="/">На главную</a>{% endblock %}
{% block content %}
<div class="card">
<h2>Список пользователей</h2>
<p><a href = "/admin/create">Тык</a>, чтобы создать пользователя</p>
<table>
<thead>
<tr>
<th class="username">Логин</th>
<th class="team">Команда</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
{% for cuser in users %}
<tr>
<td>{{ cuser.username }}</td>
<td>{{ cuser.team }}</td>
<td class="action-buttons">
{% if cuser.team not in ("$root$") %}
<a href="edit/{{ cuser.id }}" class="icon-button" title="Редактировать">✏️</button>
<a href="remove/{{ cuser.id }}" class="icon-button" title="Удалить!">🗑️</button>
{% else %}
Только по запросу | ID: {{ cuser.id }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>Активность пользователей</h2>
<p>Активность администраторов и преподавательского состава не отображается.</p>
<table>
<thead>
<tr>
<th>Логин</th>
<th>Последняя печать</th>
<th>Количество страниц</th>
</tr>
</thead>
<tbody>
{% for cuser in users %}
{% if cuser.team not in ("$root$", "$tutor$") %}
<tr>
<td>{{ cuser.username }}</td>
<td>{{ cuser.lastprint_ts|format_timestamp }}</td>
<td>{{ cuser.pages }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>Настройки печати</h2>
<br>
<form action="edit/settings" method="POST">
<div class="form-group">
<label for="watermark">Watermark на листах (доступные переменные: $time$, $team$, $page и $loc$):</label>
<input type="text" id="watermark" name="watermark" value="{{ settings['print']['watermark'] }}" required>
</div>
<div class="form-group">
<label for="pages-per-request">Лимит сгенерированных страниц на один запрос</label>
<input type="number" id="pages-per-request" name="pages-per-request" value="{{ settings['print']['limits']['pages-per-request'] }}" required>
</div>
<div class="form-group">
<label for="seconds-per-request">Лимит запросов на пользователя (задержка между запросами в секундах)</label>
<input type="number" id="seconds-per-request" name="seconds-per-request" value="{{ settings['print']['limits']['seconds-per-request'] }}" required>
</div>
<button type="submit" class="button">Сохранить текущие настройки</button>
</form>
</div>
<div class="card">
<h2>Настройки принимающего сервера</h2>
<p><span style = "color: red; font-weight: bold;">Перед любыми изменениям убедитесь, что вы знаете, что делаете!</span> По любым вопросам обращайтесь к Администратору системы.</p>
<p></p>
<br>
<form action="edit/server-rx" method="POST">
<div class="form-group">
<label for="server-ip">Принимающий сервер : IP</label>
<input type="text" id="server-ip" name="server-ip" value="{{ settings['server']['ip'] }}" required>
</div>
<div class="form-group">
<label for="server-port">Принимающий сервер : Port</label>
<input type="number" id="server-port" name="server-port" value="{{ settings['server']['port'] }}" required>
</div>
<div class="form-group">
<label for="server-secret">Принимающий сервер : Секретная фраза</label>
<input type="texts" id="server-secret" name="server-secret" value="{{ settings['server']['secret'] }}" required>
</div>
<button type="submit" class="button">Сохранить и скачать server-rx.json</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "general.html" %}
{% block title %}Создание пользователя{% endblock %}
{% block header %}Создание пользователя{% endblock %}
{% block content %}
<div class="card">
<h2>Создание новой команды</h2>
<form action="account_created" method="POST">
<div class="form-group">
<label for="username">Логин</label>
<input type="text" id="username" name="username" placeholder="Логин, например, ahmet" required>
</div>
<div class="form-group">
<label for="team_name">Команда</label>
<input type="text" id="team_name" name="team_name" placeholder="Короткое название команды, например, Физтех.Бегемотики" required>
</div>
<div class="form-group">
<label for="location">Позиция (стол, секция, пр.)</label>
<input type="text" id="location" name="location" placeholder="Метка, например, '1р1с' (т.е., 1 ряд 1 стол)" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="text" id="password" name="password" required>
</div>
<button type="submit" class="button">Создать пользователя</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "general.html" %}
{% block title %}Создание пользователя{% endblock %}
{% block header %}Создание пользователя{% endblock %}
{% block content %}
<div class="card">
<h2>Создание новой команды</h2>
<form action="account_created_many" method="POST">
<div class="form-group">
<label for="username">Логин</label>
<input type="text" id="username" name="username" placeholder="Логин, например, ahmet" required>
</div>
<div class="form-group">
<label for="team_name">Команда</label>
<input type="text" id="team_name" name="team_name" placeholder="Короткое название команды, например, Физтех.Бегемотики" required>
</div>
<div class="form-group">
<label for="location">Позиция (стол, секция, пр.)</label>
<input type="text" id="location" name="location" placeholder="Метка, например, '1р1с' (т.е., 1 ряд 1 стол)" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="text" id="password" name="password" required>
</div>
<button type="submit" class="button">Создать пользователя</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "general.html" %}
{% block title %}Пользователь создан{% endblock %}
{% block header %}Пользователь создан{% endblock %}
{% block content %}
<div class="card">
<h2>Успешно создан пользователь {{ cuser.username }}!</h2>
<form action="">
<div class="form-group">
<label for="username">Логин</label>
<input type="text" id="username" name="username" value="{{ cuser.username }}" readonly>
</div>
<div class="form-group">
<label for="team_name">Команда</label>
<input type="text" id="team_name" name="team_name" value="{{ cuser.team }}" readonly>
</div>
<div class="form-group">
<label for="location">Позиция (стол, секция, пр.)</label>
<input type="text" id="location" name="locat ion" value="{{ cuser.loc }}" readonly>
</div>
<div class="form-group">
<label for="password">Пароль (отображается один раз, на данной странице)</label>
<input type="text" id="password" name="password" value="{{ password_onetime }}" readonly>
</div>
Данную страницу можно отправить новому пользователю платформы!
</form>
</div>
<div class="card">
<a href="../admin">Вернуться обратно</a>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "general.html" %}
{% block title %}Редактирование пользователя{% endblock %}
{% block header %}Редактирование {{ edituser.username }}{% endblock %}
{% block content %}
<div class="card">
<h2>Редактирование команды</h2>
<form action="proc/{{ edituser.id }}" method="POST">
<div class="form-group">
<label for="username">Логин (не изменяем)</label>
<input type="text" id="username" name="username" value="{{ edituser.username }}" readonly>
</div>
<div class="form-group">
<label for="team_name">Команда</label>
<input type="text" id="team_name" name="team_name" value="{{ edituser.team }}" required>
</div>
<div class="form-group">
<label for="location">Позиция (стол, секция, пр.)</label>
<input type="text" id="location" name="location" value="{{ edituser.loc }}" required>
</div>
<div class="form-group" id="new_password">
<label for="new_password">Новый пароль (должен превышать 3 символа, если хотите изменить, иначе - пустое)</label>
<input type="text" id="new_password" name="new_password">
</div>
<button type="submit" class="button">Сохранить изменения</button>
</form>
</div>
{% endblock %}

View File

@ -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/<id>')
@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/<id>', 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/<id>')
@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")

View File

@ -0,0 +1,9 @@
from flask import Blueprint
m_auth = Blueprint(
'auth',
__name__,
template_folder='templates'
)
from . import views

View File

@ -0,0 +1,21 @@
{% extends "general.html" %}
{% block title %}Авторизация{% endblock %}
{% block header %}Авторизация на платформе{% endblock %}
{% block content %}
<div class="card">
<h2>Авторизация</h2>
<form action="session/login" method="POST">
<div class="form-group">
<label for="username">Логин</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="button">Авторизация</button>
</form>
</div>
{% endblock %}

View File

@ -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("/")

42
server-tx/app/config.py Normal file
View File

@ -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)
);
"""

View File

@ -0,0 +1,9 @@
from flask import Blueprint
m_print = Blueprint(
'print',
__name__,
template_folder='templates'
)
from . import views

Binary file not shown.

View File

@ -0,0 +1,20 @@
{% extends "general.html" %}
{% block title %}Загрузка кода на печать{% endblock %}
{% block header %}Команда: {{ user.team }} | Локация: {{ user.loc }} | <a href="/auth/logout">Выйти</a>{% endblock %}
{% block content %}
{% block extra %}
{% endblock %}
<div class="card">
<h2>Отправка кода на печать</h2>
<p>Скопируйте код с IDE и вставьте его в поле "Код".</p>
<form action="/printcode" method="POST">
<div class="form-group">
<label for="code">Код</label>
<textarea id="code" name="code" rows="25"></textarea>
</div>
<button type="submit" class="button">Узреть свой код на бересте!</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends "print/print_code_t.html" %}
{% block title %}Загрузка кода на печать{% endblock %}
{% block header %}Админ: {{ user.username }} | <a href="/admin">Панель администратора</a> | <a href="/auth/logout">Выйти</a>{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "print/print_code.html" %}
{% block title %}Загрузка кода на печать{% endblock %}
{% block header %}Преподаватель: {{ user.username }} | <a href="/auth/logout">Выйти</a>{% endblock %}
{% block extra %}
<div class="card">
<h2>Отправка файла на печать</h2>
<p>У вас есть права на печать документов напрямую, загружая .pdf!</p>
<form class="file-upload" action="/printpdf" method="POST" enctype="multipart/form-data">
<label for="file">Загрузка файла .PDF</label>
<input type="file" id="file" name="file" accept=".pdf">
<label style="margin-top: 10px;" for="samples">Количество копий (допустимый максимум - 15, можно повторять запросы)</label>
<input type="number" id="samples" name="samples" value="1">
<button style="margin-top: 10px;" type="submit" class="button">Распечатать</button>
</form>
</div>
{% endblock %}

View File

@ -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)}"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
{% extends "general.html" %}
{% block title %}Произошла ошибка{% endblock %}
{% block content %}
<div class="card">
<h2 style="color: darkred;">{{ error }}</h2>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebPrint | {% block title %}Default Title{% endblock %}</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='style/fav.png') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head>
<body>
<header>
{% block header %}WebPrint{% endblock %}
</header>
<div class="container">
{% block content %}{% endblock %}
</div>
{% block script %}
{% endblock %}
</body>
</html>

View File

3
server-tx/gunicorn.py Normal file
View File

@ -0,0 +1,3 @@
bind = '127.0.0.1:25565'
workers = 2
timeout = 120

View File

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

15
server-tx/server-tx.json Normal file
View File

@ -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
}
}
}

3
server-tx/wsgi.py Normal file
View File

@ -0,0 +1,3 @@
from app import get_app
app = get_app()