Update for public https://habr.com/post/421887/
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,8 +5,5 @@
|
|||||||
|
|
||||||
# Каталог с данными
|
# Каталог с данными
|
||||||
**/data/
|
**/data/
|
||||||
**/config/
|
/config
|
||||||
**/logs/
|
**/logs/
|
||||||
|
|
||||||
Python/graph.png
|
|
||||||
*.rrd
|
|
||||||
|
|||||||
31
README.html
31
README.html
@@ -1,3 +1,34 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="./myapp/static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="./myapp/static/css/font-awesome.min.css" />
|
||||||
|
<script type="text/javascript" src="./myapp/static/js/jquery-3.3.1.slim.min.js"></script>
|
||||||
|
<script type="text/javascript" src="./myapp/static/js/popper.min.js"></script>
|
||||||
|
<script type="text/javascript" src="./myapp/static/js/bootstrap.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
<h3>Описание</h3>
|
<h3>Описание</h3>
|
||||||
|
|
||||||
<p>Каркас веб-приложения на Flask</p>
|
<p>Каркас веб-приложения на Flask</p>
|
||||||
|
|
||||||
|
<p>Структура каталогов для проекта (в частности на Flask) <a href="https://habr.com/post/421887/">https://habr.com/post/421887/</a></p>
|
||||||
|
|
||||||
|
<h3>Утилиты</h3>
|
||||||
|
|
||||||
|
<p>Добавление пользователя</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
./utils/useradd.py --user admin --password admin
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<p>Запуск из консоли</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
./run.py
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
9
deploy/config/__init__.py
Normal file
9
deploy/config/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
CONFIG = 'config.demo'
|
||||||
33
deploy/config/demo.py
Normal file
33
deploy/config/demo.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
DEBUG = True
|
||||||
|
SQLDEBUG = False
|
||||||
|
|
||||||
|
SESSION_COOKIE_NAME = 'myapp'
|
||||||
|
SESSION_TYPE = 'filesystem'
|
||||||
|
|
||||||
|
TITLE = 'Проект'
|
||||||
|
|
||||||
|
DIR_BASE = '/'.join(os.path.dirname(os.path.abspath(__file__)).split('/')[:-1])
|
||||||
|
DIR_DATA = DIR_BASE + '/data'
|
||||||
|
DIR_FILES = DIR_DATA + '/files'
|
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DIR_DATA + '/db.sqlite3'
|
||||||
|
# Генерировать можно утилитой pwgen
|
||||||
|
# Пример:
|
||||||
|
# pwgen -sy 64
|
||||||
|
SECRET_KEY = '''0123456789'''
|
||||||
|
|
||||||
|
# Логирование
|
||||||
|
LOG_FILE = DIR_DATA + '/myapp.log'
|
||||||
|
LONG_LOG_FORMAT = '%(asctime)s - [%(name)s.%(levelname)s] [%(threadName)s, %(module)s.%(funcName)s@%(lineno)d] %(message)s'
|
||||||
|
LOG_FILE_SIZE = 128 # Размер файла лога в МБ
|
||||||
10
deploy/nginx/myapp.conf
Normal file
10
deploy/nginx/myapp.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name myapp www.myapp;
|
||||||
|
access_log /var/log/nginx/myapp-access.log mainproxy;
|
||||||
|
error_log /var/log/nginx/myapp-error.log info;
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:///var/run/uwsgi/myapp.sock;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ flask
|
|||||||
sqlalchemy
|
sqlalchemy
|
||||||
sqlalchemy-utils
|
sqlalchemy-utils
|
||||||
alembic
|
alembic
|
||||||
|
celery
|
||||||
8
deploy/uwsgi/myapp.ini
Normal file
8
deploy/uwsgi/myapp.ini
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[uwsgi]
|
||||||
|
chdir = /home/user/myapp
|
||||||
|
logto = /home/user/myapp/logs/myapp.log
|
||||||
|
wsgi-file = /home/user/myapp/wsgi.py
|
||||||
|
py-autoreload = 10
|
||||||
|
|
||||||
|
uid = user
|
||||||
|
gid = user
|
||||||
@@ -27,13 +27,10 @@ formatter = logging.Formatter(app.config['LONG_LOG_FORMAT'])
|
|||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
app.logger.addHandler(handler)
|
app.logger.addHandler(handler)
|
||||||
|
|
||||||
# celery
|
# API
|
||||||
from celery import Celery
|
from . import ns_api
|
||||||
|
|
||||||
celery = Celery(app.name,
|
# Пользователи
|
||||||
broker=app.config['CELERY_BROKER_URL'],
|
from . import ns_user
|
||||||
backend=app.config['CELERY_RESULT_BACKEND'],
|
|
||||||
include=['myapp.tasks'])
|
|
||||||
celery.conf.update(app.config)
|
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|||||||
14
myapp/lib/__init__.py
Normal file
14
myapp/lib/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
from .pagination import Pagination, getpage
|
||||||
|
from .passwd import pwgen, get_hash_password
|
||||||
|
from .storage import gettree, gethashtree
|
||||||
|
from .info import get_user, get_ip
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
37
myapp/lib/info.py
Normal file
37
myapp/lib/info.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
from flask import session, request
|
||||||
|
from .. import app, models
|
||||||
|
|
||||||
|
def get_user():
|
||||||
|
"""Получить текущего пользователя"""
|
||||||
|
result = None
|
||||||
|
if 'user_id' in session:
|
||||||
|
result = models.db_session.query(
|
||||||
|
models.User
|
||||||
|
).filter(
|
||||||
|
models.User.id==session['user_id']
|
||||||
|
).first()
|
||||||
|
if result is None:
|
||||||
|
session.pop('logged_in', None)
|
||||||
|
session.pop('user_id', None)
|
||||||
|
return None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_ip():
|
||||||
|
"""
|
||||||
|
Получить IP
|
||||||
|
"""
|
||||||
|
result = ''
|
||||||
|
if request.headers.getlist("X-Forwarded-For"):
|
||||||
|
result = request.headers.get("X-Forwarded-For").split(",")[0]
|
||||||
|
else:
|
||||||
|
result = request.remote_addr
|
||||||
|
return result
|
||||||
56
myapp/lib/pagination.py
Normal file
56
myapp/lib/pagination.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
|
||||||
|
def getpage(query, page=1, page_size=10):
|
||||||
|
"""
|
||||||
|
Постраничный вывод
|
||||||
|
"""
|
||||||
|
if page_size:
|
||||||
|
query = query.limit(page_size)
|
||||||
|
if page:
|
||||||
|
query = query.offset((page-1)*page_size)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
class Pagination(object):
|
||||||
|
"""
|
||||||
|
Пагинация
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, page, per_page, total_count):
|
||||||
|
self.page = page
|
||||||
|
self.per_page = per_page
|
||||||
|
self.total_count = total_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages(self):
|
||||||
|
return int(ceil(self.total_count / float(self.per_page)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_prev(self):
|
||||||
|
return self.page > 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_next(self):
|
||||||
|
return self.page < self.pages
|
||||||
|
|
||||||
|
def iter_pages(self, left_edge=2, left_current=2,
|
||||||
|
right_current=5, right_edge=2):
|
||||||
|
last = 0
|
||||||
|
for num in range(1, self.pages + 1):
|
||||||
|
if num <= left_edge or \
|
||||||
|
(num > self.page - left_current - 1 and \
|
||||||
|
num < self.page + right_current) or \
|
||||||
|
num > self.pages - right_edge:
|
||||||
|
if last + 1 != num:
|
||||||
|
yield None
|
||||||
|
yield num
|
||||||
|
last = num
|
||||||
37
myapp/lib/passwd.py
Normal file
37
myapp/lib/passwd.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .. import app
|
||||||
|
|
||||||
|
def pwgen(length=15, hex=False):
|
||||||
|
"""
|
||||||
|
Генератор пароля
|
||||||
|
"""
|
||||||
|
keylist='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
if hex:
|
||||||
|
keylist='0123456789ABCDEF'
|
||||||
|
password=[]
|
||||||
|
|
||||||
|
while len(password) < length:
|
||||||
|
a_char = random.choice(keylist)
|
||||||
|
password.append(a_char)
|
||||||
|
return ''.join(password)
|
||||||
|
|
||||||
|
def get_hash_password(password, salt = None):
|
||||||
|
"""
|
||||||
|
Получить хеш пароля SHA-512
|
||||||
|
"""
|
||||||
|
if salt == None:
|
||||||
|
salt = uuid.uuid4().hex
|
||||||
|
text = password.encode('utf-8') + salt.encode('utf-8')
|
||||||
|
h = hashlib.sha512()
|
||||||
|
h.update(text)
|
||||||
|
return str(h.hexdigest())
|
||||||
29
myapp/lib/storage.py
Normal file
29
myapp/lib/storage.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
|
||||||
|
def gettree(number, count=3):
|
||||||
|
"""
|
||||||
|
Сформировать дерево каталогов
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
newline = str(number)
|
||||||
|
while len(newline) % count:
|
||||||
|
newline = '0' + newline
|
||||||
|
for i in range(0, len(newline)//count):
|
||||||
|
result.append(newline[i*count:i*count+count])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gethashtree(hash, count=3):
|
||||||
|
"""
|
||||||
|
Сформировать дерево каталогов
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for i in range(0, count):
|
||||||
|
element = hash[2*i:2*(i+1)]
|
||||||
|
result.append(element)
|
||||||
|
return '/'.join(result)
|
||||||
@@ -5,6 +5,7 @@ __author__ = 'RemiZOffAlex'
|
|||||||
__copyright__ = '(c) RemiZOffAlex'
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__email__ = 'remizoffalex@mail.ru'
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
@@ -26,21 +27,6 @@ Base.query = db_session.query_property()
|
|||||||
# Пользователи
|
# Пользователи
|
||||||
from .users import User
|
from .users import User
|
||||||
|
|
||||||
# IP
|
|
||||||
from .ip import IP
|
|
||||||
|
|
||||||
# ACL
|
|
||||||
from .acl import (
|
|
||||||
ObjectPermission,
|
|
||||||
RolePermission,
|
|
||||||
RoleSetPermission,
|
|
||||||
UserPermission,
|
|
||||||
UserRole,
|
|
||||||
IPPermission
|
|
||||||
)
|
|
||||||
|
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = []
|
||||||
'db_session'
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: UTF-8 -*-
|
|
||||||
|
|
||||||
__author__ = 'RemiZOffAlex'
|
|
||||||
__copyright__ = '(c) RemiZOffAlex'
|
|
||||||
__license__ = 'MIT'
|
|
||||||
__email__ = 'remizoffalex@mail.ru'
|
|
||||||
__url__ = 'http://remizoffalex.ru'
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from sqlalchemy import (
|
|
||||||
Table,
|
|
||||||
Column,
|
|
||||||
Boolean,
|
|
||||||
Integer,
|
|
||||||
ForeignKey,
|
|
||||||
String,
|
|
||||||
DateTime,
|
|
||||||
Enum
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from . import Base
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermission(Base):
|
|
||||||
"""
|
|
||||||
Объекты доступа: процедура и содержащий процедуру модуль
|
|
||||||
"""
|
|
||||||
__tablename__ = "object_permission"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
funcname = Column(String)
|
|
||||||
modulename = Column(String)
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
user_permissions = relationship(
|
|
||||||
"UserPermission",
|
|
||||||
primaryjoin="ObjectPermission.id==UserPermission.object_id"
|
|
||||||
)
|
|
||||||
ip_permissions = relationship(
|
|
||||||
"IPPermission",
|
|
||||||
primaryjoin="ObjectPermission.id==IPPermission.object_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, modulename, funcname):
|
|
||||||
self.funcname = funcname
|
|
||||||
self.modulename = modulename
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<ObjectPermission('%s': '%s')>" % (self.funcname,
|
|
||||||
self.modulename)
|
|
||||||
|
|
||||||
|
|
||||||
class RolePermission(Base):
|
|
||||||
"""Роли доступа"""
|
|
||||||
__tablename__ = "role_permission"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String)
|
|
||||||
description = Column(String, default='')
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
set_objects = relationship(
|
|
||||||
"RoleSetPermission",
|
|
||||||
primaryjoin="RolePermission.id==RoleSetPermission.role_id"
|
|
||||||
)
|
|
||||||
users = relationship(
|
|
||||||
"UserRole",
|
|
||||||
primaryjoin="RolePermission.id==UserRole.role_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class RoleSetPermission(Base):
|
|
||||||
"""Набор прав доступа для роли"""
|
|
||||||
__tablename__ = "role_set_permission"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
role_id = Column(Integer, ForeignKey('role_permission.id'))
|
|
||||||
object_id = Column(Integer, ForeignKey('object_permission.id'))
|
|
||||||
permission = Column(Enum('allow', 'deny')) # Разрешение
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
role = relationship(
|
|
||||||
"RolePermission",
|
|
||||||
primaryjoin="RoleSetPermission.role_id==RolePermission.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
object_permission = relationship(
|
|
||||||
"ObjectPermission",
|
|
||||||
primaryjoin="RoleSetPermission.object_id==ObjectPermission.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, role_permission, object_permission, permission):
|
|
||||||
self.role_id = role_permission.id
|
|
||||||
self.object_id = object_permission.id
|
|
||||||
self.permission = permission
|
|
||||||
|
|
||||||
|
|
||||||
class UserPermission(Base):
|
|
||||||
"""Права доступа пользователя"""
|
|
||||||
__tablename__ = "user_permission"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
object_id = Column(Integer, ForeignKey('object_permission.id'))
|
|
||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
|
||||||
permission = Column(Enum('allow', 'deny')) # Разрешение
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
user = relationship(
|
|
||||||
"User",
|
|
||||||
primaryjoin="UserPermission.user_id==User.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
object_permission = relationship(
|
|
||||||
"ObjectPermission",
|
|
||||||
primaryjoin="UserPermission.object_id==ObjectPermission.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, object_permission, user, permission):
|
|
||||||
assert type(object_permission).__name__=='ObjectPermission', app.logger.info('Не передан объект ObjectPermission')
|
|
||||||
assert type(user).__name__=='User', app.logger.info('Не передан объект User')
|
|
||||||
self.object_id = object_permission.id
|
|
||||||
self.user_id = user.id
|
|
||||||
self.permission = permission
|
|
||||||
|
|
||||||
|
|
||||||
class UserRole(Base):
|
|
||||||
"""Роль пользователя"""
|
|
||||||
__tablename__ = "user_role"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
role_id = Column(Integer, ForeignKey('role_permission.id'))
|
|
||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
user = relationship(
|
|
||||||
"User",
|
|
||||||
primaryjoin="UserRole.user_id==User.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
role_permission = relationship(
|
|
||||||
"RolePermission",
|
|
||||||
primaryjoin="UserRole.role_id==RolePermission.id",
|
|
||||||
uselist=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, role_permission, user):
|
|
||||||
assert type(role_permission).__name__=='RolePermission', app.logger.info('Не передан объект RolePermission')
|
|
||||||
assert type(user).__name__=='User', app.logger.info('Не передан объект User')
|
|
||||||
self.role_id = role_permission.id
|
|
||||||
self.user_id = user.id
|
|
||||||
|
|
||||||
|
|
||||||
class IPPermission(Base):
|
|
||||||
"""
|
|
||||||
Права доступа для IP
|
|
||||||
"""
|
|
||||||
__tablename__ = "ip_permission"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
object_id = Column(Integer, ForeignKey('object_permission.id'))
|
|
||||||
ip_id = Column(Integer, ForeignKey('ip.id'))
|
|
||||||
permission = Column(Enum('allow', 'deny')) # Разрешение
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
ip = relationship("IP", primaryjoin="IPPermission.ip_id==IP.id", uselist=False)
|
|
||||||
object_permission = relationship("ObjectPermission", primaryjoin="IPPermission.object_id==ObjectPermission.id", uselist=False)
|
|
||||||
|
|
||||||
def __init__(self, object_permission, ip, permission):
|
|
||||||
assert type(object_permission).__name__=='ObjectPermission', app.logger.info('Не передан объект ObjectPermission')
|
|
||||||
assert type(ip).__name__=='IP', app.logger.info('Не передан объект IP')
|
|
||||||
self.object_id = object_permission.id
|
|
||||||
self.ip_id = ip.id
|
|
||||||
self.permission = permission
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: UTF-8 -*-
|
|
||||||
|
|
||||||
__author__ = 'RemiZOffAlex'
|
|
||||||
__copyright__ = '(c) RemiZOffAlex'
|
|
||||||
__license__ = 'MIT'
|
|
||||||
__email__ = 'remizoffalex@mail.ru'
|
|
||||||
__url__ = 'http://remizoffalex.ru'
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from . import Base
|
|
||||||
|
|
||||||
class IP(Base):
|
|
||||||
__tablename__ = "ip"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
ip = Column(String, nullable=False, unique=True)
|
|
||||||
description = Column(String)
|
|
||||||
|
|
||||||
# Связи
|
|
||||||
# tagquestion = relationship("TagQuestion", primaryjoin="TagQuestion.tag_id==Tag.id")
|
|
||||||
|
|
||||||
def __init__(self, ip):
|
|
||||||
self.ip = ip
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<IP('%s')>" % (self.ip)
|
|
||||||
@@ -5,21 +5,37 @@ __author__ = 'RemiZOffAlex'
|
|||||||
__copyright__ = '(c) RemiZOffAlex'
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__email__ = 'remizoffalex@mail.ru'
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime
|
from sqlalchemy import (
|
||||||
|
Table,
|
||||||
|
Column,
|
||||||
|
Boolean,
|
||||||
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
|
String,
|
||||||
|
DateTime
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from . import Base
|
from . import Base
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String, nullable=False, unique=True)
|
name = Column(String, nullable=False, unique=True)
|
||||||
password = Column(String, nullable=False)
|
password = Column(String, nullable=False)
|
||||||
|
disabled = Column(Boolean, default=True)
|
||||||
created = Column(DateTime)
|
created = Column(DateTime)
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.created = datetime.datetime.utcnow()
|
self.created = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {c.name: getattr(self, c.name)
|
||||||
|
for c in self.__table__.columns
|
||||||
|
if c.name!='password'}
|
||||||
|
|||||||
12
myapp/ns_api/README.html
Normal file
12
myapp/ns_api/README.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<a href="https://github.com/cenobites/flask-jsonrpc">https://github.com/cenobites/flask-jsonrpc</a>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
curl -i -X POST \
|
||||||
|
-H "Content-Type: application/json; indent=4" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "users.getList",
|
||||||
|
"params": {},
|
||||||
|
"id": "1"
|
||||||
|
}' http://localhost:8000/api
|
||||||
|
</pre>
|
||||||
16
myapp/ns_api/__init__.py
Normal file
16
myapp/ns_api/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
from flask_jsonrpc import JSONRPC
|
||||||
|
|
||||||
|
from .. import app
|
||||||
|
|
||||||
|
jsonrpc = JSONRPC(app, '/api')
|
||||||
|
|
||||||
|
from . import user
|
||||||
28
myapp/ns_api/user.py
Normal file
28
myapp/ns_api/user.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
from flask import abort, escape
|
||||||
|
|
||||||
|
from . import jsonrpc
|
||||||
|
from .. import models
|
||||||
|
|
||||||
|
|
||||||
|
@jsonrpc.method('users.getList')
|
||||||
|
def users_getList():
|
||||||
|
"""
|
||||||
|
Показать список пользователей
|
||||||
|
"""
|
||||||
|
users = models.db_session.query(
|
||||||
|
models.User
|
||||||
|
).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for item in users:
|
||||||
|
result.append(item.as_dict())
|
||||||
|
return result
|
||||||
21
myapp/ns_user/__init__.py
Normal file
21
myapp/ns_user/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
from .. import app
|
||||||
|
|
||||||
|
|
||||||
|
my_loader = jinja2.ChoiceLoader([
|
||||||
|
app.jinja_loader,
|
||||||
|
jinja2.FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + "/templates"),
|
||||||
|
])
|
||||||
|
app.jinja_loader = my_loader
|
||||||
67
myapp/ns_user/templates/users.html
Normal file
67
myapp/ns_user/templates/users.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{% extends "skeleton.html" %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<h3>Список пользователей</h3>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="btn btn-outline-primary" v-on:click="getUserList()">Получить/обновить список пользователей</div>
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
|
<div class="row" v-for="(user, userIdx) in users">
|
||||||
|
<div class="col py-2">
|
||||||
|
<a :href="'/user/' + user.id">{{ user.name }}</a>
|
||||||
|
|
||||||
|
<small class="text-muted">
|
||||||
|
<br />
|
||||||
|
Создан: {{ user.created }}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
|
<backtotop-component></backtotop-component>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
<ol class="breadcrumb mt-3">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="fa fa-home"></i></a></li>
|
||||||
|
<li class="breadcrumb-item">Список пользователей</li>
|
||||||
|
</ol>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script type="text/javascript" src="/static/components/backtotop.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/components/backtotop.css"></link>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
users: []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUserList: function() {
|
||||||
|
var vm = this;
|
||||||
|
axios.post(
|
||||||
|
'/api',
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": 'users.getList',
|
||||||
|
"params": {
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
).then(
|
||||||
|
function(response) {
|
||||||
|
if ('result' in response.data) {
|
||||||
|
vm.users = response.data['result'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
23
myapp/ns_user/views.py
Normal file
23
myapp/ns_user/views.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
from .. import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/users')
|
||||||
|
def users():
|
||||||
|
"""
|
||||||
|
Список пользователей
|
||||||
|
"""
|
||||||
|
pagedata = {}
|
||||||
|
pagedata['title'] = 'Список пользователей - ' + app.config['TITLE']
|
||||||
|
body = render_template('users.html', pagedata=pagedata)
|
||||||
|
return body
|
||||||
18
myapp/static/components/backtotop.css
Normal file
18
myapp/static/components/backtotop.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.scrollToTop{
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
opacity: 0.3;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.scrollToTop:hover{
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
.scrollToTop .card{
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-color: #7ca8b1;
|
||||||
|
}
|
||||||
53
myapp/static/components/backtotop.js
Normal file
53
myapp/static/components/backtotop.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
var backtotopTemplate = `
|
||||||
|
<div>
|
||||||
|
<span class="scrollToTop" style="z-index: 10;" v-show="visible" v-on:click="backToTop">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body py-2 px-2">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
Vue.component('backtotop-component', {
|
||||||
|
props: {
|
||||||
|
visibleoffset: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 600
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: backtotopTemplate,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
var vm = this;
|
||||||
|
window.addEventListener('scroll', this.catchScroll);
|
||||||
|
let currentScroll = document.documentElement.scrollTop || document.body.scrollTop
|
||||||
|
vm.visible = (currentScroll > 100);
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('scroll', this.catchScroll)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
catchScroll () {
|
||||||
|
var vm = this;
|
||||||
|
vm.visible = (window.pageYOffset > 100);
|
||||||
|
},
|
||||||
|
backToTop () {
|
||||||
|
var vm = this;
|
||||||
|
vm.scrollAnimate();
|
||||||
|
},
|
||||||
|
scrollAnimate: function() {
|
||||||
|
var vm = this;
|
||||||
|
let currentScroll = document.documentElement.scrollTop || document.body.scrollTop
|
||||||
|
if (currentScroll > 0) {
|
||||||
|
//alert(currentScroll);
|
||||||
|
window.requestAnimationFrame(vm.scrollAnimate)
|
||||||
|
window.scrollTo(0, Math.floor(currentScroll - (currentScroll / 5)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
8899
myapp/static/css/bootstrap.css
vendored
8899
myapp/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
myapp/static/css/bootstrap.min.css
vendored
Normal file
7
myapp/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2337
myapp/static/css/font-awesome.css
vendored
2337
myapp/static/css/font-awesome.css
vendored
File diff suppressed because it is too large
Load Diff
4
myapp/static/css/font-awesome.min.css
vendored
Normal file
4
myapp/static/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
myapp/static/js/axios.min.js
vendored
Normal file
9
myapp/static/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3892
myapp/static/js/bootstrap.js
vendored
3892
myapp/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
myapp/static/js/bootstrap.min.js
vendored
Normal file
7
myapp/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10253
myapp/static/js/jquery-3.2.1.js
vendored
10253
myapp/static/js/jquery-3.2.1.js
vendored
File diff suppressed because it is too large
Load Diff
2
myapp/static/js/jquery-3.3.1.slim.min.js
vendored
Normal file
2
myapp/static/js/jquery-3.3.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
5
myapp/static/js/popper.min.js
vendored
Normal file
5
myapp/static/js/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
myapp/static/js/vue.min.js
vendored
Normal file
6
myapp/static/js/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -3,10 +3,12 @@
|
|||||||
<meta name="author" content="Ремизов Александр" />
|
<meta name="author" content="Ремизов Александр" />
|
||||||
<meta name="copyright" lang="ru" content="RemiZOffAlex" />
|
<meta name="copyright" lang="ru" content="RemiZOffAlex" />
|
||||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" href="/static/css/bootstrap.css" />
|
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="/static/css/font-awesome.css" />
|
<link rel="stylesheet" href="/static/css/font-awesome.min.css" />
|
||||||
<script type="text/javascript" src="/static/js/jquery-3.2.1.js"></script>
|
<script type="text/javascript" src="/static/js/jquery-3.3.1.slim.min.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/popper.js"></script>
|
<script type="text/javascript" src="/static/js/popper.min.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/bootstrap.js"></script>
|
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/vue.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/axios.min.js"></script>
|
||||||
<title>{{ pagedata['title'] }}</title>
|
<title>{{ pagedata['title'] }}</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
7
myapp/templates/navbar.html
Normal file
7
myapp/templates/navbar.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="container-fluid py-2">
|
||||||
|
<a class="btn btn-outline-success border-0 float-right" href="/login"><i class="fa fa-sign-in"></i></a>
|
||||||
|
<a class="btn btn-outline-secondary border-0" href="/"><i class="fa fa-home"></i></a>
|
||||||
|
<a class="btn btn-outline-secondary border-0" href="/edit"><i class="fa fa-edit"></i> Редактор</a>
|
||||||
|
<a class="btn btn-outline-secondary border-0" href="/users">Vue & axios</a>
|
||||||
|
<a class="btn btn-outline-secondary border-0" href="/api/browse">API JSON-RPC</a>
|
||||||
|
</div>
|
||||||
@@ -2,13 +2,18 @@
|
|||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
<body>
|
<body>
|
||||||
|
<section id="app">
|
||||||
|
|
||||||
{% include 'topbar.html' %}
|
{% include 'navbar.html' %}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<div class="container-fluid py-2">
|
|
||||||
<a class="btn btn-outline-success float-right" href="/login"><i class="fa fa-sign-in"></i></a>
|
|
||||||
<a class="btn btn-outline-secondary" href="/"><i class="fa fa-home"></i></a>
|
|
||||||
<a class="btn btn-outline-secondary" href="/edit"><i class="fa fa-edit"></i></a>
|
|
||||||
</div>
|
|
||||||
58
utils/useradd.py
Executable file
58
utils/useradd.py
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
__author__ = 'RemiZOffAlex'
|
||||||
|
__copyright__ = '(c) RemiZOffAlex'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__email__ = 'remizoffalex@mail.ru'
|
||||||
|
__url__ = 'http://remizoffalex.ru'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
sys.path.insert(0, '/'.join(os.path.dirname(os.path.abspath(__file__)).split('/')[:-1]))
|
||||||
|
|
||||||
|
from myapp import app, lib, models
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Скрипт добавления пользователя',
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser._optionals.title = "Необязательные аргументы"
|
||||||
|
|
||||||
|
parser.add_argument("--user", dest="user", required=True, help="Новый пользователь")
|
||||||
|
parser.add_argument("--password", dest="password", help="Новый пароль")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
user = models.db_session.query(
|
||||||
|
models.User
|
||||||
|
).filter(
|
||||||
|
models.User.name==args.user
|
||||||
|
).first()
|
||||||
|
if user:
|
||||||
|
app.logger.warning('Пользователь %s уже существует' % args.user)
|
||||||
|
sys.exit(1)
|
||||||
|
user = models.User(args.user)
|
||||||
|
if args.password is None:
|
||||||
|
args.password = lib.pwgen()
|
||||||
|
print('Пароль пользователя: {}'.format(args.password))
|
||||||
|
user.password = lib.get_hash_password(
|
||||||
|
args.password,
|
||||||
|
app.config['SECRET_KEY']
|
||||||
|
)
|
||||||
|
user.disabled = False
|
||||||
|
models.db_session.add(user)
|
||||||
|
models.db_session.commit()
|
||||||
|
app.logger.info('Пользователь %s успешно добавлен' % args.user)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as err:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
exit(0)
|
||||||
Reference in New Issue
Block a user