Merge pull request 'Project is completed.' (#3) from final-system into main
Reviewed-on: #3
This commit is contained in:
commit
19135364ff
104
README.md
104
README.md
@ -0,0 +1,104 @@
|
|||||||
|
# ICPC-WEBPRINT
|
||||||
|
|
||||||
|
This platform helps contest participants to print their code using no drivers, only web application in any browser. Supports authorization, printing PDF for administrators and teachers.
|
||||||
|
|
||||||
|
Platform was already used on some events, like "The FPMI Cup : February, 2025" and "MIPT Open Programming Championship : April, 2025".
|
||||||
|
|
||||||
|
## Instalation
|
||||||
|
### Environment and Service
|
||||||
|
Both RX and TX servers have the same way to install dependencies. First, be sure to have venv globally installed on your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt install python3-venv
|
||||||
|
# or
|
||||||
|
python3 -m pip install venv
|
||||||
|
```
|
||||||
|
|
||||||
|
After that create virtual environment:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd print-tx # and then print-rx
|
||||||
|
python3 -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter environment and install dependencies from `requirements.txt`:
|
||||||
|
|
||||||
|
```
|
||||||
|
source .venv/bin/activate
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
Great! Now you have working system. You can start your project either running
|
||||||
|
```
|
||||||
|
python3 -m gunicorn wsgi_back:app -c gunicorn.py
|
||||||
|
```
|
||||||
|
in venv, or creating services on your Linux. Template could be found in `example.serivce` file. After that you can start your web app with running:
|
||||||
|
```
|
||||||
|
systemctl start example.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Secrets
|
||||||
|
|
||||||
|
Change secret code, that is used to access RX server from TX in `server-tx.json` and `server-rx.json`.
|
||||||
|
Change salt and session secret key in `server-tx/app/config.py`:
|
||||||
|
```
|
||||||
|
_SALT = '$2b$12$DE09JKsqy6Ii/zAxEAA5n.' #edit
|
||||||
|
_SECRET_SESSION_KEY = "GAZAKBAYEV-AHMET-PROJECT" #edit
|
||||||
|
```
|
||||||
|
You can also to set super admin user (it can add and remove others):
|
||||||
|
```
|
||||||
|
_SUPER_ADMINS = ( #edit
|
||||||
|
"god"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accounts with login:password like "god:god" will be checked and created if not exist, when application start.
|
||||||
|
|
||||||
|
### Setting ports
|
||||||
|
|
||||||
|
You can set port of TX part in file `gunicorn.py`, and RX part in config file `server-rx.json`. Using TX interface, in admin panel, you can configure RX server and get `server-rx.json` to load it.
|
||||||
|
|
||||||
|
### Setting default printer
|
||||||
|
|
||||||
|
Learn more about `lp` service in Linux ([Wiki](https://ru.wikipedia.org/wiki/Lp)).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Four groups: Super Admin, Admin (create and remove users, set configuration), Tutor (print PDF and code), Team Member (default, print only code)
|
||||||
|
- Print code & PDF
|
||||||
|
- Create & Delete users
|
||||||
|
- Statistic of printed pages & last printing time of member
|
||||||
|
- Setting RX server using UI
|
||||||
|
- Setting TX configuration: PDF Watermark and limits.
|
||||||
|
|
||||||
|
## Setting user group
|
||||||
|
|
||||||
|
Groups are just team names! Use `$root` for Admins, `$tutor$` for Teachers and other names for members.
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
### Team Member
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Tutor
|
||||||
|

|
||||||
|
|
||||||
|
### Админ Панель
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Создание и Редактирование пользователя
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Просмотреть данную галерею можно на [NextCloud](https://cloud.gazakbayev.net/index.php/s/gqzmo9S8KFGcQLi?dir=/) автора.
|
20
example.service
Normal file
20
example.service
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# NOTICE! Replace PATH_TO_SCRIPT with required directory!
|
||||||
|
# Paste content of this file into /etc/systemd/system/YOUR_NAME.service
|
||||||
|
#
|
||||||
|
|
||||||
|
# Suggested names for service: print-rx.service & print-tx.server
|
||||||
|
# REMEMBER to create virtual environment before using script (read README.md)
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Print Server TX/RX server script.
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=PATH_TO_SCRIPT
|
||||||
|
ExecStart=PATH_TO_SCRIPT/.venv/bin/gunicorn wsgi_back:app -c gunicorn.py
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
0
server-rx/files/.gitkeep
Normal file
0
server-rx/files/.gitkeep
Normal file
8
server-rx/gunicorn.py
Normal file
8
server-rx/gunicorn.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
with open("server-rx.json", 'r', encoding = "utf-8") as file:
|
||||||
|
server_data = json.load(file)
|
||||||
|
|
||||||
|
bind = f"{server_data['ip']}:{server_data['port']}"
|
||||||
|
workers = 2
|
||||||
|
timeout = 120
|
9
server-rx/requirements.txt
Normal file
9
server-rx/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
blinker==1.9.0
|
||||||
|
click==8.1.8
|
||||||
|
Flask==3.1.0
|
||||||
|
gunicorn==23.0.0
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.5
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
packaging==24.2
|
||||||
|
Werkzeug==3.1.3
|
5
server-rx/server-rx.json
Normal file
5
server-rx/server-rx.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": 33333,
|
||||||
|
"secret": "PROJECT_BY_GAZAKBAYEV_AHMET"
|
||||||
|
}
|
37
server-rx/wsgi_back.py
Normal file
37
server-rx/wsgi_back.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
SECRET = None
|
||||||
|
|
||||||
|
with open("server-rx.json", 'r', encoding = "utf-8") as file:
|
||||||
|
server_data = json.load(file)
|
||||||
|
SECRET = server_data['secret']
|
||||||
|
|
||||||
|
def go_to_print(file_path, samples):
|
||||||
|
os.system(f"lp -n {samples} {file_path}")
|
||||||
|
|
||||||
|
@app.route('/printfile', methods=['POST'])
|
||||||
|
def print_file():
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return 'Файл не был предоставлен.', 400
|
||||||
|
if 'secret-key' not in request.form:
|
||||||
|
return 'Секретный ключ не был предоставлен.', 400
|
||||||
|
if request.form['secret-key'] != SECRET:
|
||||||
|
return 'Авторизация не удалась.', 400
|
||||||
|
file = request.files['file']
|
||||||
|
filename = "files/" + file.filename
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(file.read())
|
||||||
|
samples = 1
|
||||||
|
if b'samples' in request.data:
|
||||||
|
samples = request.data[b'samples']
|
||||||
|
try:
|
||||||
|
go_to_print(filename, samples)
|
||||||
|
return 'Файл отправлен на печать.', 200
|
||||||
|
except Exception as e:
|
||||||
|
return f'Ошибка при печати файла: {str(e)}.', 500
|
||||||
|
finally:
|
||||||
|
return 'OK', 200
|
36
server-tx/app/__init__.py
Normal file
36
server-tx/app/__init__.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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()
|
||||||
|
DataManager.check_admins()
|
||||||
|
SessionManager.init_table()
|
||||||
|
|
||||||
|
|
||||||
|
return app
|
168
server-tx/app/_tools/database.py
Normal file
168
server-tx/app/_tools/database.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import time
|
||||||
|
import sqlite3 as sql
|
||||||
|
import json
|
||||||
|
|
||||||
|
from app._tools.models.user import User
|
||||||
|
from app._tools.models.session import Session
|
||||||
|
from app._tools import passhash as Hash
|
||||||
|
|
||||||
|
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
|
||||||
|
from app.config import _SUPER_ADMINS as SUPER_ADMINS
|
||||||
|
from app.config import _SALT as SALT
|
||||||
|
|
||||||
|
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 check_admins():
|
||||||
|
for k in SUPER_ADMINS:
|
||||||
|
if DataManager.get_user_by_name(k) == None:
|
||||||
|
DataManager.add_user(
|
||||||
|
k,
|
||||||
|
"$root$",
|
||||||
|
"Super Admin",
|
||||||
|
Hash.hashPassword(k, SALT)
|
||||||
|
)
|
||||||
|
print(f"Create super admin {k} with password {k}")
|
||||||
|
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 = "GAZAKBAYEV-AHMET-PROJECT" #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