server-tx part : web UI, PDF generator & transfer to RX server
This commit is contained in:
parent
d969addd18
commit
e1780fad99
34
server-tx/app/__init__.py
Normal file
34
server-tx/app/__init__.py
Normal 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
|
155
server-tx/app/_tools/database.py
Normal file
155
server-tx/app/_tools/database.py
Normal 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)
|
31
server-tx/app/_tools/decorators.py
Normal file
31
server-tx/app/_tools/decorators.py
Normal 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
|
10
server-tx/app/_tools/models/session.py
Normal file
10
server-tx/app/_tools/models/session.py
Normal 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
|
20
server-tx/app/_tools/models/user.py
Normal file
20
server-tx/app/_tools/models/user.py
Normal 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
|
27
server-tx/app/_tools/passhash.py
Normal file
27
server-tx/app/_tools/passhash.py
Normal 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)
|
9
server-tx/app/admin/__init__.py
Normal file
9
server-tx/app/admin/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from flask import Blueprint
|
||||
|
||||
m_admin = Blueprint(
|
||||
'admin',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
from . import views
|
101
server-tx/app/admin/templates/admin/admin_main.html
Normal file
101
server-tx/app/admin/templates/admin/admin_main.html
Normal 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 %}
|
29
server-tx/app/admin/templates/admin/user_create.html
Normal file
29
server-tx/app/admin/templates/admin/user_create.html
Normal 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 %}
|
29
server-tx/app/admin/templates/admin/user_create_many.html
Normal file
29
server-tx/app/admin/templates/admin/user_create_many.html
Normal 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 %}
|
33
server-tx/app/admin/templates/admin/user_created.html
Normal file
33
server-tx/app/admin/templates/admin/user_created.html
Normal 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 %}
|
29
server-tx/app/admin/templates/admin/user_edit.html
Normal file
29
server-tx/app/admin/templates/admin/user_edit.html
Normal 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 %}
|
187
server-tx/app/admin/views.py
Normal file
187
server-tx/app/admin/views.py
Normal 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")
|
9
server-tx/app/auth/__init__.py
Normal file
9
server-tx/app/auth/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from flask import Blueprint
|
||||
|
||||
m_auth = Blueprint(
|
||||
'auth',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
from . import views
|
21
server-tx/app/auth/templates/auth/login_form.html
Normal file
21
server-tx/app/auth/templates/auth/login_form.html
Normal 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 %}
|
41
server-tx/app/auth/views.py
Normal file
41
server-tx/app/auth/views.py
Normal 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
42
server-tx/app/config.py
Normal 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)
|
||||
);
|
||||
"""
|
9
server-tx/app/print/__init__.py
Normal file
9
server-tx/app/print/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from flask import Blueprint
|
||||
|
||||
m_print = Blueprint(
|
||||
'print',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
from . import views
|
BIN
server-tx/app/print/static/font.ttf
Normal file
BIN
server-tx/app/print/static/font.ttf
Normal file
Binary file not shown.
20
server-tx/app/print/templates/print/print_code.html
Normal file
20
server-tx/app/print/templates/print/print_code.html
Normal 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 %}
|
4
server-tx/app/print/templates/print/print_code_a.html
Normal file
4
server-tx/app/print/templates/print/print_code_a.html
Normal 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 %}
|
18
server-tx/app/print/templates/print/print_code_t.html
Normal file
18
server-tx/app/print/templates/print/print_code_t.html
Normal 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 %}
|
175
server-tx/app/print/views.py
Normal file
175
server-tx/app/print/views.py
Normal 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)}"
|
||||
)
|
BIN
server-tx/app/static/style/fav.png
Normal file
BIN
server-tx/app/static/style/fav.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
171
server-tx/app/static/style/style.css
Normal file
171
server-tx/app/static/style/style.css
Normal 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;
|
||||
}
|
9
server-tx/app/templates/error.html
Normal file
9
server-tx/app/templates/error.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "general.html" %}
|
||||
|
||||
{% block title %}Произошла ошибка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2 style="color: darkred;">{{ error }}</h2>
|
||||
</div>
|
||||
{% endblock %}
|
20
server-tx/app/templates/general.html
Normal file
20
server-tx/app/templates/general.html
Normal 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>
|
0
server-tx/databases/.gitkeep
Normal file
0
server-tx/databases/.gitkeep
Normal file
3
server-tx/gunicorn.py
Normal file
3
server-tx/gunicorn.py
Normal file
@ -0,0 +1,3 @@
|
||||
bind = '127.0.0.1:25565'
|
||||
workers = 2
|
||||
timeout = 120
|
18
server-tx/requirements.txt
Normal file
18
server-tx/requirements.txt
Normal 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
15
server-tx/server-tx.json
Normal 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
3
server-tx/wsgi.py
Normal file
@ -0,0 +1,3 @@
|
||||
from app import get_app
|
||||
|
||||
app = get_app()
|
Loading…
x
Reference in New Issue
Block a user